|
126 | 126 | box-shadow: 0 4px 10px rgba(0, 0, 0, 0.08); |
127 | 127 | margin-bottom: 5px; |
128 | 128 | padding: 8px; |
129 | | - height: 40vh; |
| 129 | + height: 20vh; /* Reduced from 40vh to 20vh */ |
130 | 130 | font-size: 1.5rem; |
131 | 131 | line-height: 1.6; |
132 | 132 | white-space: pre-wrap; |
|
364 | 364 |
|
365 | 365 | #keyboard-overlay.emoji button { |
366 | 366 | box-sizing: border-box; |
367 | | - float: left; |
| 367 | + /* 由於 flexbox 佈局,float: left 被移除 */ |
368 | 368 | text-align: center; |
369 | 369 | font-size: 1.8rem; |
| 370 | + /* ADDED: 使其看起來像獨立的按鍵 */ |
| 371 | + background-color: var(--key-bg); /* 使用鍵盤背景色 #f9f9f9 */ |
| 372 | + border: 1px solid var(--border-color); /* 添加邊框 */ |
| 373 | + border-radius: 6px; /* 圓角 */ |
| 374 | + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); /* 陰影效果 */ |
| 375 | + margin: 2px; /* 增加微小間隔,讓按鍵更明顯 */ |
| 376 | + } |
| 377 | + |
| 378 | + /* ADDED: 點擊時的反白效果 */ |
| 379 | + #keyboard-overlay.emoji button:active { |
| 380 | + background-color: var(--key-active-bg); /* 使用點擊時的顏色 #a0a0a0 */ |
| 381 | + color: white; /* 讓表情符號更突出 */ |
| 382 | + box-shadow: 0 0 1px rgba(0, 0, 0, 0.2); |
| 383 | + transform: translateY(1px); /* 輕微下壓效果 */ |
370 | 384 | } |
371 | 385 |
|
372 | 386 | #keyboard-overlay.editor { |
|
470 | 484 | const KEYCODE_DOWN=0xff54; |
471 | 485 | const KEYCODE_DEL_F=0xff; |
472 | 486 |
|
473 | | - // --- 鍵盤配置 (略) --- |
| 487 | + // --- 鍵盤配置 --- |
474 | 488 | var Keyboards = { |
475 | 489 | english: { |
476 | 490 | shifted : false, |
477 | 491 | alt : { |
| 492 | + // 頂部兩排 |
478 | 493 | 'q':'Q', 'w':'W', 'e':'E', 'r':'R', 't':'T', 'y':'Y', 'u':'U', 'i':'I', 'o':'O', 'p':'P', |
479 | 494 | 'a':'A', 's':'S', 'd':'D', 'f':'F', 'g':'G', 'h':'H', 'j':'J', 'k':'K', 'l':'L', |
| 495 | + |
| 496 | + // **修正點:新增底部一排 (zxcvbnm) 的全形字元** |
| 497 | + 'z':'Z', 'x':'X', 'c':'C', 'v':'V', 'b':'B', 'n':'N', 'm':'M', |
| 498 | + |
| 499 | + // 標點符號 |
480 | 500 | ';':';', // 全形 |
481 | 501 | '\'':'〃', // 全形 |
482 | 502 | ',':',', // 全形 |
|
516 | 536 | [{v:'1'},{v:'2'},{v:'3'},{v:'4'},{v:'5'}, |
517 | 537 | {v:'6'},{v:'7'},{v:'8'},{v:'9'},{v:'0'}], |
518 | 538 | [{v:'@'},{v:'#'},{v:'$'},{v:'%'},{v:'&'}, |
519 | | - {v:'*'},{v:'-'},{v:'+'},{v:'('},{v:')'}], |
| 539 | + {v:'*'},{v:'-'},{v:'+'},{v:'('},{v:'('}], |
520 | 540 | [{v:'⇪',c:KEYCODE_SHIFT,r:1.5,s:true}, |
521 | 541 | {v:'!'},{v:'"'},{v:'\''},{v:':'},{v:';'},{v:'/'},{v:'?'}, |
522 | 542 | {v:'⌫',c:KEYCODE_DEL,r:1.5,s:true}], |
|
730 | 750 |
|
731 | 751 | if (show === false) return; |
732 | 752 |
|
733 | | - // 取得尺寸 |
734 | | - const cand = document.getElementById('candidate-display'); |
| 753 | + // --- 修正點: 取得鍵盤的實際位置和尺寸,使其覆蓋整個按鍵區 (#keyboard) --- |
735 | 754 | const keyboard = document.getElementById('keyboard'); |
736 | | - const width = keyboard.clientWidth; |
737 | | - // 浮層高度 = 鍵盤總高度 - 候選字列高度 |
738 | | - const height = keyboard.clientHeight - cand.clientHeight; |
| 755 | + const rect = keyboard.getBoundingClientRect(); // 取得鍵盤元素在視窗中的位置和尺寸 |
| 756 | + |
| 757 | + const width = rect.width; |
| 758 | + const height = rect.height; // 取得按鍵區的實際高度 |
739 | 759 |
|
740 | 760 | // 配置浮層 div |
741 | 761 | div.dataset.show = show; |
742 | 762 | div.style.display = 'block'; |
743 | | - div.style.position = 'absolute'; // 確保能定位 |
744 | | - div.style.left = '0px'; |
745 | | - div.style.top = cand.clientHeight + 'px'; |
| 763 | + div.className = type; // 應用 class for styling |
| 764 | + |
| 765 | + // 確保浮層與鍵盤位置重合,使用 fixed 定位來精確覆蓋鍵盤 (#keyboard) |
| 766 | + div.style.position = 'fixed'; |
| 767 | + div.style.left = rect.left + 'px'; |
| 768 | + div.style.top = rect.top + 'px'; // 從鍵盤頂部開始 |
746 | 769 | div.style.width = width + 'px'; |
747 | 770 | div.style.height = height + 'px'; |
748 | 771 |
|
749 | 772 | const div2 = document.createElement('DIV'); |
750 | 773 | div2.style.width = width + 'px'; |
751 | 774 | div2.style.height = height + 'px'; |
752 | 775 | div2.style.overflowY = 'auto'; // 讓內容可滾動 |
| 776 | + |
| 777 | + // --- FIX: Add Flexbox setup for 10-column layout --- |
| 778 | + if (type === "emoji") { |
| 779 | + div2.style.display = 'flex'; |
| 780 | + div2.style.flexWrap = 'wrap'; |
| 781 | + div2.style.alignContent = 'flex-start'; |
| 782 | + // 調整 padding 以容納按鍵間距 |
| 783 | + div2.style.padding = '3px'; |
| 784 | + } |
| 785 | + // --- END FIX --- |
753 | 786 |
|
754 | 787 | div.innerHTML = ''; // 清空內容 |
755 | 788 | div.appendChild(div2); |
756 | | - div.className = type; // 應用 class for styling |
| 789 | + // --- 結束修正點 --- |
757 | 790 |
|
758 | 791 | if (type === "emoji") { |
759 | 792 | const emojis = [[ |
|
775 | 808 | "🎁","🎂","🎄","🎅","🎈","🎓","👌","👍","👎","👏", |
776 | 809 | "🎨","🎯","🎲","🏀","🏃","🏅","🏆","🏐","🏓","🏟", |
777 | 810 | "🏧","🐀","🐁","🐂","🐃","🐄","🐅","🐆","🐇","🐈", |
778 | | - "🐉","🐊","🐋","🐌","🐍","🐎","🐏","🐐","🐑","🐒", |
| 811 | + "🐉","🐊","🐋","🐌","蛇","🐎","🐏","🐐","🐑","🐒", |
779 | 812 | "🐓","🐔","🐕","🐖","🐗","🐘","🐙","🐚","🐛","🐜", |
780 | 813 | "🐝","🐞","🐟","🐠","🐢","🐧","👣","👫","💊","💋" |
781 | 814 | ]]; |
782 | 815 |
|
783 | 816 | const emojiSet = emojis[show] || emojis[0]; |
784 | 817 | const rowLength = 10; |
785 | | - const keyWidth = width / rowLength; |
| 818 | + const displayRows = 4; // <--- 固定顯示 4 行 |
| 819 | + |
| 820 | + // 由於 CSS 樣式新增了 2px margin,這裡需要調整寬度計算 |
| 821 | + // 為了簡單起見並使用 flexbox,我們設定按鈕寬度為 10% |
786 | 822 |
|
787 | | - // 計算每行有多少按鈕,然後估算每個按鈕的高度 |
788 | | - const numRows = Math.ceil(emojiSet.length / rowLength); |
789 | | - const keyHeight = height / (numRows > 0 ? numRows : 1); |
| 823 | + const keyHeight = height / displayRows; // <--- 按鈕高度為浮層高度的 1/4 |
790 | 824 |
|
791 | 825 | for (let i = 0; i < emojiSet.length; i++) { |
792 | 826 | const one = document.createElement('button'); |
793 | 827 | one.textContent = emojiSet[i]; |
794 | | - one.style.width = keyWidth + "px"; |
795 | | - one.style.height = keyHeight + "px"; |
| 828 | + |
| 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) */ |
| 832 | + |
796 | 833 | one.style.fontSize = '1.8rem'; |
797 | | - one.style.border = 'none'; |
798 | | - one.style.background = 'none'; |
799 | | - one.style.margin = '0'; |
800 | | - one.style.padding = '0'; |
| 834 | + // 移除內聯樣式,讓 CSS 來控制按鍵外觀 |
| 835 | + // one.style.border = 'none'; |
| 836 | + // one.style.background = 'none'; |
| 837 | + // one.style.margin = '0'; |
| 838 | + // one.style.padding = '0'; |
801 | 839 | div2.appendChild(one); |
802 | 840 | } |
803 | 841 |
|
804 | 842 | div2.addEventListener('click', function(e) { |
| 843 | + // 確保點擊到的是 button |
| 844 | + if (e.target.tagName !== 'BUTTON') return; |
| 845 | + |
805 | 846 | const text = e.target.textContent; |
806 | 847 | // 過濾掉可能存在的空白佔位符 |
807 | 848 | if (text && text.length > 0 && text !== ' ') |
808 | 849 | commitText(text); |
| 850 | + |
| 851 | + // **修正 1:點擊後不再隱藏浮層,以滿足連續輸入的需求** |
| 852 | + // overlay("emoji", false); // <--- 已移除此行 |
809 | 853 | }); |
810 | 854 |
|
811 | 855 | } else if (type === "editor") { |
|
844 | 888 | overlay("editor", 0); |
845 | 889 | } |
846 | 890 |
|
| 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 | + } |
| 959 | + |
847 | 960 | // --- ADDED: 工具列邏輯 --- |
848 | 961 |
|
849 | 962 | function showToolbar() { |
|
874 | 987 | function handleToolbarClick(target) { |
875 | 988 | const tool = target.dataset.tool; |
876 | 989 | if (tool === "1") { |
877 | | - console.log('Voice input triggered (Not implemented).'); |
| 990 | + startVoiceInput(); // <-- 呼叫新的語音輸入函式 |
878 | 991 | } else if (tool === "2") { |
879 | 992 | overlay("emoji", 0); |
880 | 993 | } else if (tool === "3") { |
|
942 | 1055 | */ |
943 | 1056 | function handleFunctionKey(code, keyChar) { |
944 | 1057 | // 確保在處理功能鍵時隱藏浮層 |
945 | | - overlayDiv.style.display = 'none'; |
946 | | - delete overlayDiv.dataset.show; |
| 1058 | + // **注意:如果用戶在 emoji 浮層上點擊了功能鍵(例如切換到 QWERTY),則需要關閉浮層。** |
| 1059 | + if (code === KEYCODE_SWITCH_english || code === KEYCODE_SWITCH_number || code === KEYCODE_STATE || code === KEYCODE_DEL || code === KEYCODE_ENTER || code === KEYCODE_SPACE) { |
| 1060 | + overlayDiv.style.display = 'none'; |
| 1061 | + delete overlayDiv.dataset.show; |
| 1062 | + } |
947 | 1063 |
|
948 | 1064 | switch(code) { |
949 | 1065 | case KEYCODE_SHIFT: |
|
1358 | 1474 | } |
1359 | 1475 |
|
1360 | 1476 | /** |
1361 | | - * 顯示候選字或工具列 (修正:確保無編碼時顯示工具列,而不是模式訊息) |
| 1477 | + * 顯示候選字或工具列 |
| 1478 | + * **修正 2:確保浮層開啟時,工具列(表情符號切換/關閉鍵)依然顯示。** |
1362 | 1479 | */ |
1363 | 1480 | function showCandidates() { |
1364 | 1481 | candidateDisplay.innerHTML = ''; |
1365 | 1482 |
|
1366 | | - // 1. 如果浮層開啟,優先處理浮層,清空顯示區 |
| 1483 | + // 1. 如果浮層開啟 (Emoji/Editor) |
1367 | 1484 | if (overlayDiv.dataset.show) { |
1368 | | - candidateDisplay.textContent = ''; |
| 1485 | + // 修正:浮層開啟時,必須顯示工具列,讓用戶可以切換頁面或關閉浮層。 |
| 1486 | + showToolbar(); |
1369 | 1487 | return; |
1370 | 1488 | } |
1371 | 1489 |
|
|
0 commit comments