Something went wrong. Try again.
[READ-ONLY] Mirror of https://github.com/bombshell-dev/playground.
Something went wrong. Try again.
5.5 kB · 183 lines
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184import { createApi } from 'effection/experimental';import { DispatchApi, focusable, current, type Node, type Root } from '@bomb.sh/freedom';import type { KeyDown, KeyEvent, KeyRepeat, KeyUp } from '@bomb.sh/tty';
export const KeyboardApi = createApi('@bomb.sh/keyboard', { keydown(node: Node, event: KeyDown): void { if (node.get('input')) { if (event.text && event.text?.length > 0) { insert(node, event.text); } else if (event.code === 'Backspace') { deleteBackward(node); } else if (event.code === 'Delete') { deleteForward(node); } else if (event.code === 'ArrowLeft') { moveBack(node); } else if (event.code === 'ArrowRight') { moveForward(node); } else if (event.code === 'Home') { moveMin(node); } else if (event.code === 'End') { moveMax(node); } } }, keyup(_node: Node, _event: KeyUp): void {}, keyrepeat(node: Node, event: KeyRepeat): void { KeyboardApi.invoke(node.scope, 'keydown', [ node, { ...event, type: 'keydown', }, ]); },});
export const Input = createApi('@bomb.sh/input', { insert(node: Node, text: string): void { const codepoints = [...(node.get('value') as string)]; const caret = node.get('caret') as number; codepoints.splice(caret, 0, text); node.set('value', codepoints.join('')); node.set('caret', caret + 1); }, deleteBackward(node: Node): void { const codepoints = [...(node.get('value') as string)]; let caret = node.get('caret') as number; const length = codepoints.length; if (caret > length) { console.warn('caret mismatch: ${caret}, value length: ${length}'); caret = length; } if (caret > 0) { codepoints.splice(caret - 1, 1); node.set('value', codepoints.join('')); node.set('caret', caret - 1); } }, deleteForward(node: Node): void { const codepoints = [...(node.get('value') as string)]; let caret = node.get('caret') as number; const length = codepoints.length; if (caret > length) { console.warn('caret mismatch: ${caret}, value length: ${length}'); caret = length; } if (caret < length) { codepoints.splice(caret, 1); node.set('value', codepoints.join('')); } }, moveBack(node: Node): void { const caret = node.get('caret') as number; node.set('caret', Math.max(0, caret - 1)); }, moveForward(node: Node): void { const caret = node.get('caret') as number; const codepoints = [...(node.get('value') as string)]; node.set('caret', Math.min(codepoints.length, caret + 1)); }, moveMin(node: Node): void { node.set('caret', 0); }, moveMax(node: Node): void { node.set('caret', [...(node.get('value') as string)].length); },});
// Install a text input onto a node. This stub seeds only the DOM-shaped state// (`value`, `selectionStart`, `selectionEnd`, `selectionDirection`) and makes// the node focusable. Editing behavior is enumerated as skipped specs under// `test/` and is not yet implemented — the interaction API is intentionally// left unprescribed until the state model is settled.export function initInput(node: Node): void { focusable(node); node.set('input', true); node.set('value', ''); node.set('caret', 0);}
export function useInput(root: Root): void { root.node.scope.around(DispatchApi, { *dispatch([event], next) { if (isKeyEvent(event)) { const target = current(root.node); KeyboardApi.invoke(target.scope, event.type, [ target, event as KeyDown & KeyUp & KeyRepeat, ]); return { ok: true, value: true }; } return yield* next(event); }, });
root.node.scope.around(KeyboardApi, { keydown([node, event], next): void { if (node.get('input')) { if (event.text && event.text?.length > 0) { insert(node, event.text); } else if (event.code === 'Backspace') { deleteBackward(node); } else if (event.code === 'Delete') { deleteForward(node); } else if (event.code === 'ArrowLeft') { moveBack(node); } else if (event.code === 'ArrowRight') { moveForward(node); } else if (event.code === 'Home') { moveMin(node); } else if (event.code === 'End') { moveMax(node); } else { next(node, event); } } else { next(node, event); } }, });}
function isKeyEvent(event: unknown): event is KeyEvent { const x = event as KeyEvent; // console.log({ x, key: typeof x.key === "string", code: typeof x.code === "string", type: ["keyup", "keydown", "keyrepeat"].includes(x.type) }); return ( !!x && typeof x.key === 'string' && typeof x.code === 'string' && ['keyup', 'keydown', 'keyrepeat'].includes(x.type) );}
// Exported operation wrappers over the `Input` API. Each invokes the// corresponding method at the node's scope so callers — the key dispatch above// and any custom keymap — don't reach for `Input.invoke` directly.export function insert(node: Node, text: string): void { Input.invoke(node.scope, 'insert', [node, text]);}
export function deleteBackward(node: Node): void { Input.invoke(node.scope, 'deleteBackward', [node]);}
export function deleteForward(node: Node): void { Input.invoke(node.scope, 'deleteForward', [node]);}
export function moveBack(node: Node): void { Input.invoke(node.scope, 'moveBack', [node]);}
export function moveForward(node: Node): void { Input.invoke(node.scope, 'moveForward', [node]);}
export function moveMin(node: Node): void { Input.invoke(node.scope, 'moveMin', [node]);}
export function moveMax(node: Node): void { Input.invoke(node.scope, 'moveMax', [node]);}