|
82 | 82 | align-items: center; |
83 | 83 | } |
84 | 84 |
|
85 | | - /* 輸入結果顯示區 */ |
| 85 | + /* 輸入結果顯示區 (現在是可編輯區域) */ |
86 | 86 | #text-output { |
87 | 87 | width: 100%; |
88 | 88 | background-color: var(--container-bg); |
|
97 | 97 | white-space: pre-wrap; |
98 | 98 | word-break: break-word; |
99 | 99 | color: var(--text-color); |
100 | | - cursor: text; |
| 100 | + /* 設置 cursor 為 text,並確保它能顯示游標 */ |
| 101 | + cursor: text; |
101 | 102 | } |
102 | 103 |
|
103 | 104 | /* 候選字與輸入區 */ |
|
116 | 117 | padding: 8px; |
117 | 118 | font-size: 1.2rem; |
118 | 119 | min-height: 40px; |
119 | | - line-height: 1.5; |
| 120 | + line-height: 1.5; |
120 | 121 | border: none; |
121 | 122 | border-bottom: 1px solid var(--border-color); |
122 | 123 | background-color: #fafafa; |
|
240 | 241 | </div> |
241 | 242 |
|
242 | 243 | <div id="app-container"> |
243 | | - <div id="text-output"></div> |
| 244 | + <div id="text-output" contenteditable="true" spellcheck="false"></div> |
244 | 245 |
|
245 | 246 | <div id="input-area"> |
246 | 247 | <div id="main-input">編碼顯示在此</div> |
|
262 | 263 |
|
263 | 264 | // 字母鍵的基礎映射 |
264 | 265 | const letter_maps = { |
265 | | - 'cn_normal': (key) => key.toUpperCase(), // 中文模式顯示大寫字母 (A-Z) |
266 | | - // 中文模式下,字母鍵的顯示不隨 Shift 改變,維持大寫 |
| 266 | + 'cn_normal': (key) => key.toUpperCase(), |
267 | 267 | 'cn_shifted': (key) => key.toUpperCase(), |
268 | | - 'en_normal': (key) => key.toLowerCase(), // 英文模式顯示小寫字母 (a-z) |
269 | | - 'en_shifted': (key) => key.toUpperCase() // 英文 Shift 模式顯示大寫字母 (A-Z) |
| 268 | + 'en_normal': (key) => key.toLowerCase(), |
| 269 | + 'en_shifted': (key) => key.toUpperCase() |
270 | 270 | }; |
271 | 271 |
|
272 | 272 | const key_display_maps = { |
273 | 273 | // [0, 0] Chinese Normal (Default) |
274 | 274 | 'cn_normal': { |
275 | | - // Numbers/Symbols: 中文輸入法的預設符號 (數字輸出,符號編碼) |
276 | 275 | '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '0': '0', |
277 | 276 | ';': ';', "'": '〃', ',': ',', '.': '.', '/': '/', |
278 | | - 'lang_toggle': '三碼', // 顯示 '三碼' (當前模式) |
| 277 | + 'lang_toggle': '三碼', |
279 | 278 | 'case_shift': '⇧', |
280 | | - 'backspace': '⌫', // 顯示 '⌫' |
281 | | - 'enter': '↵', // 顯示 '↵' |
282 | | - ' ': ' ' // 修正: 顯示 ' ' (單一空白字元) |
| 279 | + 'backspace': '⌫', |
| 280 | + 'enter': '↵', |
| 281 | + ' ': ' ' // 空白鍵顯示 ' ' |
283 | 282 | }, |
284 | 283 | // [0, 1] Chinese Shifted (與 cn_normal 相同,除了 case_shift 鍵本身) |
285 | 284 | 'cn_shifted': { |
286 | 285 | '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '0': '0', |
287 | 286 | ';': ';', "'": '〃', ',': ',', '.': '.', '/': '/', |
288 | 287 | 'lang_toggle': '三碼', |
289 | | - 'case_shift': '⬆', // 只有 Shift 鍵的視覺改變 |
| 288 | + 'case_shift': '⬆', |
290 | 289 | 'backspace': '⌫', |
291 | 290 | 'enter': '↵', |
292 | 291 | ' ': ' ' |
293 | 292 | }, |
294 | 293 | // [1, 0] English Normal (Lowercase) |
295 | 294 | 'en_normal': { |
296 | 295 | '1': '1', '2': '2', '3': '3', '4': '4', '5': '5', '6': '6', '7': '7', '8': '8', '9': '9', '0': '0', |
297 | | - // 英文模式預設符號 |
298 | 296 | ';': ';', "'": "'", ',': ',', '.': '.', '/': '/', |
299 | | - 'lang_toggle': 'En', // 顯示 'En' (當前模式) |
| 297 | + 'lang_toggle': 'En', |
300 | 298 | 'case_shift': '⇧', |
301 | 299 | 'backspace': '⌫', |
302 | 300 | 'enter': '↵', |
303 | 301 | ' ': ' ' |
304 | 302 | }, |
305 | 303 | // [1, 1] English Shifted (Uppercase + Secondary Symbols) |
306 | 304 | 'en_shifted': { |
307 | | - // Shifted Symbols (直接輸出) |
308 | 305 | '1': '!', '2': '@', '3': '#', '4': '$', '5': '%', '6': '^', '7': '&', '8': '*', '9': '(', '0': ')', |
309 | | - // 英文上檔符號 |
310 | 306 | ';': ':', "'": '"', ',': '<', '.': '>', '/': '?', |
311 | 307 | 'lang_toggle': 'En', |
312 | 308 | 'case_shift': '⬆', |
|
330 | 326 | (isShiftActive ? 'cn_shifted' : 'cn_normal'); |
331 | 327 | const map = key_display_maps[mapKey]; |
332 | 328 |
|
333 | | - // 1. 檢查是否為英文模式下的直接輸出 (字母, 數字, 符號) |
| 329 | + // 1. 檢查是否為英文模式下的直接輸出 |
334 | 330 | if (isEnglishMode) { |
335 | 331 | if (code.match(/^[a-z]$/)) { |
336 | | - // 英文模式下,字母直接輸出大小寫 |
337 | 332 | return letter_maps[mapKey](code); |
338 | 333 | } |
339 | | - if (code.match(/^[0-9;'./,<> ]$/)) { // 包含 space |
340 | | - // 英文模式下,數字和符號直接輸出半形/上檔符號 |
341 | | - return map[code] || code; // map[code] 對於數字和符號會輸出正確的上檔符號或數字,對於 ' ' 則輸出 ' ' |
| 334 | + if (code.match(/^[0-9;'./,<> ]$/)) { |
| 335 | + return map[code] || code; |
342 | 336 | } |
343 | 337 | return null; |
344 | 338 | } |
345 | | - |
346 | | - // 2. 檢查是否為中文模式下的直接輸出 (無,中文模式下的數字輸出在 handleChineseInput 中處理) |
347 | 339 | return null; |
348 | 340 | } |
349 | 341 |
|
| 342 | + // --- 核心工具函數:管理游標與輸入 --- |
| 343 | + |
| 344 | + // 確保游標在 contenteditable 元素的末尾 |
| 345 | + function focusAndMoveCaretToEnd() { |
| 346 | + textOutput.focus(); |
| 347 | + const range = document.createRange(); |
| 348 | + const selection = window.getSelection(); |
| 349 | + |
| 350 | + // 將範圍設定為 contenteditable 元素的所有內容 |
| 351 | + range.selectNodeContents(textOutput); |
| 352 | + // 將游標收縮到範圍的末尾 (即文字末尾) |
| 353 | + range.collapse(false); |
| 354 | + |
| 355 | + selection.removeAllRanges(); |
| 356 | + selection.addRange(range); |
| 357 | + } |
| 358 | + |
| 359 | + // 在游標位置插入文字 (適用於 contenteditable) |
| 360 | + function insertTextAtCaret(text) { |
| 361 | + textOutput.focus(); |
| 362 | + // 這是模擬鍵盤輸入最可靠的方式,它會在游標位置插入文字 |
| 363 | + document.execCommand('insertText', false, text); |
| 364 | + // 由於 execCommand 可能不會移動到絕對末尾,確保游標被重新定位 |
| 365 | + focusAndMoveCaretToEnd(); |
| 366 | + } |
| 367 | + |
| 368 | + |
350 | 369 | // --- 舊有代碼區塊 --- |
351 | 370 |
|
352 | 371 | // DOM 元素 |
|
387 | 406 | } |
388 | 407 |
|
389 | 408 | async function loadSecondaryCodemap() { |
390 | | - // 注意:這裡假設您的編碼檔名為 3codes2.txt 並且在同目錄 |
391 | 409 | const fileName = './3codes2.txt'; |
392 | 410 | try { |
393 | 411 | const response = await fetch(fileName); |
|
404 | 422 | mainInput.textContent = `錯誤:無法載入編碼檔 ${fileName}。請確認檔案存在。`; |
405 | 423 | } |
406 | 424 | resetInputState(); |
| 425 | + focusAndMoveCaretToEnd(); // 載入後立即定位游標 |
407 | 426 | } |
408 | 427 |
|
409 | 428 | // --- 處理中文模式輸入的核心函數 --- |
|
413 | 432 | // 1. 數字鍵處理 (直接輸出半形數字) |
414 | 433 | if (inputChar.match(/^[0-9]$/)) { |
415 | 434 | if (currentCandidates.length > 0) { |
416 | | - selectCandidate(0); // 如果有編碼,先上字 |
| 435 | + selectCandidate(0); |
417 | 436 | } |
418 | | - // 中文模式的數字直接輸出數字 |
419 | | - textOutput.textContent += inputChar; |
| 437 | + insertTextAtCaret(inputChar); // 插入數字 |
420 | 438 | return; |
421 | 439 | } |
422 | 440 |
|
|
425 | 443 | if (currentCode.length > 0) { |
426 | 444 | currentCode = currentCode.slice(0, -1); |
427 | 445 | updateStateAndDisplay(); |
428 | | - } else if (textOutput.textContent.length > 0) { |
429 | | - textOutput.textContent = textOutput.textContent.slice(0, -1); |
| 446 | + } else { |
| 447 | + // 游標位置刪除 |
| 448 | + textOutput.focus(); |
| 449 | + document.execCommand('delete', false, null); |
430 | 450 | } |
431 | 451 | return; |
432 | 452 | } |
433 | 453 | if (code === 'enter') { |
434 | 454 | if (currentCandidates.length > 0) { |
435 | 455 | selectCandidate(0); |
436 | 456 | } else { |
437 | | - textOutput.textContent += '\n'; |
| 457 | + insertTextAtCaret('\n'); |
438 | 458 | } |
439 | 459 | return; |
440 | 460 | } |
|
443 | 463 | if (currentCandidates.length > 0) { |
444 | 464 | selectCandidate(0); |
445 | 465 | } else { |
446 | | - textOutput.textContent += ' '; // 輸出全形空白 |
| 466 | + insertTextAtCaret(' '); // 輸出全形空白 |
447 | 467 | } |
448 | 468 | return; |
449 | 469 | } |
|
466 | 486 | function handleEnglishInput(code) { |
467 | 487 | // 1. 處理功能鍵 |
468 | 488 | if (code === 'backspace') { |
469 | | - if (textOutput.textContent.length > 0) { |
470 | | - textOutput.textContent = textOutput.textContent.slice(0, -1); |
471 | | - } |
| 489 | + textOutput.focus(); |
| 490 | + document.execCommand('delete', false, null); // 游標位置刪除 |
472 | 491 | return; |
473 | 492 | } |
474 | 493 | if (code === 'enter') { |
475 | | - textOutput.textContent += '\n'; |
| 494 | + insertTextAtCaret('\n'); |
476 | 495 | return; |
477 | 496 | } |
478 | 497 | if (code === ' ') { |
479 | | - textOutput.textContent += ' '; // 輸出半形空白 |
| 498 | + insertTextAtCaret(' '); // 輸出半形空白 |
480 | 499 | return; |
481 | 500 | } |
482 | 501 |
|
483 | 502 | // 2. 處理英文字母、數字和符號 |
484 | 503 | const outputChar = getOutputChar(code); |
485 | 504 | if (outputChar) { |
486 | | - textOutput.textContent += outputChar; |
| 505 | + insertTextAtCaret(outputChar); |
487 | 506 | } |
488 | 507 | } |
489 | 508 |
|
490 | 509 | // --- 核心狀態更新與顯示函數 (中文模式專用) --- |
491 | 510 | function updateStateAndDisplay() { |
| 511 | + // 修正: 僅顯示編碼,不加自定義游標 |
492 | 512 | mainInput.textContent = formatCode(currentCode); |
| 513 | + |
493 | 514 | currentCandidates = currentCodemap[currentCode] || []; |
494 | 515 |
|
495 | 516 | if (currentCode.length === MAX_CODE_LENGTH) { |
496 | 517 | if (currentCandidates.length === 1) { |
497 | | - textOutput.textContent += currentCandidates[0]; |
| 518 | + insertTextAtCaret(currentCandidates[0]); // 使用 insertTextAtCaret |
498 | 519 | resetInputState(); |
499 | 520 | return; |
500 | 521 | } else if (currentCandidates.length === 0) { |
|
532 | 553 | function selectCandidate(index) { |
533 | 554 | const selectedCandidate = currentCandidates[index]; |
534 | 555 | if (selectedCandidate) { |
535 | | - textOutput.textContent += selectedCandidate; |
| 556 | + insertTextAtCaret(selectedCandidate); // 使用 insertTextAtCaret |
536 | 557 | resetInputState(); |
537 | 558 | } |
538 | 559 | } |
|
542 | 563 | currentCode = ''; |
543 | 564 | currentCandidates = []; |
544 | 565 |
|
545 | | - // 更新輸入提示文字 |
| 566 | + // 更新輸入提示文字 (純文字) |
546 | 567 | mainInput.textContent = isEnglishMode ? '英文模式' : '編碼顯示在此'; |
547 | 568 | showCandidates(); |
| 569 | + focusAndMoveCaretToEnd(); // 重設後確保游標在輸出區 |
548 | 570 | } |
549 | 571 |
|
550 | 572 | // --- 格式化編碼顯示 --- |
|
560 | 582 | // --- 更新鍵盤按鍵顯示 --- |
561 | 583 | function updateKeyboardDisplay() { |
562 | 584 | const map = getCurrentDisplayMap(); |
563 | | - // 中文模式下,字母鍵的視覺始終是 letter_maps.cn_normal (大寫 A-Z) |
564 | 585 | const letterMap = isEnglishMode ? (isShiftActive ? letter_maps.en_shifted : letter_maps.en_normal) : letter_maps.cn_normal; |
565 | 586 |
|
566 | 587 | document.querySelectorAll('.key[data-code]').forEach(keyElement => { |
567 | 588 | const code = keyElement.dataset.code; |
568 | 589 |
|
569 | 590 | if (map[code]) { |
570 | | - // 處理非字母鍵和特殊功能鍵 (包括 ' ') |
571 | 591 | keyElement.textContent = map[code]; |
572 | 592 | } else if (code.match(/^[a-z]$/)) { |
573 | | - // 處理字母鍵 (a-z) |
574 | 593 | keyElement.textContent = letterMap(code); |
575 | 594 | } |
576 | 595 | }); |
577 | 596 | } |
578 | 597 |
|
579 | | - // --- 動態生成手機優化鍵盤 (符號鍵位置修正) --- |
| 598 | + // --- 動態生成手機優化鍵盤 (無變動) --- |
580 | 599 | function generateMobileKeyboard() { |
581 | 600 | const layout_mobile = [ |
582 | 601 | ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0'], |
|
598 | 617 |
|
599 | 618 | let code = keyText.toLowerCase(); |
600 | 619 |
|
601 | | - // 設定 data-code 和樣式 |
602 | 620 | if (keyText === 'Back') { |
603 | 621 | code = 'backspace'; |
604 | 622 | key.classList.add('function-key'); |
|
618 | 636 |
|
619 | 637 | key.dataset.code = code; |
620 | 638 |
|
621 | | - // 初始顯示: 根據初始模式 (中文, normal) 顯示正確的圖示或文字 |
622 | 639 | const initialMap = getCurrentDisplayMap(); |
623 | 640 | if (initialMap[code]) { |
624 | 641 | key.textContent = initialMap[code]; |
625 | 642 | } else if (code.match(/^[a-z]$/)) { |
626 | 643 | key.textContent = letter_maps.cn_normal(code); |
627 | 644 | } else { |
628 | | - key.textContent = keyText; // 保險措施 |
| 645 | + key.textContent = keyText; |
629 | 646 | } |
630 | 647 |
|
631 | 648 | // --- 鍵盤點擊事件 --- |
632 | | - key.addEventListener('click', () => { |
| 649 | + key.addEventListener('click', (event) => { |
| 650 | + // 阻止點擊鍵盤時,文字輸出區失去焦點 |
| 651 | + event.preventDefault(); |
| 652 | + |
633 | 653 | if (code === 'lang_toggle') { |
634 | 654 | isEnglishMode = !isEnglishMode; |
635 | 655 | isShiftActive = false; |
636 | 656 | updateKeyboardDisplay(); |
637 | | - resetInputState(); // 模式切換後重設輸入狀態並更新提示文字 |
| 657 | + resetInputState(); |
638 | 658 | } else if (code === 'case_shift') { |
639 | 659 | isShiftActive = !isShiftActive; |
640 | 660 | updateKeyboardDisplay(); |
|
656 | 676 | * 複製文字 |
657 | 677 | */ |
658 | 678 | function copyTextToClipboard() { |
659 | | - const textToCopy = textOutput.textContent; |
| 679 | + // contenteditable 區域的文字內容 |
| 680 | + const textToCopy = textOutput.textContent; |
660 | 681 | if (!textToCopy) { |
661 | 682 | alert('輸出區沒有文字可以複製!'); |
662 | 683 | return; |
|
682 | 703 | } |
683 | 704 | document.body.removeChild(tempInput); |
684 | 705 | } |
| 706 | + focusAndMoveCaretToEnd(); // 複製後,游標保持活躍 |
685 | 707 | } |
686 | 708 |
|
687 | 709 | /** |
|
691 | 713 | if (confirm('確定要清空所有已輸入的文字嗎?')) { |
692 | 714 | textOutput.textContent = ''; |
693 | 715 | resetInputState(); |
| 716 | + focusAndMoveCaretToEnd(); // 清空後確保游標在輸出區 |
694 | 717 | } |
695 | 718 | } |
696 | 719 |
|
|
0 commit comments