UNPKG

asciitorium

Version:

an ASCII ui framework for web + cli

148 lines (147 loc) 5.56 kB
import { Component } from '../core/Component'; import { State } from '../core/State'; function isNumberState(s) { return s && typeof s.value === 'number'; } export class TextInput extends Component { constructor(options) { const height = options.height ?? 3; const border = options.border ?? true; super({ ...options, height, border }); this.cursorIndex = 0; this.suppressCursorSync = false; this.focusable = true; this.hasFocus = false; this.numericMode = options.numeric === true || isNumberState(options.value); this.placeholder = String(options.placeholder ?? ''); // Wire up states if (isNumberState(options.value)) { this.valueNum = options.value; this.valueStr = new State(String(options.value.value ?? '')); // when external number changes, reflect into the visible string this.bind(this.valueNum, (n) => { if (!this.suppressCursorSync) this.cursorIndex = String(n ?? '').length; // Don't thrash on NaN; just render empty this.valueStr.value = Number.isFinite(n) ? String(n) : ''; }); } else { // string mode this.valueStr = options.value ?? new State(''); this.valueNum = undefined; this.bind(this.valueStr, (v) => { if (!this.suppressCursorSync) this.cursorIndex = v.length; }); } } setString(next) { this.suppressCursorSync = true; this.valueStr.value = next; this.suppressCursorSync = false; // If we have a backing number, try to parse and propagate if (this.valueNum) { const parsed = Number(next.trim()); if (next.trim() === '' || Number.isNaN(parsed)) { // empty or invalid -> don't push NaN; leave numeric as-is return; } this.valueNum.value = parsed; } } allowChar(ch, current) { if (!this.numericMode) return true; // numeric guard: digits; one '.'; leading '-' if (/[0-9]/.test(ch)) return true; if (ch === '.' && !current.includes('.')) return true; if (ch === '-' && this.cursorIndex === 0 && !current.startsWith('-')) return true; return false; } handleEvent(event) { let updated = false; const val = this.valueStr.value; if (event.length === 1 && event >= ' ') { if (!this.allowChar(event, val)) return false; const left = val.slice(0, this.cursorIndex); const right = val.slice(this.cursorIndex); this.setString(left + event + right); this.cursorIndex++; updated = true; } else if (event === 'Backspace') { if (this.cursorIndex > 0) { const left = val.slice(0, this.cursorIndex - 1); const right = val.slice(this.cursorIndex); this.setString(left + right); this.cursorIndex--; updated = true; } } else if (event === 'Delete') { if (this.cursorIndex < val.length) { const left = val.slice(0, this.cursorIndex); const right = val.slice(this.cursorIndex + 1); this.setString(left + right); updated = true; } } else if (event === 'ArrowLeft') { this.cursorIndex = Math.max(0, this.cursorIndex - 1); updated = true; } else if (event === 'ArrowRight') { this.cursorIndex = Math.min(val.length, this.cursorIndex + 1); updated = true; } else if (event === 'Home') { this.cursorIndex = 0; updated = true; } else if (event === 'End') { this.cursorIndex = val.length; updated = true; } else if (event === 'Enter') { // Commit: in numeric mode, try to coerce one last time if (this.valueNum) { const parsed = Number(this.valueStr.value.trim()); if (Number.isFinite(parsed)) this.valueNum.value = parsed; } updated = true; } return updated; } draw() { const buffer = super.draw(); const prefix = this.hasFocus ? '> ' : ' '; const prefixLength = prefix.length; const y = this.border ? 1 : 0; const x = this.border ? 1 : 0; const innerWidth = this.width - (this.border ? 2 : 0); const usableWidth = Math.max(0, innerWidth - prefixLength); const raw = this.valueStr.value.length > 0 ? this.valueStr.value : this.placeholder; const visible = raw.slice(0, usableWidth); // prefix for (let i = 0; i < prefixLength && i < innerWidth; i++) { buffer[y][x + i] = prefix[i]; } // text for (let i = 0; i < visible.length && i < usableWidth; i++) { buffer[y][x + prefixLength + i] = visible[i]; } // cursor if (this.hasFocus && usableWidth > 0) { const safeCursor = Math.min(this.cursorIndex, visible.length, usableWidth - 1); buffer[y][x + prefixLength + safeCursor] = '▉'; } this.buffer = buffer; return buffer; } }