• R/O
  • SSH
  • HTTPS

Commit

Tags
Keine Tags

Frequently used words (click to add to your profile)

javac++androidlinuxc#windowsobjective-ccocoa誰得qtpythonphprubygameguibathyscaphec計画中(planning stage)翻訳omegatframeworktwitterdomtestvb.netdirectxゲームエンジンbtronarduinopreviewer

PHPのフレームワークです。オートローディング、ルーティング、ORマッパ、フォームバリデータ、その他ユーティリティがセットになっています。


Commit MetaInfo

Revision180 (tree)
Zeit2022-08-17 13:56:59
Autortantancode

Log Message

バグ鳥等

Ändern Zusammenfassung

Diff

--- htdocs/js/ImageUploader.js (revision 179)
+++ htdocs/js/ImageUploader.js (nonexistent)
@@ -1,135 +0,0 @@
1-
2-/**
3- * 画像をアップロードするためのUIのロジックを収めるクラス。
4- * 次のようなHTMLで、縦横比を保ったまま適切なサイズにリサイズ、そのpngバイナリを data: スキーマで送信するようにする。
5- *
6- * この例では 300 x 200 のサイズに収まるようにリサイズされる。
7- * <div key="image-tile" data-width="300" data-height="200">
8- * <input type="hidden" name="foo" />
9- * <label>
10- * <input type="file" accept="image/*" onchange="ImageUploader.selected(this)" style="display:none" id="selector" />
11- * <canvas key="canvas" width="1" height="1"></canvas>
12- * </label>
13- * <div>
14- * <label for="selector">画像を変更</label>
15- * <span onclick="ImageUploader.clear(this)">画像を削除</span>
16- * </div>
17- * <div key="monitor" class="error">(エラーがここに表示される。)</div>
18- * </div>
19- */
20-class ImageUploader {
21-
22- //----------------------------------------------------------------------------------------------------------
23- /**
24- * <input type="hidden"> の現在値をプレビューキャンバスに反映する。画面表示時の初期値の反映などに使う。
25- *
26- * param 対象コントロールの key="image-tile" 要素をjQueryに渡せる形で。
27- */
28- static async initialize(tile) {
29-
30- var $tile = $(tile);
31-
32- // 現在の値を取得。入力されていないなら何もしない。
33- var data = $tile.find('input[type="hidden"]').val();
34- if(data.length == 0) return;
35-
36- // 現在の値を data: スキーマとしてオンメモリの<img>で読み込み。
37- var img = new Image();
38- img.src = data;
39- await new Promise(resolve => {img.onload = resolve;});
40-
41- // その画像をキャンバスに描く。
42- var canvas = $tile.find('canvas').get(0);
43- var ctx = canvas.getContext('2d');
44- ctx.clearRect(0, 0, canvas.width, canvas.height);
45- ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
46- }
47-
48- //----------------------------------------------------------------------------------------------------------
49- /**
50- * ファイル選択コントロールで値が変更されたときのonchangeイベントハンドラ。
51- * 選択された画像を検証、リサイズし、そのpngバイナリを data: スキーマで<input>フィールドにセットする。
52- *
53- * param イベント発火元 <input type="file" />。
54- */
55- static async selected(filer) {
56-
57- // このコントロールのルート要素、キャンバス要素、エラー表示カードを取得。
58- var $tile = $(filer).parents('[key="image-tile"]').eq(0);
59- var canvas = $tile.find('canvas').get(0);
60- var $monitor = $tile.find('[key="monitor"]');
61-
62- // 前回のエラー表示をクリアしておく。
63- $monitor.text("");
64-
65- // 入力されたファイルを取得。入力がクリアされた場合はプレビューキャンバスをクリアして終了。
66- var file = filer.files[0];
67- if(!file) {
68- canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
69- return;
70- }
71-
72- // 選択されたファイルがイメージファイルなのか確認。
73- if( !file.type.startsWith("image/") ) {
74- $monitor.text("画像を選択してください。");
75- return;
76- }
77-
78- // 大きすぎてコケるのは嫌なので…
79- if(10*1024*1024 < file.size) {
80- $monitor.text("ファイルサイズが大きすぎます。10MB以下の画像を選択してください。");
81- return;
82- }
83-
84- // 選択された画像をオンメモリの<img>で読み込み。
85- $monitor.text("ファイルを読み込んでいます...");
86- var img = new Image();
87- img.src = URL.createObjectURL(file);
88- await new Promise(resolve => {img.onload = resolve;});
89- URL.revokeObjectURL(img.src);
90- $monitor.text("");
91-
92- // 規定されているキャンバスサイズを変数 ruleWidth, ruleHeight に取得。
93- var ruleWidth = $tile.data("width"), ruleHeight = $tile.data("height");
94-
95- // 規定サイズに対する選択された画像のサイズ倍率を取得。大きい方の倍率を使って、縦横比を保ったまま拡大・縮小した場合の結果サイズを
96- // 変数 width, height に取得。
97- var ratio = Math.max(img.naturalWidth / ruleWidth, img.naturalHeight / ruleHeight);
98- var width = Math.floor(img.naturalWidth / ratio), height = Math.floor(img.naturalHeight / ratio);
99- if(width < 10 || height < 10) {
100- $monitor.text("画像が細すぎます。もう少し規定の縦横比に近づけてください。");
101- return;
102- }
103-
104- // プレビューキャンバスに画像をリサイズして描画。
105- canvas.width = width;
106- canvas.height = height;
107- var ctx = canvas.getContext('2d');
108- ctx.clearRect(0, 0, canvas.width, canvas.height);
109- ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
110-
111- // data: スキーマでバイナリを取得して、<input>フィールドへ代入する。
112- $tile.find('input[type="hidden"]').val( canvas.toDataURL() );
113- }
114-
115- //----------------------------------------------------------------------------------------------------------
116- /**
117- * 「画像を削除」リンクのクリックイベントハンドラ。
118- * プレビューキャンバスと<input>フィールドをクリアする。
119- *
120- * param 「画像を削除」要素。
121- */
122- static clear(clicked) {
123-
124- // このコントロールのルート要素、キャンバス要素を取得。
125- var $tile = $(clicked).parents('[key="image-tile"]').eq(0);
126- var canvas = $tile.find('canvas').get(0);
127-
128- // プレビューキャンバスをクリア。
129- canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
130-
131- // <input>フィールドをクリア。
132- $tile.find('input[type="file"]').val("");
133- $tile.find('input[type="hidden"]').val("");
134- }
135-}
--- htdocs/js/my/ImageUploader.js (nonexistent)
+++ htdocs/js/my/ImageUploader.js (revision 180)
@@ -0,0 +1,135 @@
1+
2+/**
3+ * 画像をアップロードするためのUIのロジックを収めるクラス。
4+ * 次のようなHTMLで、縦横比を保ったまま適切なサイズにリサイズ、そのpngバイナリを data: スキーマで送信するようにする。
5+ *
6+ * この例では 300 x 200 のサイズに収まるようにリサイズされる。
7+ * <div key="image-tile" data-width="300" data-height="200">
8+ * <input type="hidden" name="foo" />
9+ * <label>
10+ * <input type="file" accept="image/*" onchange="ImageUploader.selected(this)" style="display:none" id="selector" />
11+ * <canvas key="canvas" width="1" height="1"></canvas>
12+ * </label>
13+ * <div>
14+ * <label for="selector">画像を変更</label>
15+ * <span onclick="ImageUploader.clear(this)">画像を削除</span>
16+ * </div>
17+ * <div key="monitor" class="error">(エラーがここに表示される。)</div>
18+ * </div>
19+ */
20+class ImageUploader {
21+
22+ //----------------------------------------------------------------------------------------------------------
23+ /**
24+ * <input type="hidden"> の現在値をプレビューキャンバスに反映する。画面表示時の初期値の反映などに使う。
25+ *
26+ * param 対象コントロールの key="image-tile" 要素をjQueryに渡せる形で。
27+ */
28+ static async initialize(tile) {
29+
30+ var $tile = $(tile);
31+
32+ // 現在の値を取得。入力されていないなら何もしない。
33+ var data = $tile.find('input[type="hidden"]').val();
34+ if(data.length == 0) return;
35+
36+ // 現在の値を data: スキーマとしてオンメモリの<img>で読み込み。
37+ var img = new Image();
38+ img.src = data;
39+ await new Promise(resolve => {img.onload = resolve;});
40+
41+ // その画像をキャンバスに描く。
42+ var canvas = $tile.find('canvas').get(0);
43+ var ctx = canvas.getContext('2d');
44+ ctx.clearRect(0, 0, canvas.width, canvas.height);
45+ ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
46+ }
47+
48+ //----------------------------------------------------------------------------------------------------------
49+ /**
50+ * ファイル選択コントロールで値が変更されたときのonchangeイベントハンドラ。
51+ * 選択された画像を検証、リサイズし、そのpngバイナリを data: スキーマで<input>フィールドにセットする。
52+ *
53+ * param イベント発火元 <input type="file" />。
54+ */
55+ static async selected(filer) {
56+
57+ // このコントロールのルート要素、キャンバス要素、エラー表示カードを取得。
58+ var $tile = $(filer).parents('[key="image-tile"]').eq(0);
59+ var canvas = $tile.find('canvas').get(0);
60+ var $monitor = $tile.find('[key="monitor"]');
61+
62+ // 前回のエラー表示をクリアしておく。
63+ $monitor.text("");
64+
65+ // 入力されたファイルを取得。入力がクリアされた場合はプレビューキャンバスをクリアして終了。
66+ var file = filer.files[0];
67+ if(!file) {
68+ canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
69+ return;
70+ }
71+
72+ // 選択されたファイルがイメージファイルなのか確認。
73+ if( !file.type.startsWith("image/") ) {
74+ $monitor.text("画像を選択してください。");
75+ return;
76+ }
77+
78+ // 大きすぎてコケるのは嫌なので…
79+ if(10*1024*1024 < file.size) {
80+ $monitor.text("ファイルサイズが大きすぎます。10MB以下の画像を選択してください。");
81+ return;
82+ }
83+
84+ // 選択された画像をオンメモリの<img>で読み込み。
85+ $monitor.text("ファイルを読み込んでいます...");
86+ var img = new Image();
87+ img.src = URL.createObjectURL(file);
88+ await new Promise(resolve => {img.onload = resolve;});
89+ URL.revokeObjectURL(img.src);
90+ $monitor.text("");
91+
92+ // 規定されているキャンバスサイズを変数 ruleWidth, ruleHeight に取得。
93+ var ruleWidth = $tile.data("width"), ruleHeight = $tile.data("height");
94+
95+ // 規定サイズに対する選択された画像のサイズ倍率を取得。大きい方の倍率を使って、縦横比を保ったまま拡大・縮小した場合の結果サイズを
96+ // 変数 width, height に取得。
97+ var ratio = Math.max(img.naturalWidth / ruleWidth, img.naturalHeight / ruleHeight);
98+ var width = Math.floor(img.naturalWidth / ratio), height = Math.floor(img.naturalHeight / ratio);
99+ if(width < 10 || height < 10) {
100+ $monitor.text("画像が細すぎます。もう少し規定の縦横比に近づけてください。");
101+ return;
102+ }
103+
104+ // プレビューキャンバスに画像をリサイズして描画。
105+ canvas.width = width;
106+ canvas.height = height;
107+ var ctx = canvas.getContext('2d');
108+ ctx.clearRect(0, 0, canvas.width, canvas.height);
109+ ctx.drawImage(img, 0, 0, img.naturalWidth, img.naturalHeight, 0, 0, canvas.width, canvas.height);
110+
111+ // data: スキーマでバイナリを取得して、<input>フィールドへ代入する。
112+ $tile.find('input[type="hidden"]').val( canvas.toDataURL() );
113+ }
114+
115+ //----------------------------------------------------------------------------------------------------------
116+ /**
117+ * 「画像を削除」リンクのクリックイベントハンドラ。
118+ * プレビューキャンバスと<input>フィールドをクリアする。
119+ *
120+ * param 「画像を削除」要素。
121+ */
122+ static clear(clicked) {
123+
124+ // このコントロールのルート要素、キャンバス要素を取得。
125+ var $tile = $(clicked).parents('[key="image-tile"]').eq(0);
126+ var canvas = $tile.find('canvas').get(0);
127+
128+ // プレビューキャンバスをクリア。
129+ canvas.getContext('2d').clearRect(0, 0, canvas.width, canvas.height);
130+
131+ // <input>フィールドをクリア。
132+ $tile.find('input[type="file"]').val("");
133+ $tile.find('input[type="hidden"]').val("");
134+ }
135+}
--- htdocs/js/my/misc.js (nonexistent)
+++ htdocs/js/my/misc.js (revision 180)
@@ -0,0 +1,320 @@
1+
2+//----------------------------------------------------------------------------------------------------------
3+/**
4+ * 引数に指定されたURLをポップアップウィンドウで開く。
5+ *
6+ * @param URL、あるいは href 属性を持つHTML要素。
7+ * @param left,top,width,height などの指定。
8+ * @return onclick="return popupWindow(this)" と書けるように、固定でfalseが返る。
9+ */
10+function popupWindow(href, style) {
11+
12+ if(href instanceof HTMLElement)
13+ href = href.getAttribute('href');
14+
15+ window.open(href, '_blank', 'menubar=no,toolbar=no,scrollbars=yes,'+ style);
16+
17+ return false;
18+}
19+
20+//----------------------------------------------------------------------------------------------------------
21+/**
22+ * 引数で指定された要素の内容をクリップボードへコピーする。
23+ * Firefox で <a href="javascript: ..."> で呼び出すと "document.execCommand(‘cut’/‘copy’) was denied because it was not called from
24+ * inside a short running user-generated event handler." というエラーになる。onclick にする必要がある。
25+ */
26+function copyToClipboard(element) {
27+
28+ // 引数が文字列で指定されている場合は jQuery で要素を取得する。
29+ if(typeof element == "string")
30+ element = $(element).get(0);
31+
32+ // <input type="text"> などの場合は...
33+ if(element.select) {
34+
35+ element.select();
36+ document.execCommand('copy');
37+ element.blur();
38+
39+ // それ以外の要素は...
40+ }else {
41+
42+ // 子要素をコピーする。
43+ if(!element.firstChild) {
44+ console.error("指定された要素にコピーするべき内容がありません。");
45+ return;
46+ }
47+
48+ // あとはお決まりの流れ。
49+ var range = document.createRange();
50+ range.selectNode(element.firstChild);
51+
52+ var selection = getSelection();
53+ selection.removeAllRanges();
54+ selection.addRange(range);
55+
56+ document.execCommand('copy');
57+
58+ selection.removeAllRanges();
59+ }
60+}
61+
62+//----------------------------------------------------------------------------------------------------------
63+/**
64+ * 拡張属性の定義など。
65+ */
66+(function() {
67+
68+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
69+ // window.event を代表的イベントだけでも確実に使えるようにする。
70+ var listener = function(event){
71+ window.event = event;
72+ };
73+ document.addEventListener("focus", listener, true);
74+ document.addEventListener("click", listener, true);
75+ document.addEventListener("keypress", listener, true);
76+ document.addEventListener("input", listener, true);
77+ document.addEventListener("change", listener, true);
78+ document.addEventListener("submit", listener, true);
79+
80+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
81+ // <input type="number"> で step を 0.00000001 などの小さな値にした時、▲▼コントロールで e-8 表記になってしまうのを修正する。
82+ document.addEventListener("input", function(event){
83+
84+ // <input type="number"> で値に "e" が含まれている場合に...
85+ var input = event.target;
86+ if(input.tagName == "INPUT" && input.getAttribute("type") == "number" && input.value.includes("e")) {
87+
88+ // stepの小数桁数を取得して toFixed() で変換する。
89+ var step = input.getAttribute("step") || "1";
90+ var decimals = step.split("e-")[1] || (step.split(".")[1] || "").length; // stepが 1e-8 などとなっていても対応するが…ちょっと苦しい。
91+ input.value = parseFloat(input.value).toFixed(decimals);
92+ }
93+
94+ }, true);
95+
96+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
97+ // <a>, <button>, <input type="submit">、および、x-doubleclick="suppress" の属性が付いている要素にダブルクリックへの対抗策を施す。
98+ // <a>などにはデフォで適用されるので、解除したいタグには x-doubleclick="respect" を付ける。
99+ // ダブルクリック判定されるのは前回クリックから1秒だが、Runtime["dblclick_interval"] が定義されている場合はその値が使われる。
100+ document.addEventListener("click", function(event){
101+
102+ // x-doubleclick="respect" が付いているなら何もしない。
103+ if(event.target.getAttribute("x-doubleclick") == "respect") return;
104+
105+ // 処理対象でないなら何もしない。
106+ if( !$(event.target).is("a, button, input[type='submit'], [x-doubleclick='suppress']") ) return;
107+
108+ // 前回クリックから一定時間経過していないならイベントをキャンセルする。
109+ if(Date.now() < (event.target["x-last-click-time"] || 0) + (Runtime["dblclick_interval"] || 1000)) {
110+ console.log("ダブルクリック抑制コードにより、二回目のクリックをキャンセルしました。");
111+ event.preventDefault();
112+ event.stopPropagation();
113+ return;
114+ }
115+
116+ // 前回クリック時間として取っておく。
117+ event.target["x-last-click-time"] = Date.now();
118+ }, true);
119+
120+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
121+ // 拡張属性 x-href を定義。<button x-href="..."> のようにするとクリック時に <a href="..."> のように画面遷移できる。
122+ $(document).on("click", "button[x-href]", function(event){
123+ location.href = $(event.target).attr("x-href");
124+ });
125+
126+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
127+ // 拡張属性 x-method を定義。<a> に付けて値を "post" とすれば、POSTメソッドで遷移させることが出来る。
128+ $(document).on("click", "a[x-method='post']", function(event){
129+
130+ event.preventDefault();
131+
132+ // <form>を作成してセットアップ。
133+ var form = document.createElement("form");
134+ form.action = $(event.target).attr("href");
135+ form.method = "post";
136+ form.style.display = "none";
137+
138+ // DOMツリーに追加しないと submit できない...
139+ $(document.body).append(form);
140+ form.submit();
141+ });
142+
143+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
144+ // 拡張属性 x-frame-escape を定義。<a> に付けると、フレームとして画面が開かれている場合にそのフレームではなく新規タブでリンクを開くようになる。
145+ // 親フレームのユーティリティとして開かれている場合にそこから先に遷移させたくない <a> などに付ける。
146+ $(document).on("click", "a[x-frame-escape]", function(event){
147+ if(window.parent != window.self) {
148+ event.preventDefault();
149+ window.open( $(event.target).attr("href") );
150+ }
151+ });
152+
153+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
154+ // 拡張属性 x-proceed-confirm を定義。<form> に付けると送信時に確認ダイアログを出すことが出来る。
155+ // 値は確認時のメッセージだが、値が 空文字, "x-proceed-confirm", "true", "confirm" となっている場合はデフォルトのメッセージになる。また、"#" で
156+ // 始まる値の場合は、それをidに持つ要素がコピーされてメッセージとなる。
157+ // submitボタンにnameを付けていてもそのキーは送信できないので注意。
158+ // 確認ダイアログには、Runtime["confirmation_class"] が設定されているならそれを、設定されていないなら ConfirmDialogue を使う。
159+ // 確認後に送信した時、カスタムイベント "submit-seriously" が発生する。
160+ $(document).on("submit", "form[x-proceed-confirm]", function(event){
161+
162+ // x-oneshot 属性によるフラグが付いている場合は処理しない。
163+ if(event.target["x-oneshot-fired"]) return;
164+
165+ // メッセージを取得。
166+ var message = event.target.getAttribute("x-proceed-confirm");
167+ if( ["", "x-proceed-confirm", "true", "confirm"].includes(message) )
168+ message = "{:common-other#送信するか:}";
169+ else if( message.startsWith("#") )
170+ message = Ui.mold(message);
171+
172+ // 確認ダイアログを表示。
173+ var dialogue = Runtime.getClass("confirmation_class", ConfirmDialogue);
174+ var d = new dialogue();
175+ d.message = message;
176+ d.open();
177+
178+ // 「OK」されたら、フォームの送信とカスタムイベントの発行。
179+ d.onProceed = ()=>{
180+ event.target.submit();
181+ $(event.target).trigger("submit-seriously");
182+ };
183+
184+ // 送信自体は一度キャンセルする必要がある。
185+ event.stopPropagation();
186+ event.preventDefault();
187+ });
188+
189+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
190+ // 拡張属性 x-oneshot を定義。<form> に付けると、一度しか送信できなくなる。
191+ $(document).on("submit submit-seriously", "form[x-oneshot]", function(event){
192+
193+ // x-proceed-confirm でキャンセル処理が行われているなら処理しない。stopPropagation() しても同じ要素に設定されているハンドラまでは
194+ // 伝わってしまうので…
195+ if(event.isPropagationStopped()) return;
196+
197+ // すでに送信されている場合は...
198+ if(event.target["x-oneshot-fired"]) {
199+ event.stopPropagation();
200+ event.preventDefault();
201+ console.log("フォームを送信しようとしましたが、すでに送信されているため x-oneshot 属性によってキャンセルされました。")
202+ return;
203+ }
204+
205+ // 送信済みのマークを付ける。
206+ event.target["x-oneshot-fired"] = true;
207+
208+ // Firefoxだとブラウザバックしたときにフラグが生きたままなので、対処する必要がある。
209+ // onunload?せっかくのメモリキャッシュだし…
210+ window.addEventListener('pagehide', ()=>event.target["x-oneshot-fired"]=false, {"once":true});
211+ });
212+
213+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
214+ // 拡張属性 x-interlude を定義。<form> に付けると、値に指定した時間が経過後、まだ画面遷移できていないならローディングインジケータが表示される。
215+ // 値を省略した場合はデフォルトのタイムアウト値が使われる。
216+ $(document).on("submit submit-seriously", "form[x-interlude]", function(event){
217+
218+ // x-proceed-confirm でキャンセル処理が行われているなら処理しない。stopPropagation() しても同じ要素に設定されているハンドラまでは
219+ // 伝わってしまうので…
220+ if(event.isPropagationStopped()) return;
221+
222+ // ローディングインジケータに使うクラスを取得。
223+ var indicator = Runtime.getClass("indicator_class", LoadingIndicator);
224+
225+ // 属性の値に設定されたタイムアウトでインジケータを表示する。
226+ // 空文字や、XHTML風に値に "x-interlude" が設定されていた場合でも、これでデフォルト処理される。
227+ var interlude = indicator.startInterlude( Number(event.target.getAttribute("x-interlude")) );
228+
229+ // Firefoxだとブラウザバックしたときにインジケーターが出っぱなしになっているので、対処する必要がある。
230+ // onunload?せっかくのメモリキャッシュだし…
231+ window.addEventListener('pagehide', ()=>interlude.finish(), {"once":true});
232+ });
233+
234+ //- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
235+ // スクリプトエラーが発生した場合の処理を記述。
236+ var errorEater = function(error) {
237+
238+ // 前回発生したエラーと同じ内容ならスルー。
239+ if(window["x-error-previous"] && window["x-error-previous"] == error.message)
240+ return;
241+
242+ // 次回参照するために取っておく。
243+ window["x-error-previous"] = error.message;
244+
245+ // 1分以内に連続して発生したエラーの数を数えるので、最初のエラーからそれ以上経過しているならカウンタをリセットする。
246+ var now = Date.now();
247+ if(!window["x-error-time"] || window["x-error-time"] + 60*1000 <= now) {
248+ window["x-error-time"] = now;
249+ window["x-error-count"] = 0;
250+ }
251+
252+ // 連続エラー発生回数をカウント。3回を超えたならサーバには報告しない。
253+ if(3 < ++window["x-error-count"]) return;
254+
255+ // エラー報告APIへのリクエストを作成。
256+ error["useragent"] = navigator.userAgent;
257+ var api = new Api("user/sorry.report", null, error);
258+
259+ // この挙動をユーザには見せないようにする。
260+ api.loadingAttendant = false;
261+ api.errorAttendant = false;
262+
263+ // 実行。ここでエラーになると無限ループすることになるので、阻止する。
264+ try {
265+ api.transmit().catch(console.warn);
266+ }catch(e) {
267+ console.warn(e);
268+ }
269+ };
270+
271+ window.addEventListener('error', function(event){
272+ errorEater({
273+ "filename":event.filename, "lineno":event.lineno, "colno":event.colno,
274+ "message":event.message, "stacktrace":event.error && event.error.stack,
275+ });
276+ });
277+
278+ window.addEventListener('unhandledrejection', function(event){
279+
280+ var reason = event.reason;
281+
282+ if(reason instanceof Error)
283+ var error = {"message":`Unhandled rejection: ${reason.message}`, "stacktrace":reason.stack};
284+ else
285+ var error = {"message":`Unhandled rejection: ${reason.toString()}`};
286+
287+ errorEater(error);
288+ });
289+
290+})();
291+
292+//----------------------------------------------------------------------------------------------------------
293+/**
294+ * DOMContentLoaded で実行される。
295+ * $(document).ready() を使ってると、他の <script defer src="..."> で読み込んだ外部jsの関数を参照できないときがある…
296+ */
297+document.addEventListener("DOMContentLoaded", function() {
298+
299+ // "x-preface-", "x-warn-" で始まるURLハッシュが付いている場合は、そのハッシュが示すidを持つ要素をメッセージとしてトースト表示する。
300+ if( location.hash.startsWith("#x-preface-") || location.hash.startsWith("#x-warn-") ) {
301+
302+ // ハッシュが示すidを持つ要素があるときのみ処理する。
303+ var message = $(location.hash);
304+ if(message.length > 0) {
305+
306+ // メッセージ表示に使うクラスを取得。
307+ var toaster = location.hash.startsWith("#x-preface-") ? Runtime.getClass("toaster_class", Toaster) : Runtime.getClass("dialogue_class", Dialogue);
308+
309+ // メッセージを表示。
310+ toaster.showMessage( message.html() );
311+
312+ // URLハッシュを削除してリロードやヒストリーバックで再表示されないようにする。
313+ location.replace("#");
314+ }
315+ }
316+
317+ // initializeLayout(), initializePage() という関数があるならそれをコールする。
318+ if(typeof window["initializeLayout"] == "function") initializeLayout();
319+ if(typeof window["initializePage"] == "function") initializePage();
320+});
--- lib/Localizer.php (revision 179)
+++ lib/Localizer.php (revision 180)
@@ -440,11 +440,13 @@
440440 */
441441 private function readyTranslatedScript($path) {
442442
443- // 翻訳前のパスと翻訳後のパスを取得。例えば、優先言語 ja で対象パスが some/foo.js なら cgm/transjs/some/foo.ja.js となる。
444- $tran = 'cgm/transjs/' . preg_replace('/\.\w+$/', ".{$this->langOrder[0]}$0", $path);
445- $pathphys = AppMojo::path("htdocs/$path");
446- $tranphys = AppMojo::path("htdocs/$tran");
443+ // 翻訳後の htdocs/ からの相対パスを取得。例えば、優先言語 ja で対象パスが some/foo.js なら cgm/translate/some/foo.ja.js となる。
444+ $tran = 'cgm/translate/' . preg_replace('/\.\w+$/', ".{$this->langOrder[0]}$0", $path);
447445
446+ // 翻訳前のパスと翻訳後の物理パスを取得。
447+ $pathphys = AppMojo::path("htdocs/{$path}");
448+ $tranphys = AppMojo::path("var/{$tran}");
449+
448450 // 翻訳前のパスにファイルがないなら、引数のパスをそのまま返す。
449451 if( !leaf_exists($pathphys) )
450452 return $path;
@@ -452,12 +454,21 @@
452454 // 翻訳後のパスにファイルがない、あるいは古い場合は翻訳して準備する。
453455 // WEBサーバが分散されてると問題になりそうに見えるが、その場合 var/cgm はNFSなどで共有されるので問題ない。
454456 if(!leaf_exists($tranphys) || filemtime($tranphys) < filemtime($pathphys)) {
457+
458+ // エンコード方式の決定。リクエストされているファイルの拡張子に従う。
459+ $encode = match( strtolower(path_extension($path)) ) {
460+ '.js' => 'js',
461+ '.htm', '.html' => 'html',
462+ default => null,
463+ };
464+
465+ // ファイルの内容を読み込んで、テキストシンボルを置換、キャッシュファイルに出力する。
455466 $text = file_get_contents($pathphys);
456- $text = SmartyRenderer::expandSmartyTemplate($text, array());
467+ $text = $this->processSymbolicText($text, $encode);
457468 file_dig_contents($tranphys, $text);
458469 }
459470
460- // 翻訳後のパスを返す。
471+ // 翻訳後の htdocs/ からの相対パスを返す。
461472 return $tran;
462473 }
463474
--- resources/smarty.plugin/function.asset_url.php (revision 179)
+++ resources/smarty.plugin/function.asset_url.php (revision 180)
@@ -13,7 +13,7 @@
1313 */
1414 function smarty_function_asset_url($params, $template) {
1515
16- if(@$params['path'])
16+ if( isset($params['path']) )
1717 $url = UrlUtil::assetUrl($params['path']);
1818 else
1919 $url = UrlUtil::assetUrl($params['cat'], $params['id'] ?? @$params['arg1'], $params['type'] ?? @$params['arg2'], @$params['arg3']);
--- sites/admin/layout.html (revision 179)
+++ sites/admin/layout.html (revision 180)
@@ -61,6 +61,7 @@
6161
6262 {[block name="body"]}
6363
64+ {[* 左側メニューパネル *]}
6465 {[block name="navigator"]}
6566
6667 {[if $navigation|default:true]}
@@ -103,6 +104,7 @@
103104 {[/if]}
104105 {[/block]}
105106
107+ {[* 左側メニューパネルを除いたページ全体。上部のナビゲーションタイトル部分も含む。 *]}
106108 {[block name="main"]}
107109 <div id="page-main">
108110
@@ -117,6 +119,7 @@
117119 {[/if]}
118120
119121 <div id="page-content">
122+ {[* ナビゲーションやタイトル等を除いたページ全体。各ページの主要な内容となる。 *]}
120123 {[block name="content"]}{[/block]}
121124 </div>
122125