diff --git a/client/public/version.js b/client/public/version.js index e1cffa5..aab15a1 100644 --- a/client/public/version.js +++ b/client/public/version.js @@ -1,3 +1,3 @@ // Maintainer-controlled web client version. // Format: YYYY.MM.DD Rn (example: 2026.02.20 R2) -window.CHGRID_WEB_VERSION = "2026.02.20 R66"; +window.CHGRID_WEB_VERSION = "2026.02.20 R67"; diff --git a/client/src/main.ts b/client/src/main.ts index 1b6ba11..617730a 100644 --- a/client/src/main.ts +++ b/client/src/main.ts @@ -606,6 +606,42 @@ function shouldReplaceCurrentText(code: string, key: string): boolean { return false; } +function textInputMaxLengthForMode(mode: typeof state.mode): number | null { + if (mode === 'nickname') return NICKNAME_MAX_LENGTH; + if (mode === 'chat') return 500; + if (mode === 'itemPropertyEdit') return 500; + 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 { + const maxLength = textInputMaxLengthForMode(state.mode); + if (maxLength === null) { + return false; + } + const text = normalizePastedText(raw); + if (!text) { + 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; +} + function describeCharacter(ch: string): string { if (ch === ' ') return 'space'; if (ch === '\t') return 'tab'; @@ -1891,6 +1927,14 @@ function setupInputHandlers(): void { document.addEventListener('keyup', (event) => { state.keysPressed[event.code] = false; }); + + document.addEventListener('paste', (event) => { + if (document.activeElement !== dom.canvas) return; + if (!state.running) return; + const pasted = event.clipboardData?.getData('text') ?? ''; + if (!pasteIntoActiveTextInput(pasted)) return; + event.preventDefault(); + }); } async function populateAudioDevices(): Promise {