Files
mhoennig ba0a1cf419
werkator build succeeded after 02:49
form example
2026-09-16 12:08:41 +02:00

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}`);
}
}
}
};
})();