Files
chat_grid/client/src/input/textInput.ts

197 lines
6.9 KiB
TypeScript
Raw Normal View History

2026-02-20 08:16:43 -05:00
export function applyTextInput(
key: string,
currentString: string,
cursorPos: number,
maxLength: number,
): { newString: string; newCursorPos: number } {
let newString = currentString;
let newCursorPos = cursorPos;
const lowerKey = key.toLowerCase();
if (lowerKey === 'arrowleft') {
newCursorPos = Math.max(0, cursorPos - 1);
} else if (lowerKey === 'arrowright') {
newCursorPos = Math.min(newString.length, cursorPos + 1);
} else if (lowerKey === 'backspace') {
if (cursorPos > 0) {
newString = newString.slice(0, cursorPos - 1) + newString.slice(cursorPos);
newCursorPos = cursorPos - 1;
}
} else if (lowerKey === 'delete') {
if (cursorPos < newString.length) {
newString = newString.slice(0, cursorPos) + newString.slice(cursorPos + 1);
}
2026-02-20 08:16:43 -05:00
} else if (lowerKey === 'home') {
newCursorPos = 0;
} else if (lowerKey === 'end') {
newCursorPos = newString.length;
} else if (key.length === 1 && newString.length < maxLength) {
newString = newString.slice(0, cursorPos) + key + newString.slice(cursorPos);
newCursorPos = cursorPos + 1;
}
return { newString, newCursorPos };
}
export function shouldReplaceCurrentText(
code: string,
key: string,
replaceTextOnNextType: boolean,
): { replaceTextOnNextType: boolean; shouldReplace: boolean } {
if (!replaceTextOnNextType) return { replaceTextOnNextType: false, shouldReplace: false };
if (code === 'ArrowLeft' || code === 'ArrowRight' || code === 'Home' || code === 'End') {
return { replaceTextOnNextType: false, shouldReplace: false };
}
if (code === 'Backspace' || code === 'Delete') {
return { replaceTextOnNextType: false, shouldReplace: false };
}
if (key.length === 1) {
return { replaceTextOnNextType: false, shouldReplace: true };
}
return { replaceTextOnNextType: true, shouldReplace: false };
}
export function normalizePastedText(raw: string): string {
return raw
.replace(/\r\n/g, '\n')
.replace(/\r/g, '\n')
.replace(/\n/g, ' ')
.replace(/[\u0000-\u0008\u000B-\u001F\u007F]/g, '');
}
export function applyPastedText(
raw: string,
currentString: string,
cursorPos: number,
maxLength: number,
replaceTextOnNextType: boolean,
): { handled: boolean; newString: string; newCursorPos: number; replaceTextOnNextType: boolean } {
const text = normalizePastedText(raw);
if (!text) {
return { handled: true, newString: currentString, newCursorPos: cursorPos, replaceTextOnNextType };
}
if (replaceTextOnNextType) {
const replacement = text.slice(0, maxLength);
return {
handled: true,
newString: replacement,
newCursorPos: replacement.length,
replaceTextOnNextType: false,
};
}
const available = Math.max(0, maxLength - currentString.length);
if (available <= 0) {
return { handled: true, newString: currentString, newCursorPos: cursorPos, replaceTextOnNextType: false };
}
const insert = text.slice(0, available);
return {
handled: true,
newString: currentString.slice(0, cursorPos) + insert + currentString.slice(cursorPos),
newCursorPos: cursorPos + insert.length,
replaceTextOnNextType: false,
};
}
export function mapTextInputKey(code: string, key: string): string {
if (code === 'ArrowLeft') return 'arrowleft';
if (code === 'ArrowRight') return 'arrowright';
if (code === 'Backspace') return 'backspace';
if (code === 'Delete') return 'delete';
if (code === 'Home') return 'home';
if (code === 'End') return 'end';
return key;
}
/** Returns whether a character should be treated as part of a word for Ctrl+Arrow navigation. */
function isWordCharacter(ch: string): boolean {
return /[A-Za-z0-9_'\u2019]/.test(ch);
}
export function moveCursorWordLeft(text: string, cursorPos: number): number {
if (cursorPos <= 0) return 0;
let pos = cursorPos - 1;
while (pos > 0 && !isWordCharacter(text[pos])) pos -= 1;
while (pos > 0 && isWordCharacter(text[pos - 1])) pos -= 1;
return pos;
}
export function moveCursorWordRight(text: string, cursorPos: number): number {
let pos = cursorPos;
while (pos < text.length && isWordCharacter(text[pos])) pos += 1;
while (pos < text.length && !isWordCharacter(text[pos])) pos += 1;
return pos;
}
/** Returns the contiguous word under the current cursor index, or null when none. */
function wordAtCursor(text: string, cursorPos: number): string | null {
if (cursorPos < 0 || cursorPos >= text.length || !isWordCharacter(text[cursorPos])) {
return null;
}
let start = cursorPos;
while (start > 0 && isWordCharacter(text[start - 1])) start -= 1;
let end = cursorPos + 1;
while (end < text.length && isWordCharacter(text[end])) end += 1;
return text.slice(start, end);
}
export function describeCharacter(ch: string): string {
if (ch === ' ') return 'space';
if (ch === '\t') return 'tab';
if (ch === '.') return 'period';
if (ch === ',') return 'comma';
if (ch === ':') return 'colon';
if (ch === ';') return 'semicolon';
if (ch === '!') return 'exclamation mark';
if (ch === '?') return 'question mark';
if (ch === "'") return 'apostrophe';
if (ch === '"') return 'quote';
if (ch === '/') return 'slash';
if (ch === '\\') return 'backslash';
if (ch === '-') return 'dash';
if (ch === '_') return 'underscore';
if (ch === '=') return 'equals';
if (ch === '+') return 'plus';
if (ch === '*') return 'asterisk';
if (ch === '&') return 'ampersand';
if (ch === '@') return 'at sign';
if (ch === '#') return 'hash';
if (ch === '%') return 'percent';
if (ch === '$') return 'dollar sign';
if (ch === '^') return 'caret';
if (ch === '|') return 'pipe';
if (ch === '~') return 'tilde';
if (ch === '`') return 'backtick';
if (ch === '(') return 'left parenthesis';
if (ch === ')') return 'right parenthesis';
if (ch === '[') return 'left bracket';
if (ch === ']') return 'right bracket';
if (ch === '{') return 'left brace';
if (ch === '}') return 'right brace';
if (ch === '<') return 'less than';
if (ch === '>') return 'greater than';
return ch;
}
export function describeCursorCharacter(text: string, cursorPos: number): string | null {
if (cursorPos < 0 || cursorPos > text.length) return null;
if (cursorPos === text.length) return 'space';
return describeCharacter(text[cursorPos]);
}
export function describeCursorWordOrCharacter(text: string, cursorPos: number): string | null {
if (cursorPos === text.length) return 'space';
const word = wordAtCursor(text, cursorPos);
if (word) return word;
return describeCursorCharacter(text, cursorPos);
}
export function describeBackspaceDeletedCharacter(text: string, cursorPos: number): string | null {
if (cursorPos <= 0 || cursorPos > text.length) return null;
return describeCharacter(text[cursorPos - 1]);
}
export function describeDeleteDeletedCharacter(text: string, cursorPos: number): string | null {
if (cursorPos < 0 || cursorPos >= text.length) return null;
return describeCharacter(text[cursorPos]);
}