Refactor text editing helpers into input module
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
// Maintainer-controlled web client version.
|
// Maintainer-controlled web client version.
|
||||||
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
// Format: YYYY.MM.DD Rn (example: 2026.02.20 R2)
|
||||||
window.CHGRID_WEB_VERSION = "2026.02.21 R94";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R95";
|
||||||
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
// Optional display timezone for timestamps. Falls back to America/Detroit if unset/invalid.
|
||||||
window.CHGRID_TIME_ZONE = "America/Detroit";
|
window.CHGRID_TIME_ZONE = "America/Detroit";
|
||||||
|
|||||||
@@ -28,3 +28,157 @@ export function applyTextInput(
|
|||||||
|
|
||||||
return { newString, newCursorPos };
|
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 === 'Home') return 'home';
|
||||||
|
if (code === 'End') return 'end';
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWordCharacter(ch: string): boolean {
|
||||||
|
return /[A-Za-z0-9_]/.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,17 @@ import {
|
|||||||
type EffectId,
|
type EffectId,
|
||||||
} from './audio/effects';
|
} from './audio/effects';
|
||||||
import { RADIO_CHANNEL_OPTIONS, RadioStationRuntime, normalizeRadioChannel, normalizeRadioEffect, normalizeRadioEffectValue } from './audio/radioStationRuntime';
|
import { RADIO_CHANNEL_OPTIONS, RadioStationRuntime, normalizeRadioChannel, normalizeRadioEffect, normalizeRadioEffectValue } from './audio/radioStationRuntime';
|
||||||
import { applyTextInput } from './input/textInput';
|
import {
|
||||||
|
applyPastedText,
|
||||||
|
applyTextInput,
|
||||||
|
describeBackspaceDeletedCharacter,
|
||||||
|
describeCursorCharacter,
|
||||||
|
describeCursorWordOrCharacter,
|
||||||
|
mapTextInputKey,
|
||||||
|
moveCursorWordLeft,
|
||||||
|
moveCursorWordRight,
|
||||||
|
shouldReplaceCurrentText,
|
||||||
|
} from './input/textInput';
|
||||||
import { type IncomingMessage, type OutgoingMessage } from './network/protocol';
|
import { type IncomingMessage, type OutgoingMessage } from './network/protocol';
|
||||||
import { SignalingClient } from './network/signalingClient';
|
import { SignalingClient } from './network/signalingClient';
|
||||||
import { CanvasRenderer } from './render/canvasRenderer';
|
import { CanvasRenderer } from './render/canvasRenderer';
|
||||||
@@ -512,23 +522,6 @@ function openItemPropertyOptionSelect(item: WorldItem, key: string): void {
|
|||||||
audio.sfxUiBlip();
|
audio.sfxUiBlip();
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldReplaceCurrentText(code: string, key: string): boolean {
|
|
||||||
if (!replaceTextOnNextType) return false;
|
|
||||||
if (code === 'ArrowLeft' || code === 'ArrowRight' || code === 'Home' || code === 'End') {
|
|
||||||
replaceTextOnNextType = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (code === 'Backspace' || code === 'Delete') {
|
|
||||||
replaceTextOnNextType = false;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (key.length === 1) {
|
|
||||||
replaceTextOnNextType = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function textInputMaxLengthForMode(mode: typeof state.mode): number | null {
|
function textInputMaxLengthForMode(mode: typeof state.mode): number | null {
|
||||||
if (mode === 'nickname') return NICKNAME_MAX_LENGTH;
|
if (mode === 'nickname') return NICKNAME_MAX_LENGTH;
|
||||||
if (mode === 'chat') return 500;
|
if (mode === 'chat') return 500;
|
||||||
@@ -536,39 +529,16 @@ function textInputMaxLengthForMode(mode: typeof state.mode): number | null {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
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, '');
|
|
||||||
}
|
|
||||||
|
|
||||||
function pasteIntoActiveTextInput(raw: string): boolean {
|
function pasteIntoActiveTextInput(raw: string): boolean {
|
||||||
const maxLength = textInputMaxLengthForMode(state.mode);
|
const maxLength = textInputMaxLengthForMode(state.mode);
|
||||||
if (maxLength === null) {
|
if (maxLength === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const text = normalizePastedText(raw);
|
const result = applyPastedText(raw, state.nicknameInput, state.cursorPos, maxLength, replaceTextOnNextType);
|
||||||
if (!text) {
|
if (!result.handled) return false;
|
||||||
return true;
|
state.nicknameInput = result.newString;
|
||||||
}
|
state.cursorPos = result.newCursorPos;
|
||||||
if (replaceTextOnNextType) {
|
replaceTextOnNextType = result.replaceTextOnNextType;
|
||||||
const replacement = text.slice(0, maxLength);
|
|
||||||
state.nicknameInput = replacement;
|
|
||||||
state.cursorPos = replacement.length;
|
|
||||||
replaceTextOnNextType = false;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const available = Math.max(0, maxLength - state.nicknameInput.length);
|
|
||||||
if (available <= 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const insert = text.slice(0, available);
|
|
||||||
state.nicknameInput =
|
|
||||||
state.nicknameInput.slice(0, state.cursorPos) + insert + state.nicknameInput.slice(state.cursorPos);
|
|
||||||
state.cursorPos += insert.length;
|
|
||||||
replaceTextOnNextType = false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -591,58 +561,6 @@ function isTextEditingMode(mode: typeof state.mode): boolean {
|
|||||||
return mode === 'nickname' || mode === 'chat' || mode === 'itemPropertyEdit';
|
return mode === 'nickname' || mode === 'chat' || mode === 'itemPropertyEdit';
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapTextInputKey(code: string, key: string): string {
|
|
||||||
if (code === 'ArrowLeft') return 'arrowleft';
|
|
||||||
if (code === 'ArrowRight') return 'arrowright';
|
|
||||||
if (code === 'Backspace') return 'backspace';
|
|
||||||
if (code === 'Home') return 'home';
|
|
||||||
if (code === 'End') return 'end';
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWordCharacter(ch: string): boolean {
|
|
||||||
return /[A-Za-z0-9_]/.test(ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
function announceCursorWordOrCharacter(text: string, cursorPos: number): void {
|
|
||||||
if (cursorPos === text.length) {
|
|
||||||
updateStatus('space');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const word = wordAtCursor(text, cursorPos);
|
|
||||||
if (word) {
|
|
||||||
updateStatus(word);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
announceCursorCharacter(text, cursorPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTextInputEdit(code: string, key: string, maxLength: number, ctrlKey = false, allowReplaceOnNextType = false): void {
|
function applyTextInputEdit(code: string, key: string, maxLength: number, ctrlKey = false, allowReplaceOnNextType = false): void {
|
||||||
if (ctrlKey && code === 'KeyA') {
|
if (ctrlKey && code === 'KeyA') {
|
||||||
replaceTextOnNextType = true;
|
replaceTextOnNextType = true;
|
||||||
@@ -652,12 +570,14 @@ function applyTextInputEdit(code: string, key: string, maxLength: number, ctrlKe
|
|||||||
}
|
}
|
||||||
if (ctrlKey && code === 'ArrowLeft') {
|
if (ctrlKey && code === 'ArrowLeft') {
|
||||||
state.cursorPos = moveCursorWordLeft(state.nicknameInput, state.cursorPos);
|
state.cursorPos = moveCursorWordLeft(state.nicknameInput, state.cursorPos);
|
||||||
announceCursorWordOrCharacter(state.nicknameInput, state.cursorPos);
|
const spoken = describeCursorWordOrCharacter(state.nicknameInput, state.cursorPos);
|
||||||
|
if (spoken) updateStatus(spoken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ctrlKey && code === 'ArrowRight') {
|
if (ctrlKey && code === 'ArrowRight') {
|
||||||
state.cursorPos = moveCursorWordRight(state.nicknameInput, state.cursorPos);
|
state.cursorPos = moveCursorWordRight(state.nicknameInput, state.cursorPos);
|
||||||
announceCursorWordOrCharacter(state.nicknameInput, state.cursorPos);
|
const spoken = describeCursorWordOrCharacter(state.nicknameInput, state.cursorPos);
|
||||||
|
if (spoken) updateStatus(spoken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -665,7 +585,9 @@ function applyTextInputEdit(code: string, key: string, maxLength: number, ctrlKe
|
|||||||
const beforeCursor = state.cursorPos;
|
const beforeCursor = state.cursorPos;
|
||||||
const mappedKey = mapTextInputKey(code, key);
|
const mappedKey = mapTextInputKey(code, key);
|
||||||
|
|
||||||
if (allowReplaceOnNextType && shouldReplaceCurrentText(code, key)) {
|
const replaceDecision = shouldReplaceCurrentText(code, key, replaceTextOnNextType);
|
||||||
|
replaceTextOnNextType = replaceDecision.replaceTextOnNextType;
|
||||||
|
if (allowReplaceOnNextType && replaceDecision.shouldReplace) {
|
||||||
state.nicknameInput = key;
|
state.nicknameInput = key;
|
||||||
state.cursorPos = key.length;
|
state.cursorPos = key.length;
|
||||||
return;
|
return;
|
||||||
@@ -675,51 +597,15 @@ function applyTextInputEdit(code: string, key: string, maxLength: number, ctrlKe
|
|||||||
state.nicknameInput = result.newString;
|
state.nicknameInput = result.newString;
|
||||||
state.cursorPos = result.newCursorPos;
|
state.cursorPos = result.newCursorPos;
|
||||||
if (code === 'Backspace') {
|
if (code === 'Backspace') {
|
||||||
announceBackspaceDeletedCharacter(beforeText, beforeCursor);
|
const spoken = describeBackspaceDeletedCharacter(beforeText, beforeCursor);
|
||||||
|
if (spoken) updateStatus(spoken);
|
||||||
}
|
}
|
||||||
if (code === 'ArrowLeft' || code === 'ArrowRight' || code === 'Home' || code === 'End') {
|
if (code === 'ArrowLeft' || code === 'ArrowRight' || code === 'Home' || code === 'End') {
|
||||||
announceCursorCharacter(state.nicknameInput, state.cursorPos);
|
const spoken = describeCursorCharacter(state.nicknameInput, state.cursorPos);
|
||||||
|
if (spoken) updateStatus(spoken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemPropertyValue(item: WorldItem, key: string): string {
|
function getItemPropertyValue(item: WorldItem, key: string): string {
|
||||||
if (key === 'title') return item.title;
|
if (key === 'title') return item.title;
|
||||||
if (key === 'type') return item.type;
|
if (key === 'type') return item.type;
|
||||||
@@ -741,22 +627,6 @@ function getItemPropertyValue(item: WorldItem, key: string): string {
|
|||||||
return String(item.params[key] ?? '');
|
return String(item.params[key] ?? '');
|
||||||
}
|
}
|
||||||
|
|
||||||
function announceCursorCharacter(text: string, cursorPos: number): void {
|
|
||||||
if (cursorPos < 0 || cursorPos > text.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (cursorPos === text.length) {
|
|
||||||
updateStatus('space');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updateStatus(describeCharacter(text[cursorPos]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function announceBackspaceDeletedCharacter(text: string, cursorPos: number): void {
|
|
||||||
if (cursorPos <= 0 || cursorPos > text.length) return;
|
|
||||||
updateStatus(describeCharacter(text[cursorPos - 1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
function squareWord(distance: number): string {
|
function squareWord(distance: number): string {
|
||||||
return distance === 1 ? 'square' : 'squares';
|
return distance === 1 ? 'square' : 'squares';
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user