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