グローバルJavaScript入力欄をAceエディタ化 & jQuery対応 [geoEditor-1-2]
下記記事で紹介したスクリプトの改良版です(v.2)。
usidesu.hatenablog.com 主な改善点
- エディタを導入すると、jQueryが使えるようになります。ggbOnInit関数内でも使えます。
- エディタに記述したスクリプトにエラーがあっても、次回読み込み時にエディタが維持され、元のグローバルJavaScirpt欄に戻らないようにしました。
- コンソールへのエラーメッセージ表示機能を充実させました。
導入方法
グローバルJavaScript記述欄に、本記事末尾のスクリプトをコピペして、アプレットを保存する。
エディター導入前に、すでにグローバルJavaScript欄にスクリプトを記述している場合は、そのスクリプトを一旦メモ帳などにコピーしておき、エディター導入後に、再びペーストしてください。
ブラウザを更新することで、アプレットを再読み込みすれば、グローバルJavaScript記述欄がJSエディタ化する。
記述後は、上部の「//クリックして確定//」というボタンを押せば、記述内容を確定できる。その後、アプレットを再読み込みすれば、記述したスクリプトが使えるようになる。
デフォルトでは、エラー行番号とエディタ上の行番号が一致しません。
一致させたい場合は、下記スクリプト中「// firstLineNumber: ...」をコメントインしてください。
なお、下記スクリプトに変更を加えた場合は、「firstLineNumber:」直後の数字部分を、スクリプトの全行数に揃えてください。
無効化の方法
①エディタの任意の箇所に「// disable_usiusi」とコメントする(カギカッコ不要) 。
②エディタ上部の確定ボタンを押し、アプレットを保存する。
③アプレットを再読込して、グローバルJS入力欄が元に戻っていることを確認する。
④グローバルJS入力欄に書かれたスクリプトを全て削除する。これまでエディタに記述したスクリプトを引き続き使用したい場合は、それをグローバルJS入力欄にコピペする。そして、アプレットを保存する。
再導入したい場合
上記ステップ③まで実行した段階で、エディタを再導入したい場合は、グローバルJS入力欄のコメント「// disable_usiusi」を削除して、アプレットを再読み込みしてください。
ステップ④まで実行した段階で、エディタを再導入したい場合は、本記事上述「導入方法」に従って再導入してください。
スクリプト
// クリックして確定 // // -------------------------ウインドウクリック時に1回だけ実行----------------------------------------------------------------------- window.addEventListener("pointerdown", usiusi_usiOnInit); // ウインドウクリック時 function usiusi_usiOnInit() { if (location.href.indexOf("https://www.geogebra.org/classic/") >= 0) { // web版GeoGebra Classicにのみ対応 usiusi_insertJS(); // aceエディタ動作に必要なJavaScriptをインポート usiusi_mutationFunc(); // (GeoGebraが提供する既存の)グローバルJS入力欄がdefineされるのを監視し、defineされたらaceエディタを仕込む } window.removeEventListener("pointerdown", usiusi_usiOnInit); } // --------------------------------------------------------------------------------------------------------------------------- // -------------------------jQueryをインポート-------------------------------------- // function importjQuery() { // document.head.appendChild((function () { // var jq = document.createElement("script"); // jq.src = '//ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js'; // return jq; // })()); // } // ------------------------------------------------------------------------------- // ------------------------必要なJSを読み込み----------------------------------------------------------- function usiusi_insertJS() { const scriptUrl1 = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js"; const scriptUrl2 = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ext-language_tools.min.js"; const scriptUrl3 = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/theme-sqlserver.min.js"; // const scriptUrl3 = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/theme-monokai.min.js"; const scriptUrl4 = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/mode-javascript.min.js"; const scriptUrl5 = "https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ext-beautify.min.js"; var el1 = document.createElement('script'); el1.src = scriptUrl1; document.head.appendChild(el1); var el2 = document.createElement('script'); el2.src = scriptUrl2; document.head.appendChild(el2); var el3 = document.createElement('script'); el3.src = scriptUrl3; document.head.appendChild(el3); var el4 = document.createElement('script'); el4.src = scriptUrl4; document.head.appendChild(el4); var el5 = document.createElement('script'); el5.src = scriptUrl5; document.body.appendChild(el5); } // ------------------------------------------------------------------------------------------------- // ------------------------グローバルJSテキストエリアの真上にdivを挿入---------------------- function usiusi_insertDivToGlobalJsDiv() { var globalJsArea = document.getElementsByClassName("scriptArea")[2]; var globalJsDiv = globalJsArea.parentElement; // var rect = globalJsArea.getBoundingClientRect() // 絶対位置が知りたいときは使え var newDiv = document.createElement("div"); // newDiv.style.backgroundColor = "red"; globalJsDiv.appendChild(newDiv); newDiv.id = "editor"; newDiv.className = "noGGB"; // globalJsDiv.appendChild(globalJsArea); // 順番入れ替え newDiv.style.width = "100%"; newDiv.style.height = "1000px"; newDiv.style.resize = "none"; newDiv.style.overflow = "scroll"; } // ----------------------------------------------------------------------------------- // -------body直下にdivを挿入、絶対位置でグローバルJSテキストエリアの真上に置く----- // function insertDivToBody() { // var newDiv = document.createElement("div"); // // newDiv.style.backgroundColor = "red"; // document.body.appendChild(newDiv); // newDiv.id = "editor"; // newDiv.style.position = "absolute"; // newDiv.style.width = "200px"; // newDiv.style.height = "200px"; // newDiv.style.top = "0px"; // newDiv.style.left = "0px"; // newDiv.style.resize = "both"; // newDiv.style.overflow = "scroll"; // newDiv.addEventListener("mouseover", usiusi_makeEditor); // } // ----------------------------------------------------------------------- // ------------------------挿入したdivをaceエディタにする--------------------- function usiusi_makeEditor() { var editor = ace.edit("editor"); // editor.setTheme("ace/theme/monokai"); editor.setTheme("ace/theme/sqlserver"); editor.getSession().setMode("ace/mode/javascript"); editor.setFontSize(16); editor.setOptions({ // firstLineNumber: 416, // エラー行番号とのズレを解消したいときはコメントアウト(数値は、本コードの最終行番号に揃えること) enableBasicAutocompletion: true, enableSnippets: true, enableLiveAutocompletion: true, showFoldWidgets: false }); } // ----------------------------------------------------------------------- // ------------------------GeoGebra側指定のCSS適用を回避------------------------------------------- // これをしないと、エディタの入力欄が真っ白になり、入力内容が見えなかった。 // エディタ関連要素に、クラス「noGGB」を設定し、GeoGebraFrameクラスの全要素対象のCSS「.GeoGebraFrame *{...}」に、否定疑似クラス「:not(.noGGB)」を追加する。 // なお、spanに適用されるCSSは、GeoGebra側指定のままである。当該CSSが指定するフォントサイズと、エディタのフォントサイズ(関数「usiusi_makeEditor()」内「editor.setFontSize(16);」)が合わないと、カーソルがずれる。 function usiusi_addClass() { var linkedCss = document.querySelector("#simple-bundle"); // linkタグ[#simple-bundle]を探す if (linkedCss) { // 例外追加したいCSSが、linkタグ[#simple-bundle]の外部リンクとして導入されている場合 var cssUrl = linkedCss.href; // 外部リンクのURLを取得 // var cssUrl = "https://www.geogebra.org/apps/latest/css/bundles/simple-bundle.css"; linkedCss.remove(); // 外部リンクCSS削除 var newCss = document.createElement('style'); // 差し替え用のCSS newCss.className = "ggw_resource"; // URLからCSSのテキストデータを取得し、セレクタ「.GeoGebraFrame *{...}」を、否定疑似クラス付きセレクタ「.GeoGebraFrame *:not(.noGGB){...}」に変更し、差し替え用のCSSの中身として利用する。 fetch(cssUrl) // (1) リクエスト送信 .then(response => response.text()) // (2) レスポンスデータを取得 .then(data => { // (3)レスポンスデータを処理 newCss.innerText = data.replace("*", "*:not(.noGGB)"); }); document.head.appendChild(newCss); //headに差し替え用CSSをインポート var bundleCss = document.querySelector("#bundle"); // もともとのlinkedCssに優先して適用されていたCSS。先ほどnewCssをインポートしたことで、bundleCssの適用順は、newCssに劣後している。そこで、bundleCssを一旦削除→再インポートすることで、newCssに優先して適用させる。 bundleCss.remove(); document.head.appendChild(bundleCss); // bundleCss優先適用 console.log("[#simple-bundle] exists. so remove it and inported CSS with :not(.noGGB)."); } else { // 例外追加したいCSSが、外部リンクではなくstyleタグに直接記述してある場合 document.querySelector("head > style.ggw_resource").textContent = document.querySelector("head > style.ggw_resource").textContent.replace("*", "*:not(.noGGB)"); // セレクタ「.GeoGebraFrame *{...}」を、否定疑似クラス付きセレクタ「.GeoGebraFrame *:not(.noGGB){...}」に変更する console.log("[#simple-bundle] undefined. so edited [head > style.ggw_resource]."); } var targets = document.getElementById("editor").querySelectorAll("div, span"); [...targets].forEach(v => // console.log(v.className) v.className = "noGGB " + v.className ); document.getElementById("editor").addEventListener("mousemove", function () { // クラス設定 var targets = document.getElementById("editor").querySelectorAll("div, span"); [...targets].forEach(v => { if (v.className.indexOf("noGGB") < 0) { v.className = "noGGB " + v.className; } }); }); // 行を加除した際のDOM変更に対応。その都度クラス「NoGGB」を設定する。 // 監視対象ノード const mutationTarget = document.getElementById("editor"); // インスタンス const observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { // console.log(n); // 警告アイコンがちゃんと表示されるように、レイアウト調整 var target = document.getElementById("editor"); target.getElementsByClassName("ace_scroller")[0].style.left = "50px"; target.getElementsByClassName("ace_gutter")[0].style.width = "50px"; target.getElementsByClassName("ace_gutter-layer")[0].style.width = "50px"; // クラス設定 var targets = document.getElementById("editor").querySelectorAll("div, span"); [...targets].forEach(v => { if (v.className.indexOf("noGGB") < 0) { v.className = "noGGB " + v.className; } }); }); }); // オブザーバ const config = { childList: true, subtree: true, attributes: true }; observer.observe(mutationTarget, config); } // --------------------------------------------------------------------------------------------- // -----------------------関数を引数として、その関数が使用する引数名一覧を配列で取得----------------------- function getParams(func) { var source = func.toString() .replace(/\/\/.*$|\/\*[\s\S]*?\*\/|\s/gm, ''); // strip comments if (!source.match(/\((.*?)\)/)) { // アロー関数かつ引数丸括弧省略時 return [source.toString().substr(0, source.toString().indexOf("=>"))]; } var params = source.match(/\((.*?)\)/)[1].split(','); if (params.length === 1 && params[0] === '') { return []; } return params; } // --------------------------------------------------------------------------------------------- // -----------------------もともとのグローバルJSテキストエリアと、新たに作ったエディタとの間の、データやりとりを設定------------------------------ function usiusi_attach() { var globalJsArea = document.getElementsByClassName("scriptArea")[2]; // もともとのテキストエリア var editor = ace.edit("editor"); // エディタ // エディタ導入時:ユーザーが入力したスクリプトをエディタに表示 var userStartingMark = "// userScript" + "_below"; // 本コード末尾の、ユーザー入力スクリプトの開始文句 var userInputScript; // ユーザーが入力したスクリプトのテキストデータを格納 if (typeof getEditorText == "function") { // ユーザーが入力したスクリプトにエラーがあり、そのまま読み込むとJSエディタ読み込みができないおそれがある場合→後述のエディタ入力時処理で作成したグローバル関数getEditorText()を用いて、テンプレート化により読み込み回避されたユーザー入力スクリプトを呼び出す userInputScript = getEditorText(); try { var errorCheck_userInputScript = Function(userInputScript)(); } catch (e) { console.error(e); console.error("上記エラーを解消するには、JavaScriptエディタに入力したスクリプトのエラーを解決してください。"); } } else { // ユーザーが入力したスクリプトが正常に読み込める状態の場合 userInputScript = globalJsArea.value.substring(globalJsArea.value.indexOf(userStartingMark) + userStartingMark.length + 2); // ユーザー入力スクリプトの開始文句以下に記載された、ユーザー入力スクリプト } userInputScript = userInputScript.replace(/usiusi_userOnInit/g, "ggbOnInit"); // [jQuery対応用] ユーザー入力スクリプト中「usiusi_userOnInit」を「ggbOnInit」に書き換える。エディタ上における外見上は「ggbOnInit」であっても、以下のエディタ入力時処理のところで、「usiusi_userOnInit」に改名して評価させる。 editor.setValue(userInputScript); // エディタ内容をセット。ユーザー入力スクリプトのみを表示するようにしている。 // エディタ入力時:エディタへの入力内容(ユーザー入力スクリプト)を、もともとのテキストエリアに反映 var endingMark = "// usiusi" + "_end"; // 本コード末尾の、うし記述スクリプトの終わり文句 var usiusiScript = globalJsArea.value.substring(0, globalJsArea.value.indexOf(endingMark) + endingMark.length); // 本コード1行目~終わり文句(すなわち、うし記述スクリプト部分) var editorTextArea = document.getElementsByClassName("ace_text-input")[0]; // キーイベント登録対象要素 function syncglobalJsArea() { var editorText = editor.getValue(); // 現在の、エディタの入力データ(ユーザー入力スクリプト) try { var errorCheck_editorText = Function(editorText)(); // もし現在のエディタの入力データにエラーがあり、そのまま保存され、アプレットを再読込してしまうと、JSエディタ化自体も道連れで失敗する可能性がある。それに対処する。 // エディタ入力内容が正常である場合の処理 try { var userFunc = Function(editorText + "return ggbOnInit;")(); // エディタ入力データ中の「ggbOnInit」関数を取得(関数が未定義のときは、ここからcatch文へ飛ぶ) // エディタ入力データ中の「ggbOnInit」関数を正常に取得できた場合の処理 // [jQuery対応用] もともとのテキストエリアのggbOnInit関数の引数名一覧を、エディタ入力データ中の「ggbOnInit」関数の引数名一覧に同期 var userFunc_arguments = getParams(userFunc); // その関数の引数名を配列として取得 var userFunc_arguments_joinText = userFunc_arguments.join(", "); // カンマ+半角スペースで結合 var ggbOnInitStartingMark = "// usiusi_ggbOnInit" + "_start"; // うし記述スクリプト内のggbOnInit関数定義の開始文句 var ggbOnInitEndingMark = "// usiusi_ggbOnInit" + "_end"; // うし記述スクリプト内のggbOnInit関数定義の終了文句 var ggbOnInitStringAtUsiusiScript = usiusiScript.substring(usiusiScript.indexOf(ggbOnInitStartingMark) + ggbOnInitStartingMark.length, usiusiScript.indexOf(ggbOnInitEndingMark)); // うし記述スクリプト内のggbOnInit関数定義式を文字列で取得 var ggbOnInitStringAtUsiusiScript_newArgs = "" + "\n" + "function ggbOnInit(" + userFunc_arguments_joinText + ") {" + "\n" + " var jq = document.createElement(\"script\");" + "\n" + " jq.src = '//ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js';" + "\n" + " document.head.appendChild(jq);" + "\n" + " jq.onload = function () {" + "\n" + " try {" + "\n" + " usiusi_userOnInit(" + userFunc_arguments_joinText + ");" + "\n" + " } catch (e) {" + "\n" + " console.error(e.name + \": \" + e.message + \"\\n at: ggbOnInit\");" + "\n" + " }" + "\n" + " }" + "\n" + "}" + "\n" ; // うし記述スクリプト内のggbOnInit関数に記述された、引数名宣言部分(関数冒頭とonload内の2箇所)を、エディタ入力データ中の「ggbOnInit」関数の引数名に揃えたもの // var ggbOnInitStringAtUsiusiScript_newArgs = ggbOnInitStringAtUsiusiScript.replace(/\(([\s\S]*?)\)/,userFunc_arguments_text); // (※onload内の引数名までは変更できないので却下) usiusiScript = usiusiScript.replace(ggbOnInitStringAtUsiusiScript, ggbOnInitStringAtUsiusiScript_newArgs); // console.log("ggbOnInit関数の引数一覧を同期しました。"); } catch (error) { // エディタ入力データ中の「ggbOnInit」関数を正常に取得できなかった場合の処理 // エラー出すだけで、引数名はいじらない。 // console.log(error); // console.log("Now ggbOnInit at script made by user seems to be undefind. So arguments of function ggbOnInit at script made by usi are left as they are."); } var editorText_replaced = editorText.replace(/ggbOnInit/g, "usiusi_userOnInit"); // [jQuery対応用] エディタ入力データ中「ggbOnInit」を「usiusi_userOnInit」に書き換える。ホンモノのggbOnInitは、うし記述スクリプトの中にある。 globalJsArea.value = usiusiScript + "\n\n" + userStartingMark + "\n\n" + editorText_replaced; // console.log("グローバルJSテキストエリアのvalueを、エディタの内容に同期しました。変更を確定するには、確定ボタンを押してください。"); } catch (error) { // エディタ入力内容が異常である場合の処理 // editorTextを丸ごとテンプレートリテラル化して、グローバル関数getEditorTextで包み、アプレット読み込み時に取り出せるようにする // console.error(error); var editorText_bqEscape = editorText.replace(/\`/g, String.raw`\``); // editorTextにあるバッククオートをエスケープ var editorText_template = "\`" + editorText_bqEscape + "\`"; var editorText_wrappedByFunction = "function getEditorText(){\n return " + editorText_template + ";\n}" // [jQuery対応用] もともとのテキストエリアのggbOnInit関数を設定 var ggbOnInitStartingMark = "// usiusi_ggbOnInit" + "_start"; // うし記述スクリプト内のggbOnInit関数定義の開始文句 var ggbOnInitEndingMark = "// usiusi_ggbOnInit" + "_end"; // うし記述スクリプト内のggbOnInit関数定義の終了文句 var ggbOnInitStringAtUsiusiScript = usiusiScript.substring(usiusiScript.indexOf(ggbOnInitStartingMark) + ggbOnInitStartingMark.length, usiusiScript.indexOf(ggbOnInitEndingMark)); // うし記述スクリプト内のggbOnInit関数定義式を文字列で取得 var ggbOnInitStringAtUsiusiScript_disableUsiusi_userOnInit = "" + "\n" + "function ggbOnInit() {" + "\n" + " var jq = document.createElement(\"script\");" + "\n" + " jq.src = '//ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js';" + "\n" + " document.head.appendChild(jq);" + "\n" + "}" + "\n" ; usiusiScript = usiusiScript.replace(ggbOnInitStringAtUsiusiScript, ggbOnInitStringAtUsiusiScript_disableUsiusi_userOnInit); // console.log("エディタ上の入力内容にエラーがあるため、一時的に、ggbOnInit関数内のusiusi_userOnInitを無効化しました。") globalJsArea.value = usiusiScript + "\n\n" + userStartingMark + "\n\n" + editorText_wrappedByFunction; // console.log("エディタ上の入力内容にエラーがあるため、一時的に、現在の入力内容をテンプレートリテラル化し、グローバル関数「getEditorText()」で取得できるようにしました。これで、次回アプレット読み込み時のJSエディタ実装への影響を回避できます。"); } } editorTextArea.addEventListener("keyup", syncglobalJsArea); //エディタダブルクリック時:コードを整形(拡張機能「beautify」を使用) document.getElementById("editor").addEventListener("dblclick", function () { var beautify = ace.require("ace/ext/beautify"); beautify.beautify(editor.session); // コードを整形 syncglobalJsArea(); }); } // ------------------------------------------------------------------------------------------------------------------------------- // -----------------------グローバルJSテキストエリアを、実行ボタンに仕立てる------------------------------ function usiusi_makeGlobalJsAreaExecuteButton() { var globalJsArea = document.getElementsByClassName("scriptArea")[2]; globalJsArea.readOnly = true; globalJsArea.style.resize = "none"; globalJsArea.style.width = "100%"; globalJsArea.style.overflow = "hidden"; globalJsArea.style.height = "25px"; globalJsArea.style.backgroundColor = "#afeeee"; // ペールターコイズ } // --------------------------------------------------------------------------------------------- // ------------------------位置を同期------------------------------------------------------------- // function usiusi_syncPosition() { // var globalJsArea = document.getElementsByClassName("scriptArea")[2]; // if (!globalJsArea) { return; } // var rect = globalJsArea.getBoundingClientRect(); // var newDiv = document.getElementById("editor"); // newDiv.style.width = rect.width + "px"; // newDiv.style.height = rect.height + "px"; // newDiv.style.top = rect.top + "px"; // newDiv.style.left = rect.left + "px"; // } // --------------------------------------------------------------------------------------------- // ----------------------グローバルJS入力欄がdefineされた際に、エディタを仕込む(あるいはエディタを無効化する)----------------------------- function usiusi_mutationFunc() { // 監視対象ノード const target = document.body; // インスタンス const observer = new MutationObserver((mutations) => { var globalJsArea = document.getElementsByClassName("scriptArea")[2]; // (既存の)グローバルJS入力欄 // console.log("searching..."); if (globalJsArea) { // グローバルJS入力欄がdefineされたら実行 observer.disconnect(); // 監視を終了する var userStartingMark = "// userScript" + "_below"; // 本コード末尾の、ユーザー入力欄の開始文句 var userInputScript = globalJsArea.value.substring(globalJsArea.value.indexOf(userStartingMark) + userStartingMark.length + 2); // ユーザー入力スクリプトの開始文句以下に記載された、ユーザー入力スクリプト var disableCommand = "// disable" + "_usiusi"; // エディタ無効化文句(エディタにこれをコメントすると、次回読み込み時以降はエディタが仕込まれない) if (userInputScript.indexOf(disableCommand) >= 0) { console.log("エディタが正常に無効化されました。\n引き続き、以下の手順を実行してください。\n[手順]グローバルJS入力欄の1行目から、コメント「" + userStartingMark + "」までを削除し、アプレットを保存する。[手順おわり]\nなお、コメント「" + userStartingMark + "」より下は、これまでエディタに記述されていたスクリプトです。残す場合は削除しないよう、ご注意ください。\nエディタを再度有効化したいときは、上記の[手順]を行わずに、以下のステップに従って操作してください。\n①グローバルJS入力欄のコメント「" + disableCommand + "」を削除する 。\n②アプレットを保存する。\n③アプレットを再読込して、エディタが復活していることを確認する。"); } else { usiusi_insertDivToGlobalJsDiv(); usiusi_makeEditor(); usiusi_addClass(); usiusi_attach(); usiusi_makeGlobalJsAreaExecuteButton(); console.log("グローバルJavaScript入力欄に、エディタを実装しました。\nエディタを無効化したいときは、以下の手順に従って操作してください。\n①エディタの任意の箇所に、「" + disableCommand + "」とコメントする(カギカッコ不要) 。\n②エディタ上部の確定ボタンを押し、アプレットを保存する。\n③アプレットを再読込して、グローバルJS入力欄が元に戻っていることを確認する。\n④グローバルJS入力欄の1行目から、コメント「" + userStartingMark + "」までを削除し、アプレットを保存する。\nなお、ステップ④において、グローバルJS入力欄のコメント「" + userStartingMark + "」より下は、これまでエディタに記述したスクリプトです。残す場合は削除しないよう、ご注意ください。"); } } }); // オブザーバ const config = { childList: true, subtree: true }; observer.observe(target, config); } // -------------------------------------------------------------------------------------------------------------------------- // ----------------------ggbOnInitを使って、アプレット読み込み時にjQueryをインポート。インポート後(onload)、ユーザーがエディタ上のggbOnInitに入力したスクリプトを格納した関数usiusi_userOnInitを実行する。----------------------------- // usiusi_ggbOnInit_start function ggbOnInit() { var jq = document.createElement("script"); jq.src = '//ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js'; document.head.appendChild(jq); jq.onload = function () { try { usiusi_userOnInit(); } catch (e) { console.error(e.name + ": " + e.message + "\n at: ggbOnInit"); } } } // usiusi_ggbOnInit_end // -------------------------------------------------------------------------------------------------------------------------- // usiusi_end // userScript_below function usiusi_userOnInit() { }