JavaScript for Blink Edition - tablacus/TablacusExplorer GitHub Wiki
Tablacus Explorer Blink Edition using WebView2 operates with two JavaScript engines:
- UI JavaScript (Blink/V8)
- Synchronous JScript (Edge legacy/Chakra)
Because Blink JavaScript (1) cannot return values to external scripts in real-time, the Chakra engine (2) from Edge legacy is also used in parallel.
Script File Assignment
- For files in the
script
folder:consts.js
andcommon.js
are used by both the UI JavaScript and synchronous JScript.ui.js
,index.js
, andoptions.js
are used only by the UI JavaScript.sync.js
,sync1.js
, andsyncb.js
are used only by synchronous JScript.background.js
is used by synchronous JScript for multi-process script execution.threads.js
is used by synchronous JScript for multithreaded script execution.update.js
is used by WSH when updating Tablacus Explorer.
※ Functions that exist only in sync*.js
and not on the UI JavaScript side will be copied over as needed.
- For individual addons:
script.js
andoptions.js
are used by the UI JavaScript.sync.js
is used by both the UI JavaScript and synchronous JScript.
How to Use the Two JavaScript Engines
UI JavaScript (Blink/V8)
This is executed when the script type is JavaScript
.
Properties, methods of Tablacus Explorer objects, and return values from synchronous JScript functions are returned as asynchronous Promise
objects. You should generally use await
to receive them. Also, if you're using await
inside a function, declare it as an async
function.
const FV = await GetFolderView();
wsh.Popup(await FV.FolderItem.Path);
※ GetFolderView
fetches the currently opened folder view using synchronous JScript. When accessing a property of a property or calling a method, it seems sufficient to place a single await
at the beginning:
wsh.Popup(await (await FV.FolderItem).Path);
The window
object of the synchronous JScript engine is assigned to the $
variable.
To load UI JavaScript:
importScript("options.js");
To load synchronous JScript:
$.importScript("sync.js");
Note: When menus are open, the UI JavaScript appears to be paused. In those cases, use synchronous JScript instead.
Synchronous JScript (Edge legacy/Chakra)
Executed when the script type is JScript
. Since await
is not needed, it has high compatibility with the standard Trident version.
Calling UI JavaScript directly from synchronous JScript can cause errors. Use the InvokeUI
function or api.Invoke
instead:
InvokeUI("Addons.FilterBar.Exec", [Ctrl, pt]);
api.Invoke(UI.Addons.FolderListMenu, [oMenu, items, pt]);
Arguments are passed as the second element of an array. Return values from UI JavaScript are always undefined
, so use callback functions if you need to retrieve values.
You cannot access HTML elements from synchronous JScript. The following causes an error:
const el = document.getElementById("...");
Use UI JavaScript to manipulate HTML.
You can use setTimeout
, but since it uses the UI JavaScript version internally, it won’t return a timer ID. Use UI JavaScript if you need to manage timer IDs.
Shared Objects between the Two Engines
The following shared objects are available: g_
, Common
, Sync
, UI
, te
, etc.
g_
contains basic settings.Common
is a shared object for addons.Common.AddressBar
is used for shared address bar state.Sync
contains synchronous JScript functions and is used to call them from UI JavaScript.UI
is primarily used by the core to coordinate between the two engines (e.g., viaInvokeUI
).te
is the Tablacus Explorer object. Its settings are inte.Data
.
{}
and Arrays []
Between Engines
Passing Objects Passing native {}
and []
objects between the two engines often causes errors.
Use the following Tablacus Explorer methods for compatibility:
- Replace
{}
withapi.CreateObject("Object")
- Replace
[]
withapi.CreateObject("Array")
- Alternatively, convert to string via JSON before passing.
If you must access a UI JavaScript object or array from synchronous JScript:
api.ObjGetI(Object, "propertyName");
(You cannot write properties using this method.)
Accessing synchronous JScript objects or arrays from UI JavaScript is theoretically possible, but reading undefined properties may cause errors.
Use the following for safe access:
await api.ObjGetI(Object, "propertyName");
api.ObjPutI(Object, "propertyName", newValue);
Performance Optimization Tips
Using many await
calls can slow down execution.
ui_
:
The following constants are cached in ui_ Key |
Original Value | Meaning |
---|---|---|
ui_.TEPath |
await api.GetModuleFileName(null) |
Path to Tablacus Explorer |
ui_.Installed |
await te.Data.Installed |
Installation folder |
ui_.hwnd |
await te.hwnd |
Window handle |
ui_.DoubleClickTime |
await sha.GetSystemInformation("DoubleClickTime") |
Double-click interval |
ui_.bit |
await api.sizeof("HANDLE") * 8 |
Bit count (32/64) |
Arrays can be converted to native arrays
You can convert arrays to native arrays using:
await api.CreateObject("SafeArray", yourArray);
Example:
let ar = await api.CreateObject("Array");
await ar.push("abc", "def");
if (window.chrome) {
ar = await api.CreateObject("SafeArray", ar);
}
wsh.Popup(ar.join("
"));
Promise.all
You can group multiple promises using const FV = await GetFolderView();
const FolderItem = await FolderItem;
wsh.Popup([await FolderItem.Name, await FolderItem.Path].join("\n"));
Synchronous:
const FV = await GetFolderView();
const FolderItem = await FolderItem;
wsh.Popup(await Promise.all([FolderItem.Name, FolderItem.Path]).join("\n"));
Asynchronous:
const FV = await GetFolderView();
const FolderItem = await FolderItem;
Promise.all([FolderItem.Name, FolderItem.Path]).then(function (r) {
wsh.Popup(r.join("\n"));
});
https://gist.github.com/tablacus/ada487fe34a170b0dcdfe1fd21579ed8
Asynchronous Functions Also Run in Parallel
https://gist.github.com/tablacus/9befe6a8e4088b1a304cc5ed341c5cf0
Other Notes
In UI JavaScript, you cannot get the length
of external objects. Use await GetLength()
instead or convert them to native arrays.
const nLength = await GetLength(ar);
Strings passed between engines will be truncated if they contain a NULL character (``).
Passing null
from UI JavaScript to synchronous JScript becomes undefined
.
Passing undefined
from synchronous JScript to UI JavaScript becomes null
.
For this reason, strict comparisons between undefined
and null
are discouraged:
if (o == null) { // true for both null and undefined
}
Use == null
or != null
.
HTML elements become invalid if passed externally:
const el = document.getElementById("...");
const o = await api.CreateObject("Object");
o.el = el;
Here, o.el
will be a different object. Always keep HTML elements in UI JavaScript variables.
You cannot export the UI JavaScript window
object:
const o = await api.CreateObject("Object");
o.window = window;
This will raise an error. Objects that cannot be serialized to JSON cannot be exported.
HTML5 Drag & Drop does not coexist with native drag & drop in the main UI. Thus, ondragover
and ondrop
do not work there. Use native drag & drop events instead.
In the options screen, HTML5 D&D is available since native D&D is not used there.
※ Native drag & drop requires synchronous, real-time return values, so it must be handled in synchronous JScript. However, since you cannot access HTML elements there, you'll need to store element positions (e.g., via DragEnter
events) in advance — which can be tedious.
Bonus: Standard Trident Edition
The standard Trident version of Tablacus Explorer runs entirely on a single JavaScript engine, where $ = window
.
During script loading, it automatically removes async/await
, so scripts are compatible with the Blink version.
Promise.all
is also supported through a polyfill to ensure compatibility.
This is the result of much trial and error. If you find a better method, please let me know.
Japanese
WebView2を使ったTablacus Explorer Blink版では2つのJavaScriptエンジンで動作しています。
- UI用JavaScript (Blink/V8)
- 同期用JScript (Edge legacy/Chakra)
1.のBlinkのJavaScriptには外部にリアルタイムに戻り値を返せないという問題があるので2.のEdge用のChakraエンジンを併用しています。
スクリプトファイルの割り振り
- scriptフォルダに入っているファイルでは 「consts.js」「common.js」はUI用JavaScript、同期用JScriptの両方で使用します。 「ui.js」「index.js」「options.js」はUI用JavaScriptのみで使用します。 「sync.js」「sync1.js」「syncb.js」は同期用JScriptのみで使用します。 「background.js」はマルチプロセスのスクリプト実行時に同期用JScriptで使用します。 「threads.js」はマルチスレッドのスクリプト実行時に同期用JScriptで使用します。 「update.js」はTablacus Explorer更新時にWSHで使用します。
※ sync*.js にありUI用JavaScript側にない関数はコピーされます。
- 個々のアドオンでは 「script.js」「options.js」はUI用JavaScriptで使用しています。 「sync.js」はUI用JavaScriptで同期用JScriptで使用しています。
2つのJavaScriptエンジンの使い方
UI用JavaScript (Blink/V8)
タイプがJavaScript
の場合に実行されます。
Tablacus Explorerのオブジェクトのプロパティやメソッド、同期用JScriptの関数の戻り値などは非同期のPromiseオブジェクトで返されます。
基本的にawait
で受け取ってください。また、関数内でawait
を使う場合はasync function () {'や'async () => {
みたいに非同期関数にしてください。
const FV = await GetFolderView();
wsh.Popup(await FV.FolderItem.Path);
※GetFolderView
は同期用JScriptで現在開いているフォルダビューを取得します。
下のFV.FolderItem.Path
の様にオブジェクトのプロパティのプロパティを受け取る場合やオブジェクトのプロパティのメソッドの戻り値を取得する場合は前に一つawait
があれば良いようです。
wsh.Popup(await (await FV.FolderItem).Path);
みたいに書くこともできます。
同期用JScriptのwindowオブジェクトは変数の$
に入っています。
UI用JavaScriptを読み込む場合
importScript("options.js");
同期用JScriptを読み込む場合
$.importScript("sync.js");
メニューなどが開いている場合はUI用JavaScriptは停止しているようです。メニュー中のスクリプトは同期用JScriptを使用してください。
同期用JScript (Edge legacy/Chakra)
タイプがJScript
の場合に実行されます。await
などが不要なので標準のTrident版と互換性が高いです。
同期用JScriptからUI用JavaScriptは直接呼び出すとなぜかエラーが出るのでInvokeUI
関数かapi.Invoke(
を使います。
InvokeUI("Addons.FilterBar.Exec", [Ctrl, pt]);
api.Invoke(UI.Addons.FolderListMenu, [oMenu, items, pt]);
引数は2番目の配列として渡します。
UI用JavaScriptからの戻り値は必ずundefined
になります。値を返したい場合はコールバック関数を使います。
同期用JScriptからHTMLの部品にはアクセスできません。エラーが出ます。
const el = document.getElementById("○○");
上記の様にHTMLを操作する場合はUI用JavaScriptを使用します。
setTimeout関数は使えますが、UI用JavaScript用の物を利用している関係で戻り値のタイマーIDが取得できません。 タイマーIDを使うものはUI用JavaScriptを使用してください。
2つのJavaScriptエンジンで共有のオブジェクト
「g_」「Common」「Sync」「UI」「te」などの共有オブジェクトがあります。 g_ は基本的な設定など Common はアドオン共有のオブジェクトで Common.AddressBar はアドレスバーで共有しています。 Sync は同期用JScriptの関数などを入れ、UI用JavaScriptから呼び出したりするのに使用しています。 UI は主に本体で2つのJavaScriptの連携に使用しています。(InvokeUIなど) te は Tablacus Explorerのオブジェクトです。te.Dataに設定があります。
オブジェクト{}、配列[]の受け渡しについて
2つのJavaScriptエンジンでオブジェクト{}
、配列[]
を受け渡すと相性が悪くエラーが出やすいです。
以下のTablacus Explorerのオブジェクト、配列を使用すると問題なく動作します。
{}
の代わりに api.CreateObject("Object")
[]
の代わりに api.CreateObject("Array")
もしくはJSONなど文字列に変換して受け渡しを行ってください。
どうしてもUI用JavaScriptのオブジェクト{}
、配列[]
を同期用JScriptで読み込みたい場合は
api.ObjGetI(Object, "プロパティ名");
を使うとうまくいくかもしれません。書き込みはできません。
逆に同期用JScriptのオブジェクト{}
、配列[]
は一応読み込みできますが、未定義のプロパティを読み込むとエラーが出ます。
安全に読み書きを行う場合は
await api.ObjGetI(Object, "プロパティ名");
api.ObjPutI(Object, "プロパティ名", 新しい値);
を使用してください。
高速化テクニック
awaitが続くとどうしても遅くなるようです。
以下の固定値はui_にも置いています。
ui_ | 元 | 意味 |
---|---|---|
ui_.TEPath | await api.GetModuleFileName(null) | Tablacus Explorerのパス |
ui_.Installed | await te.Data.Installed | Tablacus Explorerのフォルダ; |
ui_.hwnd | await te.hwnd | Tablacus Explorerのウインドウハンドル |
ui_.DoubleClickTime | await sha.GetSystemInformation("DoubleClickTime") | ダブルクリックの時間; |
ui_.bit | await api.sizeof("HANDLE") * 8 | ビット数(32/64) |
配列はネイティブの配列に変換できます。
配列はawait api.CreateObject("SafeArray", 変数名);
でUI用JavaScriptのネイティブの配列に変換できます。
let ar = await api.CreateObject("Array");
await ar.push("abc", "def");
if (window.chrome) {
ar = await api.CreateObject("SafeArray", ar);
}
wsh.Popup(ar.join("\n"));
Promise.all で複数の非同期オブジェクトをまとめることもできます。
const FV = await GetFolderView();
const FolderItem = await FolderItem;
wsh.Popup([await FolderItem.Name, await FolderItem.Path].join("\n"));
同期
const FV = await GetFolderView();
const FolderItem = await FolderItem;
wsh.Popup(await Promise.all([FolderItem.Name, FolderItem.Path]).join("\n"));
非同期
const FV = await GetFolderView();
const FolderItem = await FolderItem;
Promise.all([FolderItem.Name, FolderItem.Path]).then(function (r) {
wsh.Popup(r.join("\n"));
});
https://gist.github.com/tablacus/ada487fe34a170b0dcdfe1fd21579ed8
非同期関数を使用しても並列動作になります。
https://gist.github.com/tablacus/9befe6a8e4088b1a304cc5ed341c5cf0
その他注意点
UI用JavaScriptでは外部オブジェクトのlength
は取得できません。代わりにawait GetLength(
使用するか配列などの場合は上記のネイティブ配列化を使います。
const nLength = await GetLength(ar);
受け渡しの文字列にNULL文字\0
を含むとそこで切られます。
UI用JavaScriptから同期用JScriptにnull
を渡すとundefined
になります。
逆に同期用JScriptからUI用JavaScriptにundefined
を渡すとnull
になります。
以上の結果からundefined
とnull
を厳密に判別するのはお勧めしません。
if (o == null) {// null もしくは undefied の場合 true
}
== null
や!= null
を使用しています。
HTMLのエレメントは外部に持ち出すと使えなくなります。
const el = document.getElementById("○○");
const o = await api.CreateObject("Object");
o.el = el;
o.el
は別物になっています。HTMLのエレメントは必ず、UI用JavaScriptの変数に入れてください。
UI用JavaScriptのwindowオブジェクトは外部に持ち出せません
const o = await api.CreateObject("Object");
o.window = window;
上記コードはエラーが出ます。JSON化できないオブジェクトは持ち出し不可のようです。
HTML5 ドラッグ&ドロップは通常画面ではネイティブドラッグ&ドロップと共存できなかったので、ondragoverイベントとondropイベントは動作しません。ネイティブドラッグ&ドロップの方のイベントを使ってください。オプション画面の方はネイティブドラッグ&ドロップを使用していないので使えます。
※ネイティブドラッグ&ドロップのイベントはリアルタイムで戻り値を返さなければならない関係で、同期JScriptの方を使う事になるのですが、同期JScriptの方からはHTMLの部品にアクセスできないので、事前のDragEnterイベント辺りで項目の場所(RECT)を保存するなど工夫が必要で面倒だったりします。
おまけ:標準のTrident版
標準のTrident版Tablacus Explorerの場合は一つのJavaScriptエンジンで動作し、$ = window;
になっています。
また、スクリプトの読み込み時に自動的にasync/await
などは削除していますので、Blink版と同じスクリプトで動作しています。
Promise.all
も代替処理を入れて同じコードで動作させています。
以上、試行錯誤したものです。もっと良い方法があれば教えてください。