JavaScript for Blink Edition - tablacus/TablacusExplorer GitHub Wiki

Tablacus Explorer Blink Edition using WebView2 operates with two JavaScript engines:

  1. UI JavaScript (Blink/V8)
  2. 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 and common.js are used by both the UI JavaScript and synchronous JScript.
    • ui.js, index.js, and options.js are used only by the UI JavaScript.
    • sync.js, sync1.js, and syncb.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 and options.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., via InvokeUI).
  • te is the Tablacus Explorer object. Its settings are in te.Data.

Passing Objects {} and Arrays [] Between Engines

Passing native {} and [] objects between the two engines often causes errors.
Use the following Tablacus Explorer methods for compatibility:

  • Replace {} with api.CreateObject("Object")
  • Replace [] with api.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.

The following constants are cached in ui_:

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("
"));

You can group multiple promises using Promise.all

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エンジンで動作しています。

  1. UI用JavaScript (Blink/V8)
  2. 同期用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になります。

以上の結果からundefinednullを厳密に判別するのはお勧めしません。

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も代替処理を入れて同じコードで動作させています。

以上、試行錯誤したものです。もっと良い方法があれば教えてください。