|
9 | 9 | <link href="https://fonts.googleapis.com/css2?family=Noto+Sans+TC:wght@400;700&display=swap" rel="stylesheet"> |
10 | 10 | <style> |
11 | 11 | /* ----------------------------------------------------- */ |
12 | | - /* 樣式定義 */ |
| 12 | + /* 樣式定義 (略,與前版相同) */ |
13 | 13 | /* ----------------------------------------------------- */ |
14 | 14 |
|
15 | 15 | :root { |
|
101 | 101 | background-color: var(--primary-color); |
102 | 102 | color: white; |
103 | 103 | cursor: pointer; |
104 | | - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); |
| 104 | + box-shadow: 0 2px 5px rgba |
| 105 | + |
| 106 | +[Image of X] |
| 107 | +(0, 0, 0, 0.1); |
105 | 108 | transition: background-color 0.2s; |
106 | 109 | } |
107 | 110 |
|
|
440 | 443 | let cursorPosition = 0; |
441 | 444 | let preferredOffset = -1; |
442 | 445 |
|
| 446 | + // --- ADDED: 簡繁切換狀態 --- |
| 447 | + let isSimplifiedMode = false; // 預設為繁體模式 |
| 448 | + |
443 | 449 | // --- 長按相關狀態 --- |
444 | 450 | let longPressTimer = null; |
445 | 451 | let longPressCommitted = false; |
|
484 | 490 | const KEYCODE_DOWN=0xff54; |
485 | 491 | const KEYCODE_DEL_F=0xff; |
486 | 492 |
|
487 | | - // --- 鍵盤配置 --- |
| 493 | + // --- 簡繁轉換表 (基礎示範用,完整轉換需要更大的字典) --- |
| 494 | + const TS_MAP = { |
| 495 | + '體': '体', '碼': '码', '說': '说', '這': '这', '個': '个', |
| 496 | + '們': '们', '樂': '乐', '風': '风', '開': '开', '對': '对', |
| 497 | + '會': '会', '國': '国', '議': '议', '變': '变', '應': '应', |
| 498 | + '學': '学', '發': '发', '廠': '厂', '廣': '广', '裡': '里', |
| 499 | + '幾': '几', '為': '为', '從': '从', '後': '后', '麵': '面', |
| 500 | + '術': '术', '隻': '只', '頭': '头', '話': '话', '裡': '里' |
| 501 | + }; |
| 502 | + |
| 503 | + /** |
| 504 | + * 執行簡體字轉換 |
| 505 | + */ |
| 506 | + function convertToSimplified(text) { |
| 507 | + if (!isSimplifiedMode) { |
| 508 | + return text; // 繁體模式,不轉換 |
| 509 | + } |
| 510 | + |
| 511 | + let simplifiedText = ''; |
| 512 | + for (const char of text) { |
| 513 | + // 簡單的字符替換 |
| 514 | + simplifiedText += TS_MAP[char] || char; |
| 515 | + } |
| 516 | + return simplifiedText; |
| 517 | + } |
| 518 | + |
| 519 | + // --- 鍵盤配置 (略,與前版相同) --- |
488 | 520 | var Keyboards = { |
489 | 521 | english: { |
490 | 522 | shifted : false, |
|
493 | 525 | 'q':'Q', 'w':'W', 'e':'E', 'r':'R', 't':'T', 'y':'Y', 'u':'U', 'i':'I', 'o':'O', 'p':'P', |
494 | 526 | 'a':'A', 's':'S', 'd':'D', 'f':'F', 'g':'G', 'h':'H', 'j':'J', 'k':'K', 'l':'L', |
495 | 527 |
|
496 | | - // **修正點:新增底部一排 (zxcvbnm) 的全形字元** |
| 528 | + // 底部一排 (zxcvbnm) |
497 | 529 | 'z':'Z', 'x':'X', 'c':'C', 'v':'V', 'b':'B', 'n':'N', 'm':'M', |
498 | 530 |
|
499 | 531 | // 標點符號 |
|
727 | 759 | } |
728 | 760 | } |
729 | 761 |
|
730 | | - // --- ADDED: 浮層處理函數 --- |
| 762 | + // --- ADDED: 浮層處理函數 (略,與前版相同) --- |
731 | 763 |
|
732 | 764 | /** |
733 | 765 | * 處理表情符號/編輯器的浮層顯示與隱藏 |
734 | | - * @param {string} type - 'emoji' 或 'editor' |
735 | | - * @param {number|boolean} show - 0, 1 (emoji sub index) 或 false (hide) |
736 | 766 | */ |
737 | 767 | function overlay(type, show) { |
738 | | - // 使用現有的 overlayDiv 元素 |
739 | 768 | const div = overlayDiv; |
740 | 769 |
|
741 | | - // 隱藏/清理舊浮層 |
742 | 770 | const isShowing = div.dataset.show; |
743 | 771 | if (isShowing) { |
744 | 772 | div.style.display = 'none'; |
745 | 773 | div.innerHTML = ''; |
746 | 774 | delete div.dataset.show; |
747 | | - // 如果點擊了當前正在顯示的按鈕,則隱藏後返回 |
748 | 775 | if (isShowing == show && div.className == type) return; |
749 | 776 | } |
750 | 777 |
|
751 | 778 | if (show === false) return; |
752 | 779 |
|
753 | | - // --- 修正點: 取得鍵盤的實際位置和尺寸,使其覆蓋整個按鍵區 (#keyboard) --- |
754 | 780 | const keyboard = document.getElementById('keyboard'); |
755 | | - const rect = keyboard.getBoundingClientRect(); // 取得鍵盤元素在視窗中的位置和尺寸 |
| 781 | + const rect = keyboard.getBoundingClientRect(); |
756 | 782 |
|
757 | 783 | const width = rect.width; |
758 | | - const height = rect.height; // 取得按鍵區的實際高度 |
| 784 | + const height = rect.height; |
759 | 785 |
|
760 | | - // 配置浮層 div |
761 | 786 | div.dataset.show = show; |
762 | 787 | div.style.display = 'block'; |
763 | | - div.className = type; // 應用 class for styling |
| 788 | + div.className = type; |
764 | 789 |
|
765 | | - // 確保浮層與鍵盤位置重合,使用 fixed 定位來精確覆蓋鍵盤 (#keyboard) |
766 | 790 | div.style.position = 'fixed'; |
767 | 791 | div.style.left = rect.left + 'px'; |
768 | | - div.style.top = rect.top + 'px'; // 從鍵盤頂部開始 |
| 792 | + div.style.top = rect.top + 'px'; |
769 | 793 | div.style.width = width + 'px'; |
770 | 794 | div.style.height = height + 'px'; |
771 | 795 |
|
772 | 796 | const div2 = document.createElement('DIV'); |
773 | 797 | div2.style.width = width + 'px'; |
774 | 798 | div2.style.height = height + 'px'; |
775 | | - div2.style.overflowY = 'auto'; // 讓內容可滾動 |
| 799 | + div2.style.overflowY = 'auto'; |
776 | 800 |
|
777 | | - // --- FIX: Add Flexbox setup for 10-column layout --- |
778 | 801 | if (type === "emoji") { |
779 | 802 | div2.style.display = 'flex'; |
780 | 803 | div2.style.flexWrap = 'wrap'; |
781 | 804 | div2.style.alignContent = 'flex-start'; |
782 | | - // 調整 padding 以容納按鍵間距 |
783 | 805 | div2.style.padding = '3px'; |
784 | 806 | } |
785 | | - // --- END FIX --- |
786 | 807 |
|
787 | | - div.innerHTML = ''; // 清空內容 |
| 808 | + div.innerHTML = ''; |
788 | 809 | div.appendChild(div2); |
789 | | - // --- 結束修正點 --- |
790 | | - |
| 810 | + |
791 | 811 | if (type === "emoji") { |
792 | 812 | const emojis = [[ |
793 | 813 | "😀","😁","😂","😃","😄","😅","😆","😇","😈","😉", |
|
815 | 835 |
|
816 | 836 | const emojiSet = emojis[show] || emojis[0]; |
817 | 837 | const rowLength = 10; |
818 | | - const displayRows = 4; // <--- 固定顯示 4 行 |
| 838 | + const displayRows = 4; |
819 | 839 |
|
820 | | - // 由於 CSS 樣式新增了 2px margin,這裡需要調整寬度計算 |
821 | | - // 為了簡單起見並使用 flexbox,我們設定按鈕寬度為 10% |
822 | | - |
823 | | - const keyHeight = height / displayRows; // <--- 按鈕高度為浮層高度的 1/4 |
| 840 | + const keyHeight = height / displayRows; |
824 | 841 |
|
825 | 842 | for (let i = 0; i < emojiSet.length; i++) { |
826 | 843 | const one = document.createElement('button'); |
827 | 844 | one.textContent = emojiSet[i]; |
828 | 845 |
|
829 | | - // --- FIX: 使用百分比寬度和計算高度確保 10 欄精確排列 --- |
830 | | - one.style.width = `calc(${100 / rowLength}% - 4px)`; /* 10% 減去左右 margin (2px*2) */ |
831 | | - one.style.height = keyHeight - 4 + "px"; /* 高度減去上下 margin (2px*2) */ |
| 846 | + one.style.width = `calc(${100 / rowLength}% - 4px)`; |
| 847 | + one.style.height = keyHeight - 4 + "px"; |
832 | 848 |
|
833 | 849 | one.style.fontSize = '1.8rem'; |
834 | | - // 移除內聯樣式,讓 CSS 來控制按鍵外觀 |
835 | | - // one.style.border = 'none'; |
836 | | - // one.style.background = 'none'; |
837 | | - // one.style.margin = '0'; |
838 | | - // one.style.padding = '0'; |
| 850 | + |
839 | 851 | div2.appendChild(one); |
840 | 852 | } |
841 | 853 |
|
842 | 854 | div2.addEventListener('click', function(e) { |
843 | | - // 確保點擊到的是 button |
844 | 855 | if (e.target.tagName !== 'BUTTON') return; |
845 | 856 |
|
846 | 857 | const text = e.target.textContent; |
847 | | - // 過濾掉可能存在的空白佔位符 |
848 | 858 | if (text && text.length > 0 && text !== ' ') |
849 | 859 | commitText(text); |
850 | | - |
851 | | - // **修正 1:點擊後不再隱藏浮層,以滿足連續輸入的需求** |
852 | | - // overlay("emoji", false); // <--- 已移除此行 |
853 | 860 | }); |
854 | 861 |
|
855 | 862 | } else if (type === "editor") { |
|
888 | 895 | overlay("editor", 0); |
889 | 896 | } |
890 | 897 |
|
891 | | - // --- ADDED: 語音輸入邏輯 --- |
892 | | - |
893 | | - /** |
894 | | - * 啟動 Web Speech API 語音辨識 |
895 | | - */ |
896 | | - function startVoiceInput() { |
897 | | - // 檢查瀏覽器是否支援 Web Speech API |
898 | | - const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; |
899 | | - |
900 | | - if (!SpeechRecognition) { |
901 | | - alert('抱歉,您的瀏覽器不支援 Web 語音輸入功能 (Speech Recognition API)。建議使用 Chrome 瀏覽器。'); |
902 | | - return; |
903 | | - } |
904 | | - |
905 | | - // 建立 Recognition 物件 |
906 | | - const recognition = new SpeechRecognition(); |
907 | | - |
908 | | - // 設定: |
909 | | - recognition.lang = 'cmn-Hant-TW'; // 繁體中文 |
910 | | - recognition.continuous = false; // 每次只錄製一次 |
911 | | - recognition.interimResults = false; // 只傳回最終結果 |
912 | | - |
913 | | - // 儲存原始 UI 狀態 |
914 | | - const originalMainInput = mainInput.textContent; |
915 | | - |
916 | | - // 處理事件 |
917 | | - recognition.onstart = function() { |
918 | | - mainInput.textContent = '🔊 正在聆聽... 請開始說話。'; |
919 | | - candidateDisplay.innerHTML = '<span style="color: red; font-weight: bold;">[語音辨識中...]</span>'; |
920 | | - }; |
921 | | - |
922 | | - recognition.onresult = function(event) { |
923 | | - // 取得最終結果 |
924 | | - const transcript = event.results[event.results.length - 1][0].transcript; |
925 | | - |
926 | | - // 將結果提交到輸出區 |
927 | | - if (transcript) { |
928 | | - commitText(transcript); |
929 | | - } |
930 | | - |
931 | | - // 恢復 UI 狀態 |
932 | | - recognition.onend(); |
933 | | - }; |
934 | | - |
935 | | - recognition.onerror = function(event) { |
936 | | - alert('語音辨識發生錯誤: ' + event.error + ' (請檢查麥克風權限)'); |
937 | | - console.error('Speech Recognition Error:', event.error); |
938 | | - // 恢復 UI 狀態 |
939 | | - recognition.onend(); |
940 | | - }; |
941 | | - |
942 | | - recognition.onend = function() { |
943 | | - // 恢復 UI 狀態 |
944 | | - mainInput.textContent = originalMainInput; |
945 | | - // 重新繪製工具列 |
946 | | - showCandidates(); |
947 | | - }; |
948 | | - |
949 | | - try { |
950 | | - recognition.start(); |
951 | | - } catch (e) { |
952 | | - alert('啟動語音輸入失敗,請檢查瀏覽器麥克風權限設定。'); |
953 | | - console.error('Recognition start failed:', e); |
954 | | - // 恢復 UI 狀態 |
955 | | - mainInput.textContent = originalMainInput; |
956 | | - showCandidates(); |
957 | | - } |
958 | | - } |
| 898 | + // --- REMOVED: startVoiceInput function --- |
959 | 899 |
|
960 | 900 | // --- ADDED: 工具列邏輯 --- |
961 | 901 |
|
962 | 902 | function showToolbar() { |
963 | 903 | candidateDisplay.innerHTML = ''; |
964 | 904 |
|
| 905 | + // **CHANGE: Replaced '🎤' with '簡/繁' toggle** |
965 | 906 | const toolbarItems = [ |
966 | | - {title:"🎤",tool:"1"}, |
| 907 | + {title: isSimplifiedMode ? "簡" : "繁", tool:"1"}, // 簡繁切換鍵 |
967 | 908 | {title:"😊",tool:"2"}, // Emoji 1 |
968 | 909 | {title:"☀",tool:"3"}, // Emoji 2 |
969 | 910 | {title:"🌀",tool:"4"}, // Placeholder |
|
987 | 928 | function handleToolbarClick(target) { |
988 | 929 | const tool = target.dataset.tool; |
989 | 930 | if (tool === "1") { |
990 | | - startVoiceInput(); // <-- 呼叫新的語音輸入函式 |
| 931 | + // **CHANGE: Simplified/Traditional Mode Toggle** |
| 932 | + isSimplifiedMode = !isSimplifiedMode; |
| 933 | + showCandidates(); // Redraw toolbar to update "簡" / "繁" label |
| 934 | + console.log("Mode switched to:", isSimplifiedMode ? "Simplified" : "Traditional"); |
991 | 935 | } else if (tool === "2") { |
992 | 936 | overlay("emoji", 0); |
993 | 937 | } else if (tool === "3") { |
|
1055 | 999 | */ |
1056 | 1000 | function handleFunctionKey(code, keyChar) { |
1057 | 1001 | // 確保在處理功能鍵時隱藏浮層 |
1058 | | - // **注意:如果用戶在 emoji 浮層上點擊了功能鍵(例如切換到 QWERTY),則需要關閉浮層。** |
1059 | 1002 | if (code === KEYCODE_SWITCH_english || code === KEYCODE_SWITCH_number || code === KEYCODE_STATE || code === KEYCODE_DEL || code === KEYCODE_ENTER || code === KEYCODE_SPACE) { |
1060 | 1003 | overlayDiv.style.display = 'none'; |
1061 | 1004 | delete overlayDiv.dataset.show; |
|
1338 | 1281 | } |
1339 | 1282 | } |
1340 | 1283 |
|
| 1284 | + /** |
| 1285 | + * 提交文字 (包含簡繁轉換邏輯) |
| 1286 | + */ |
1341 | 1287 | function commitText(textToCommit) { |
1342 | | - committedText = committedText.slice(0, cursorPosition) + textToCommit + committedText.slice(cursorPosition); |
1343 | | - cursorPosition += textToCommit.length; |
| 1288 | + // **CHANGE: 在提交前進行簡繁轉換** |
| 1289 | + const finalCommitText = convertToSimplified(textToCommit); |
| 1290 | + |
| 1291 | + committedText = committedText.slice(0, cursorPosition) + finalCommitText + committedText.slice(cursorPosition); |
| 1292 | + cursorPosition += finalCommitText.length; |
1344 | 1293 | preferredOffset = -1; |
1345 | 1294 | resetInputState(); |
1346 | 1295 | } |
|
1387 | 1336 |
|
1388 | 1337 | async function loadSecondaryCodemap() { |
1389 | 1338 | const fileName = './3codes2.txt'; |
1390 | | - let textToParse = `# 這是備用的模擬編碼表 (用於測試)\n測\tqwe\n試\trty\n工\taaa\n夫\tqwe\n「\t;'\n」\t';\n這\tqwe\n`; |
| 1339 | + let textToParse = `# 這是備用的模擬編碼表 (用於測試)\n測\tqwe\n試\trty\n工\taaa\n夫\tqwe\n這\tqwe\n代\tqwe\n碼\tzxc\n說\tzxc\n裡\tqwe\n個\tzxc\n對\tqwe\n會\trty\n國\taaa\n議\tqwe\n變\tqwe\n應\tqwe\n學\trty\n發\taaa\n廠\tqwe\n廣\tqwe\n裡\tqwe\n幾\tqwe\n為\trty\n從\taaa\n後\tqwe\n麵\tqwe\n術\tqwe\n隻\trty\n頭\taaa\n話\tqwe\n裡\tqwe\n「\t;'\n」\t';\n`; |
1391 | 1340 | let statusMessage = '編碼表載入中...'; |
1392 | 1341 |
|
1393 | 1342 | try { |
|
1475 | 1424 |
|
1476 | 1425 | /** |
1477 | 1426 | * 顯示候選字或工具列 |
1478 | | - * **修正 2:確保浮層開啟時,工具列(表情符號切換/關閉鍵)依然顯示。** |
1479 | 1427 | */ |
1480 | 1428 | function showCandidates() { |
1481 | 1429 | candidateDisplay.innerHTML = ''; |
1482 | 1430 |
|
1483 | 1431 | // 1. 如果浮層開啟 (Emoji/Editor) |
1484 | 1432 | if (overlayDiv.dataset.show) { |
1485 | | - // 修正:浮層開啟時,必須顯示工具列,讓用戶可以切換頁面或關閉浮層。 |
1486 | 1433 | showToolbar(); |
1487 | 1434 | return; |
1488 | 1435 | } |
|
1507 | 1454 |
|
1508 | 1455 | currentCandidates.forEach((candidate, index) => { |
1509 | 1456 | const span = document.createElement('span'); |
| 1457 | + // **NOTE: 候選字在顯示時不轉換,僅在選字 commitText 時才轉換。** |
1510 | 1458 | span.textContent = candidate; |
1511 | 1459 | span.onclick = () => selectCandidate(index); |
1512 | 1460 | candidateDisplay.appendChild(span); |
|
0 commit comments