diff --git a/web/fizzy.js b/web/fizzy.js index e822da0f8..21d3d5a0e 100644 --- a/web/fizzy.js +++ b/web/fizzy.js @@ -1060,6 +1060,15 @@ export class Fizzy { window.matchMedia(q).addEventListener("change", () => post({ type: "prefs", prefs: prefs() })); } + // What the keyboard does to the fragment before the caret reaches the worker as typing. + const inputFilter = new TextInput(hidden, { + text: (text) => post({ type: "text", text }), + backspace: () => { + post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); + post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); + }, + }); + worker.onerror = (err) => { console.error(err); onError(`Failed to load the editor (${err.message || "worker error"}).`); @@ -1081,7 +1090,7 @@ export class Fizzy { canvas.style.cursor = m.name; break; case "textInput": - if ((textInputRect.length === 0) !== (m.rect.length === 0)) textInput.clear(); + if ((textInputRect.length === 0) !== (m.rect.length === 0)) inputFilter.clear(); textInputRect = m.rect; oskCheck(); break; @@ -1218,17 +1227,9 @@ export class Fizzy { axis(1, ev.deltaY, []); }, { passive: false }); - // What the keyboard does to the fragment before the caret reaches the worker as typing. - const textInput = new TextInput(hidden, { - text: (text) => post({ type: "text", text }), - backspace: () => { - post({ type: "key", kind: 5, key: "Backspace", repeat: 0, mods: 0 }); - post({ type: "key", kind: 6, key: "Backspace", repeat: 0, mods: 0 }); - }, - }); - canvas.addEventListener("pointerdown", () => textInput.clear()); + canvas.addEventListener("pointerdown", () => inputFilter.clear()); const keydown = (ev) => { - textInput.keydown(ev); + inputFilter.keydown(ev); if (ev.key === "Tab") { if (ev.ctrlKey) return; ev.preventDefault(); diff --git a/web/tests/embed.test.mjs b/web/tests/embed.test.mjs index 6de090af0..b49501e2d 100644 --- a/web/tests/embed.test.mjs +++ b/web/tests/embed.test.mjs @@ -139,3 +139,33 @@ assert.equal(document.fullscreenElement, parentA, "leaving the second embed reve toggles[0](); assert.equal(document.fullscreenElement, null, "leaving the first embed exits full screen"); console.log("embed.test: ok"); + +// Deliver text input as soon as the handler is installed, before setup completes. +let earlyTextInputs = 0; +let filterClears = 0; +const { TextInput } = await import(pathToFileURL(path.join(dir, "web.js")).href); +TextInput.prototype.clear = () => { filterClears++; }; +Canvas.prototype.transferControlToOffscreen = function () { return {}; }; +globalThis.OffscreenCanvas = class {}; +globalThis.ResizeObserver = class { observe() {} }; +globalThis.Worker = class { + set onmessage(handler) { + this.handler = handler; + handler({ data: { type: "textInput", rect: [10, 20, 80, 30] } }); + earlyTextInputs++; + } + postMessage(message) { + if (message.type === "init") this.handler({ data: { type: "ready" } }); + } +}; +const workerCanvas = document.createElement("canvas"); +workerCanvas.parentElement = new Element(); +await new Promise((resolve, reject) => { + new Fizzy({ canvas: workerCanvas, worker: true, persist: false, onReady: resolve, onError: reject }).run(); +}); +assert.equal(earlyTextInputs, 1, "worker text input is handled during setup"); +assert.equal(filterClears, 1, "entering text input clears the initialized filter"); +assert.equal(document.body.children[0].style.left, "10px", "early text input positions the hidden input"); +workerCanvas.dispatch("pointerdown", { pointerId: 1, pointerType: "mouse", buttons: 1, clientX: 0, clientY: 0, timeStamp: 0 }); +assert.equal(filterClears, 2, "pointer input still clears the filter"); +console.log("embed.test: worker startup and early text input ok");