Skip to content

Commit 119bcff

Browse files
authored
Add files via upload
1 parent 7f918f1 commit 119bcff

1 file changed

Lines changed: 86 additions & 25 deletions

File tree

3code_mobile.html

Lines changed: 86 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@
440440

441441
// --- 簡繁切換狀態 ---
442442
let isSimplifiedMode = false; // 預設為繁體模式
443-
// 將 TS_MAP 改為可變的變數,並非同步載入
443+
// 簡繁轉換表將從 ts.txt 非同步載入
444444
let TS_MAP = null;
445445

446446
/**
@@ -458,7 +458,7 @@
458458
const text = await response.text();
459459

460460
const newMap = {};
461-
// 根據換行符號切割,然後遍歷每一行
461+
// 每一行是一個轉換對:繁體字\t簡體字
462462
const lines = text.trim().split('\n');
463463

464464
for (const line of lines) {
@@ -958,6 +958,7 @@
958958
// **修正**:如果浮層是開啟的,且點擊的是「簡繁切換鍵」(tool 1),則只切換模式並立即返回,不影響浮層狀態
959959
if (isOverlayActive && tool === "1") {
960960
isSimplifiedMode = !isSimplifiedMode;
961+
target.innerHTML = isSimplifiedMode ? "简" : "繁"; // 立即更新按鈕文字
961962
console.log("TS mode switched while overlay active. Overlay preserved.");
962963
// 這裡不需要 showCandidates,因為浮層開啟時,下方的 candidateDisplay 應該保持清空或顯示工具列
963964
// 但我們不希望它觸發重繪 (showCandidates) 導致浮層閃爍,因此直接 return
@@ -1114,18 +1115,20 @@
11141115
}
11151116
break;
11161117
case KEYCODE_RIGHT:
1117-
if (cursorPosition < committedText.length) {
1118+
// 游標位置的限制應是邏輯字符的數量
1119+
if (cursorPosition < Array.from(committedText).length) {
11181120
cursorPosition++;
11191121
updateOutputDisplay();
11201122
}
11211123
break;
11221124
case KEYCODE_UP:
11231125
case KEYCODE_DOWN:
11241126
// 簡化處理:在沒有完整編輯器邏輯時,上下鍵暫時模擬左右移動
1127+
const maxLen = Array.from(committedText).length;
11251128
if (code === KEYCODE_UP && cursorPosition > 0) {
11261129
cursorPosition--;
11271130
updateOutputDisplay();
1128-
} else if (code === KEYCODE_DOWN && cursorPosition < committedText.length) {
1131+
} else if (code === KEYCODE_DOWN && cursorPosition < maxLen) {
11291132
cursorPosition++;
11301133
updateOutputDisplay();
11311134
}
@@ -1135,8 +1138,10 @@
11351138
updateOutputDisplay();
11361139
break;
11371140
case KEYCODE_DEL_F: // Forward Delete
1138-
if (cursorPosition < committedText.length) {
1139-
committedText = committedText.slice(0, cursorPosition) + committedText.slice(cursorPosition + 1);
1141+
const chars = Array.from(committedText);
1142+
if (cursorPosition < chars.length) {
1143+
chars.splice(cursorPosition, 1);
1144+
committedText = chars.join('');
11401145
updateOutputDisplay();
11411146
}
11421147
break;
@@ -1283,12 +1288,14 @@
12831288
compositionHtml = formatComposition(currentCode);
12841289
}
12851290

1286-
const fullTextLength = committedText.length;
1291+
// 關鍵:使用 Array.from 處理 committedText,確保能正確遍歷 Emoji
1292+
const committedChars = Array.from(committedText);
1293+
const fullTextLength = committedChars.length;
12871294

12881295
while (currentPos <= fullTextLength) {
12891296

12901297
if (softWrapPoints.includes(currentPos)) {
1291-
if (currentPos > 0 && committedText[currentPos - 1] !== '\n') {
1298+
if (currentPos > 0 && committedChars[currentPos - 1] !== '\n') {
12921299
outputHtml += NON_BREAK_ANCHOR;
12931300
}
12941301
}
@@ -1299,7 +1306,7 @@
12991306
}
13001307

13011308
if (currentPos < fullTextLength) {
1302-
const char = committedText[currentPos];
1309+
const char = committedChars[currentPos];
13031310
if (char === '\n') {
13041311
outputHtml += '<br>';
13051312
} else {
@@ -1320,24 +1327,46 @@
13201327

13211328
/**
13221329
* 提交文字 (包含簡繁轉換邏輯)
1330+
* [修正:使用 Array.from() 確保 Emoji 正確插入]
13231331
*/
13241332
function commitText(textToCommit) {
13251333
// 在提交前進行簡繁轉換
13261334
const finalCommitText = convertToSimplified(textToCommit);
13271335

1328-
committedText = committedText.slice(0, cursorPosition) + finalCommitText + committedText.slice(cursorPosition);
1329-
cursorPosition += finalCommitText.length;
1336+
// 關鍵修改:使用 Array.from() 將字串轉換為可正確處理代理對的字符陣列
1337+
const committedChars = Array.from(committedText);
1338+
const charsToInsert = Array.from(finalCommitText);
1339+
1340+
// 在游標位置插入新的字符
1341+
committedChars.splice(cursorPosition, 0, ...charsToInsert);
1342+
committedText = committedChars.join(''); // 重組字串
1343+
1344+
// 游標移動的步數是插入字符的數量
1345+
cursorPosition += charsToInsert.length;
1346+
13301347
preferredOffset = -1;
13311348
resetInputState();
13321349
}
13331350

1351+
/**
1352+
* 處理刪除鍵
1353+
* [修正:使用 Array.from() 確保 Emoji 正確刪除]
1354+
*/
13341355
function backspaceOutput() {
13351356
if (!isEnglishMode && !isSymbolMode && currentCode.length > 0) {
13361357
currentCode = currentCode.slice(0, -1);
13371358
updateStateAndDisplay();
13381359
} else if (cursorPosition > 0) {
1339-
committedText = committedText.slice(0, cursorPosition - 1) + committedText.slice(cursorPosition);
1340-
cursorPosition -= 1;
1360+
1361+
// 關鍵修改:使用 Array.from() 將字串轉換為可正確處理 Emoji 的字符陣列
1362+
const committedChars = Array.from(committedText);
1363+
1364+
// 刪除游標前的一個字符 (無論它佔用多少個碼元)
1365+
committedChars.splice(cursorPosition - 1, 1);
1366+
1367+
committedText = committedChars.join(''); // 重組字串
1368+
cursorPosition -= 1; // 游標只移動一步 (因為刪除了一個可視字符)
1369+
13411370
preferredOffset = -1;
13421371
updateOutputDisplay();
13431372
resetInputState();
@@ -1574,6 +1603,10 @@
15741603
}
15751604
function handleSearchKeydown(event) { if (event.key === 'Backspace') { event.stopPropagation(); } }
15761605

1606+
/**
1607+
* 將 DOM 文本節點偏移量 (UTF-16 碼元索引) 映射到邏輯字符索引 (可視字符索引)
1608+
* [修正:使用 Array.from() 處理 Emoji 代理對]
1609+
*/
15771610
function mapDOMOffsetToTextIndex(targetNode, targetOffset) {
15781611
let committedIndex = 0;
15791612
const outputDiv = document.getElementById('text-output');
@@ -1582,6 +1615,7 @@
15821615
outputDiv,
15831616
NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT,
15841617
{ acceptNode: (node) => {
1618+
// 排除游標和正在編輯的文字
15851619
if (node.nodeType === Node.ELEMENT_NODE && (node.classList.contains('cursor') || node.classList.contains('composition-text'))) {
15861620
return NodeFilter.FILTER_SKIP;
15871621
}
@@ -1595,30 +1629,56 @@
15951629
if (node.nodeType === Node.TEXT_NODE) {
15961630
const textContent = node.textContent;
15971631

1632+
// 關鍵:將內容分解為可視字符(Grapheme Clusters)
1633+
const fullChars = Array.from(textContent);
1634+
15981635
if (node === targetNode) {
1599-
let count = 0;
1600-
for (let i = 0; i < targetOffset; i++) {
1601-
const char = textContent[i];
1636+
let codeUnitCount = 0; // 累積的 UTF-16 碼元數
1637+
let charCount = 0; // 累積的邏輯字符數
1638+
1639+
for (const char of fullChars) {
1640+
if (codeUnitCount >= targetOffset) {
1641+
// 點擊發生在該字符的開頭或之前,charCount 即為正確位置。
1642+
break;
1643+
}
1644+
1645+
// 累積碼元數 (Emoji 可能會讓 char.length > 1)
1646+
codeUnitCount += char.length;
1647+
1648+
// 累積邏輯字符數 (排除特殊錨點)
16021649
if (char !== NON_BREAK_ANCHOR && char !== '\u200c') {
1603-
count++;
1650+
charCount++;
1651+
}
1652+
1653+
// 邊界修正:如果點擊恰好落在字元中間,瀏覽器可能會給出字元內部的 offset。
1654+
// 為了保險,如果 targetOffset 在當前字符範圍內,但更接近字符結尾,我們將游標放在字符之後。
1655+
// 這裡我們採用最簡單的:只要 currentCodeUnitIndex 加上該字符長度大於 targetOffset,就停止。
1656+
if (codeUnitCount >= targetOffset) {
1657+
// 如果 targetOffset 落在這個字元中間或結尾,我們通常將游標放在這個字元之後。
1658+
// 由於上面的 charCount 已經累加,這裡直接跳出即可。
1659+
// *若要實現更精確的「四捨五入」游標,此處需複雜邏輯,但為了穩定性,我們信任單向累加。*
1660+
break;
16041661
}
16051662
}
1606-
committedIndex += count;
1663+
1664+
committedIndex += charCount;
16071665
return committedIndex;
16081666
}
16091667

1610-
for (const char of textContent) {
1668+
// 如果不是目標節點,計算完整的字符數
1669+
for (const char of fullChars) {
16111670
if (char !== NON_BREAK_ANCHOR && char !== '\u200c') {
16121671
committedIndex++;
16131672
}
16141673
}
16151674

16161675
} else if (node.nodeName === 'BR') {
1617-
committedIndex++;
1676+
committedIndex++; // 換行符佔一個字符
16181677
}
16191678
}
16201679

1621-
return committedText.length;
1680+
// 如果遍歷結束仍未返回,則表示點擊在文字末尾
1681+
return Array.from(committedText).length;
16221682
}
16231683

16241684
function handleTextTap(event) {
@@ -1657,8 +1717,9 @@
16571717
updateOutputDisplay();
16581718
}
16591719
} else if (newNode === textOutput && newOffset === textOutput.childNodes.length) {
1660-
if (cursorPosition !== committedText.length) {
1661-
cursorPosition = committedText.length;
1720+
// 點擊在文本結尾的空白處
1721+
if (cursorPosition !== Array.from(committedText).length) {
1722+
cursorPosition = Array.from(committedText).length;
16621723
preferredOffset = -1;
16631724
updateOutputDisplay();
16641725
}
@@ -1668,9 +1729,9 @@
16681729

16691730

16701731
// 頁面啟動
1671-
document.addEventListener('DOMContentLoaded', async () => { // **改為 async**
1732+
document.addEventListener('DOMContentLoaded', async () => { // **注意:這裡必須改成 async**
16721733
drawKeyboard();
1673-
await loadTSMap(); // **新增:載入簡繁轉換表**
1734+
await loadTSMap(); // **在這裡等待轉換表載入**
16741735
loadSecondaryCodemap();
16751736

16761737
textOutput.addEventListener('click', handleTextTap);

0 commit comments

Comments
 (0)