79 lines
3.7 KiB
JavaScript
79 lines
3.7 KiB
JavaScript
"use strict";
|
|
(() => {
|
|
const nodes = new Map([[0, document.getElementById("keluna-root")]]);
|
|
let inputRevision = 0;
|
|
const revisions = new Map();
|
|
const resetInput = new Map();
|
|
const composingInputs = new Set();
|
|
let keys = new Set();
|
|
let forwardBlur = false;
|
|
window.addEventListener("keydown", event => {
|
|
const key = event.key.length === 1 ? event.key.toLowerCase() : event.key;
|
|
if (!keys.has(key) || event.ctrlKey || event.altKey || event.metaKey || event.isComposing) return;
|
|
event.preventDefault();
|
|
if (!event.repeat) window.webkit.messageHandlers.keluna.postMessage(`key:${key}`);
|
|
});
|
|
window.addEventListener("blur", () => {
|
|
if (forwardBlur) window.webkit.messageHandlers.keluna.postMessage("blur");
|
|
});
|
|
window.keluna = {
|
|
apply(patches) {
|
|
for (const patch of patches) {
|
|
if (patch.op === "keyboard") { keys = new Set(patch.keys); continue; }
|
|
if (patch.op === "blur") { forwardBlur = true; continue; }
|
|
if (patch.op === "create") {
|
|
const element = document.createElement(patch.tag);
|
|
element.dataset.kelunaId = String(patch.node);
|
|
nodes.set(patch.node, element);
|
|
nodes.get(patch.parent).appendChild(element);
|
|
continue;
|
|
}
|
|
const node = nodes.get(patch.node);
|
|
if (!node) throw new Error(`Unknown Keluna node ${patch.node}`);
|
|
switch (patch.op) {
|
|
case "property": {
|
|
if (patch.name === "value" && composingInputs.has(patch.node)) break;
|
|
if ((patch.name === "value" || patch.name === "checked") && patch.revision < (revisions.get(patch.node) || 0)) break;
|
|
// Assigning even identical values can move the caret in some controls.
|
|
if (node[patch.name] !== patch.value || (patch.name === "value" && node.validity?.badInput)) node[patch.name] = patch.value;
|
|
if (patch.name === "value" || patch.name === "checked") resetInput.get(patch.node)?.();
|
|
break;
|
|
}
|
|
case "listenValue": {
|
|
let composing = false;
|
|
let lastSent;
|
|
const send = event => {
|
|
if (composing || event.isComposing || node.disabled || node.readOnly) return;
|
|
const value = String(node[patch.property]);
|
|
const bad = node.validity?.badInput ? 1 : 0;
|
|
const signature = `${bad}:${value}`;
|
|
if (signature === lastSent) return;
|
|
lastSent = signature;
|
|
const revision = ++inputRevision;
|
|
revisions.set(patch.node, revision);
|
|
if (node.type === "radio") {
|
|
for (const [id, peer] of nodes) {
|
|
if (peer.type === "radio" && peer.name === node.name) revisions.set(id, revision);
|
|
}
|
|
}
|
|
window.webkit.messageHandlers.keluna.postMessage(`value:${patch.node}:${revision}:${bad}:${value}`);
|
|
};
|
|
resetInput.set(patch.node, () => { lastSent = undefined; });
|
|
node.addEventListener(patch.event, send);
|
|
node.addEventListener("compositionstart", () => { composing = true; composingInputs.add(patch.node); });
|
|
node.addEventListener("compositionend", () => { composing = false; composingInputs.delete(patch.node); send({}); });
|
|
break;
|
|
}
|
|
case "text": node.textContent = patch.value; break;
|
|
case "attribute": node.setAttribute(patch.name, patch.value); break;
|
|
case "style": node.style.setProperty(patch.name, patch.value); break;
|
|
case "listen": node.addEventListener("click", () => {
|
|
window.webkit.messageHandlers.keluna.postMessage(`click:${patch.node}`);
|
|
}); break;
|
|
default: throw new Error(`Unknown Keluna operation ${patch.op}`);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
})();
|