const KEY_ROWS = [ ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"], ["K", "L", "M", "N", "O", "P", "Q", "R", "S", "T"], ["U", "V", "W", "X", "Y", "Z", "-", "'", ".", ","], ["SPACE", "BACK", "CLR", "DONE"], ]; const KEYBOARD_TEMPLATE = ` `; const clamp = (value, min, max) => Math.max(min, Math.min(max, value)); export const createKeyboardOverlay = ({ mountRoot }) => { mountRoot.innerHTML = KEYBOARD_TEMPLATE; const root = mountRoot.querySelector("[data-overlay-keyboard]"); const grid = mountRoot.querySelector("[data-overlay-keyboard-grid]"); const keyButtons = []; let openState = false; let selectedRow = 0; let selectedCol = 0; let handlers = null; const applySelection = () => { keyButtons.forEach((button) => button.classList.remove("is-focused")); const selected = keyButtons.find( (button) => Number(button.dataset.row) === selectedRow && Number(button.dataset.col) === selectedCol, ); selected?.classList.add("is-focused"); }; const currentKey = () => { const selected = keyButtons.find( (button) => Number(button.dataset.row) === selectedRow && Number(button.dataset.col) === selectedCol, ); return selected?.dataset.key ?? null; }; const build = () => { if (!grid) { return; } grid.innerHTML = ""; keyButtons.length = 0; KEY_ROWS.forEach((row, rowIndex) => { row.forEach((key, colIndex) => { const button = document.createElement("button"); button.type = "button"; button.className = "overlay-keyboard-key"; button.dataset.row = String(rowIndex); button.dataset.col = String(colIndex); button.dataset.key = key; button.textContent = key === "SPACE" ? "Space" : key; if (key === "DONE") { button.classList.add("is-done"); } if (key === "SPACE") { button.classList.add("is-wide"); } grid.append(button); keyButtons.push(button); }); }); }; build(); const open = (nextHandlers = {}) => { handlers = nextHandlers; openState = true; selectedRow = 0; selectedCol = 0; root.hidden = false; applySelection(); }; const close = () => { openState = false; root.hidden = true; handlers = null; }; const move = (direction) => { const rowWidth = KEY_ROWS[selectedRow]?.length ?? 0; if (direction === "left") { selectedCol = clamp(selectedCol - 1, 0, Math.max(0, rowWidth - 1)); return; } if (direction === "right") { selectedCol = clamp(selectedCol + 1, 0, Math.max(0, rowWidth - 1)); return; } if (direction === "up") { selectedRow = clamp(selectedRow - 1, 0, KEY_ROWS.length - 1); const nextWidth = KEY_ROWS[selectedRow]?.length ?? 0; selectedCol = clamp(selectedCol, 0, Math.max(0, nextWidth - 1)); return; } if (direction === "down") { selectedRow = clamp(selectedRow + 1, 0, KEY_ROWS.length - 1); const nextWidth = KEY_ROWS[selectedRow]?.length ?? 0; selectedCol = clamp(selectedCol, 0, Math.max(0, nextWidth - 1)); } }; const handleAction = (action) => { if (!openState) { return false; } if (action === "up" || action === "down" || action === "left" || action === "right") { move(action); applySelection(); return true; } if (action === "accept") { const key = currentKey(); if (!key) { return true; } if (key === "DONE") { handlers?.onSubmit?.(); return true; } if (key === "BACK") { handlers?.onBackspace?.(); return true; } if (key === "CLR") { handlers?.onClear?.(); return true; } handlers?.onKey?.(key === "SPACE" ? " " : key); return true; } if (action === "back") { handlers?.onBackspace?.(); return true; } if (action === "menu") { handlers?.onSubmit?.(); return true; } if (action === "l1") { handlers?.onPrevField?.(); return true; } if (action === "r1") { handlers?.onNextField?.(); return true; } return false; }; return { open, close, isOpen: () => openState, handleAction, }; };