Something went wrong. Try again.
Monorepo for the nibling.run platform
Something went wrong. Try again.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798import type { Language } from "./keys.ts";
export type CaretMove = "left" | "right" | "up" | "down";
/** * A view that edits text at a caret. The compact key bar drives the focused one, so a phone can * reach keys its on-screen keyboard buries. The CodeMirror editor will implement this too. */export interface EditingTarget { /** What's being edited, so the key bar can offer that language's symbols. */ readonly language: Language; /** Inserts `text` at the caret, leaving the caret `caret` characters into it (default: after it). */ insert(text: string, caret?: number): void; move(direction: CaretMove): void; indent(): void; undo(): void; redo(): void; /** Gives up focus, which closes an on-screen keyboard. */ blur(): void;}
export function isEditingTarget(value: unknown): value is EditingTarget & HTMLElement { return value instanceof HTMLElement && typeof (value as Partial<EditingTarget>).insert === "function";}
type Field = HTMLTextAreaElement | HTMLInputElement;
/** * Turns off every kind of text assistance on a field that holds code or commands. Each platform * reads a different attribute: browsers read autocomplete; iOS keyboards read autocorrect and * autocapitalize; Chrome on Android asks the keyboard for no suggestions when spellcheck is off; * writingsuggestions stops the browser's inline completions. */export function codeField(field: Field): void { field.spellcheck = false; field.autocomplete = "off"; field.autocapitalize = "off"; field.setAttribute("autocorrect", "off"); field.setAttribute("writingsuggestions", "false");}
/** * Inserts through the browser's own editing command, so the insertion joins the field's undo * history and fires `input` like typing does. Falls back to setRangeText where the command is gone. */export function insertAtCaret(field: Field, text: string, caret = text.length): void { field.focus(); const start = field.selectionStart ?? field.value.length; if (!document.execCommand("insertText", false, text)) { field.setRangeText(text, start, field.selectionEnd ?? start, "end"); field.dispatchEvent(new InputEvent("input", { bubbles: true, inputType: "insertText", data: text })); } field.setSelectionRange(start + caret, start + caret);}
/** * The column a run of up/down moves aims for, as editors keep it: passing through a short or empty * line doesn't lose it. It lapses as soon as the caret is somewhere the last vertical move didn't put it. */const goals = new WeakMap<Field, { column: number; at: number }>();
/** Moves a field's caret one character, or one line keeping its goal column. */export function moveCaret(field: Field, direction: CaretMove): void { field.focus(); const { value } = field; const at = direction === "left" || direction === "up" ? (field.selectionStart ?? 0) : (field.selectionEnd ?? 0); let next = at; if (direction === "left") next = Math.max(0, at - 1); else if (direction === "right") next = Math.min(value.length, at + 1); else { const lineStart = value.lastIndexOf("\n", at - 1) + 1; const goal = goals.get(field); const column = goal?.at === at ? goal.column : at - lineStart; if (direction === "up") { if (lineStart === 0) next = 0; else { const previousStart = value.lastIndexOf("\n", lineStart - 2) + 1; next = Math.min(previousStart + column, lineStart - 1); } } else { const lineEnd = value.indexOf("\n", at); if (lineEnd === -1) next = value.length; else { const nextEnd = value.indexOf("\n", lineEnd + 1); next = Math.min(lineEnd + 1 + column, nextEnd === -1 ? value.length : nextEnd); } } goals.set(field, { column, at: next }); } field.setSelectionRange(next, next);}
/** Runs the browser's undo or redo on a focused field. */export function history(field: Field, command: "undo" | "redo"): void { field.focus(); document.execCommand(command);}