Add ctrl+word navigation and ctrl+a select-all in text editors
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 R90";
|
window.CHGRID_WEB_VERSION = "2026.02.21 R91";
|
||||||
// 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";
|
||||||
|
|||||||
@@ -552,6 +552,13 @@ function pasteIntoActiveTextInput(raw: string): boolean {
|
|||||||
if (!text) {
|
if (!text) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (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);
|
const available = Math.max(0, maxLength - state.nicknameInput.length);
|
||||||
if (available <= 0) {
|
if (available <= 0) {
|
||||||
return true;
|
return true;
|
||||||
@@ -573,7 +580,38 @@ function mapTextInputKey(code: string, key: string): string {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyTextInputEdit(code: string, key: string, maxLength: number, allowReplaceOnNextType = false): void {
|
function moveCursorWordLeft(text: string, cursorPos: number): number {
|
||||||
|
let pos = cursorPos;
|
||||||
|
while (pos > 0 && /\s/.test(text[pos - 1])) pos -= 1;
|
||||||
|
while (pos > 0 && !/\s/.test(text[pos - 1])) pos -= 1;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
function moveCursorWordRight(text: string, cursorPos: number): number {
|
||||||
|
let pos = cursorPos;
|
||||||
|
while (pos < text.length && /\s/.test(text[pos])) pos += 1;
|
||||||
|
while (pos < text.length && !/\s/.test(text[pos])) pos += 1;
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTextInputEdit(code: string, key: string, maxLength: number, ctrlKey = false, allowReplaceOnNextType = false): void {
|
||||||
|
if (ctrlKey && code === 'KeyA') {
|
||||||
|
replaceTextOnNextType = true;
|
||||||
|
state.cursorPos = state.nicknameInput.length;
|
||||||
|
updateStatus(`${state.nicknameInput} selected`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctrlKey && code === 'ArrowLeft') {
|
||||||
|
state.cursorPos = moveCursorWordLeft(state.nicknameInput, state.cursorPos);
|
||||||
|
announceCursorCharacter(state.nicknameInput, state.cursorPos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (ctrlKey && code === 'ArrowRight') {
|
||||||
|
state.cursorPos = moveCursorWordRight(state.nicknameInput, state.cursorPos);
|
||||||
|
announceCursorCharacter(state.nicknameInput, state.cursorPos);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const beforeText = state.nicknameInput;
|
const beforeText = state.nicknameInput;
|
||||||
const beforeCursor = state.cursorPos;
|
const beforeCursor = state.cursorPos;
|
||||||
const mappedKey = mapTextInputKey(code, key);
|
const mappedKey = mapTextInputKey(code, key);
|
||||||
@@ -1405,7 +1443,7 @@ function handleNormalModeInput(code: string, shiftKey: boolean): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleChatModeInput(code: string, key: string): void {
|
function handleChatModeInput(code: string, key: string, ctrlKey: boolean): void {
|
||||||
if (code === 'Enter') {
|
if (code === 'Enter') {
|
||||||
const message = state.nicknameInput.trim();
|
const message = state.nicknameInput.trim();
|
||||||
if (message.length > 0) {
|
if (message.length > 0) {
|
||||||
@@ -1431,7 +1469,7 @@ function handleChatModeInput(code: string, key: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTextInputEdit(code, key, 500);
|
applyTextInputEdit(code, key, 500, ctrlKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleEffectSelectModeInput(code: string, key: string): void {
|
function handleEffectSelectModeInput(code: string, key: string): void {
|
||||||
@@ -1785,7 +1823,7 @@ function handleItemPropertiesModeInput(code: string, key: string): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemPropertyEditModeInput(code: string, key: string): void {
|
function handleItemPropertyEditModeInput(code: string, key: string, ctrlKey: boolean): void {
|
||||||
const itemId = state.selectedItemId;
|
const itemId = state.selectedItemId;
|
||||||
const propertyKey = state.editingPropertyKey;
|
const propertyKey = state.editingPropertyKey;
|
||||||
if (!itemId || !propertyKey) {
|
if (!itemId || !propertyKey) {
|
||||||
@@ -1879,7 +1917,7 @@ function handleItemPropertyEditModeInput(code: string, key: string): void {
|
|||||||
audio.sfxUiCancel();
|
audio.sfxUiCancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applyTextInputEdit(code, key, 500, true);
|
applyTextInputEdit(code, key, 500, ctrlKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleItemPropertyOptionSelectModeInput(code: string, key: string): void {
|
function handleItemPropertyOptionSelectModeInput(code: string, key: string): void {
|
||||||
@@ -1935,7 +1973,7 @@ function handleItemPropertyOptionSelectModeInput(code: string, key: string): voi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleNicknameModeInput(code: string, key: string): void {
|
function handleNicknameModeInput(code: string, key: string, ctrlKey: boolean): void {
|
||||||
if (code === 'Enter') {
|
if (code === 'Enter') {
|
||||||
const clean = sanitizeName(state.nicknameInput);
|
const clean = sanitizeName(state.nicknameInput);
|
||||||
if (clean) {
|
if (clean) {
|
||||||
@@ -1959,7 +1997,7 @@ function handleNicknameModeInput(code: string, key: string): void {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyTextInputEdit(code, key, NICKNAME_MAX_LENGTH, true);
|
applyTextInputEdit(code, key, NICKNAME_MAX_LENGTH, ctrlKey, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isTypingKey(code: string): boolean {
|
function isTypingKey(code: string): boolean {
|
||||||
@@ -2006,9 +2044,9 @@ function setupInputHandlers(): void {
|
|||||||
if (isTypingKey(code) && state.keysPressed[code]) return;
|
if (isTypingKey(code) && state.keysPressed[code]) return;
|
||||||
|
|
||||||
if (state.mode === 'nickname') {
|
if (state.mode === 'nickname') {
|
||||||
handleNicknameModeInput(code, event.key);
|
handleNicknameModeInput(code, event.key, event.ctrlKey);
|
||||||
} else if (state.mode === 'chat') {
|
} else if (state.mode === 'chat') {
|
||||||
handleChatModeInput(code, event.key);
|
handleChatModeInput(code, event.key, event.ctrlKey);
|
||||||
} else if (state.mode === 'effectSelect') {
|
} else if (state.mode === 'effectSelect') {
|
||||||
handleEffectSelectModeInput(code, event.key);
|
handleEffectSelectModeInput(code, event.key);
|
||||||
} else if (state.mode === 'listUsers') {
|
} else if (state.mode === 'listUsers') {
|
||||||
@@ -2022,7 +2060,7 @@ function setupInputHandlers(): void {
|
|||||||
} else if (state.mode === 'itemProperties') {
|
} else if (state.mode === 'itemProperties') {
|
||||||
handleItemPropertiesModeInput(code, event.key);
|
handleItemPropertiesModeInput(code, event.key);
|
||||||
} else if (state.mode === 'itemPropertyEdit') {
|
} else if (state.mode === 'itemPropertyEdit') {
|
||||||
handleItemPropertyEditModeInput(code, event.key);
|
handleItemPropertyEditModeInput(code, event.key, event.ctrlKey);
|
||||||
} else if (state.mode === 'itemPropertyOptionSelect') {
|
} else if (state.mode === 'itemPropertyOptionSelect') {
|
||||||
handleItemPropertyOptionSelectModeInput(code, event.key);
|
handleItemPropertyOptionSelectModeInput(code, event.key);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user