Here is the updated `handleEditSubmit` function. All comments have been translated into English, and the logic handles mode persistence correctly.
Since mode switching is strictly disallowed once active, the check `if (!wasSingleWordMode)` will only evaluate to `true` during the very first initialization of the single-word mode session (when `editArea` is not yet open), ensuring a clean slate. On all subsequent sequential inputs within the same mode, it will safely skip clearing the keys, preserving your edit rows just like phrase mode does.
```javascript
function handleEditSubmit() {
let val = document.getElementById('codeInput').value.trim();
if (!val) return;
// Record the single word mode state before execution to check for repeated inputs
let wasSingleWordMode = isSingleWordMode;
// Validation profile for the input string
let isEnglishCode = /^[a-zA-Z]+$/.test(val);
let isZCode = isEnglishCode && val[0].toLowerCase() === 'z';
let isSingleWordInput = (!isEnglishCode && val.length === 1) || (isEnglishCode && !isZCode);
let isPhraseInput = (!isEnglishCode && val.length > 1) || isZCode;
// Check input constraints if the editor interface is already operational
if (document.getElementById('editArea').style.display === 'block') {
if (isSingleWordMode && isPhraseInput) {
alert('Constraint Error: Single Word mode active. Input cannot contain phrases or z-starting codes.');
return;
}
if (!isSingleWordMode && isSingleWordInput) {
alert('Constraint Error: Phrase mode active. Input cannot contain single words or non-z codes.');
return;
}
}
document.getElementById('codeInput').value = '';
selectedEditItem = null;
justMovedItem = null;
pendingAction = null;
updateActionButtonsUI();
// Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
isSingleWordMode = false;
singleWordActiveCode = '';
currentTargetWord = '';
if (isEnglishCode && val[0].toLowerCase() !== 'z') {
isSingleWordMode = true;
singleWordActiveCode = val.toLowerCase().substring(0, 4);
} else if (!isEnglishCode && val.length === 1) {
isSingleWordMode = true;
currentTargetWord = val; // Set the target word to highlight the Chinese char
// Reverse lookup code from charDefCodeMap
for (let [code, chars] of charDefCodeMap) {
if (chars.includes(val)) {
singleWordActiveCode = code;
break;
}
}
}
if (isSingleWordMode) {
// If already in single word mode, do not clear activeEditKeys to retain current edit rows
if (!wasSingleWordMode) {
activeEditKeys.clear(); // Only clear during initial session setup
}
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
// Do not overwrite if an original snapshot state already exists, ensuring Undo mechanism works correctly
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
// Original Phrase Logic
if (isEnglishCode) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
// Check input pattern profile
if (/^[a-zA-Z]+$/.test(val)) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
// CRITICAL FIX: Capture baseline state *before* pushing the new word
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
saveStateForUndo(`Added new word: ${val}`);
lines.push(val);
processContent(JSON.stringify(lines));
}
let code = 'z';
let c1 = ccharfirst.get(val[0]) || '';
if (val.length === 1) {
code += c1;
} else if (val.length === 2) {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharsecond.get(val[1]) || 'x';
code += c1 + c2 + c3;
} else {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharfirst.get(val[2]) || '';
code += c1 + c2 + c3;
}
currentEditCode = code.substring(0, 4);
}
}
if (currentEditCode.length >= 1) activeEditKeys.add('z');
if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);
// Standard initialization fallback block
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
}
// Keep input area visible allowing repeated inputs
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
// Toggle action buttons visibility based on single word vs phrase mode
if (isSingleWordMode) {
document.getElementById('btnMoveEnd').style.display = 'none';
document.getElementById('btnDelete').style.display = 'none';
} else {
document.getElementById('btnMoveEnd').style.display = 'inline-block';
document.getElementById('btnDelete').style.display = 'inline-block';
}
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
renderEditArea();
}
```
```
function handleEditSubmit() {
let val = document.getElementById('codeInput').value.trim();
if (!val) return;
// Record the single word mode state before execution to check for repeated inputs
let wasSingleWordMode = isSingleWordMode;
// Validation profile for the input string
let isEnglishCode = /^[a-zA-Z]+$/.test(val);
let isZCode = isEnglishCode && val[0].toLowerCase() === 'z';
let isSingleWordInput = (!isEnglishCode && val.length === 1) || (isEnglishCode && !isZCode);
let isPhraseInput = (!isEnglishCode && val.length > 1) || isZCode;
// Check input constraints if the editor interface is already operational
if (document.getElementById('editArea').style.display === 'block') {
if (isSingleWordMode && isPhraseInput) {
alert('Constraint Error: Single Word mode active. Input cannot contain phrases or z-starting codes.');
return;
}
if (!isSingleWordMode && isSingleWordInput) {
alert('Constraint Error: Phrase mode active. Input cannot contain single words or non-z codes.');
return;
}
}
document.getElementById('codeInput').value = '';
selectedEditItem = null;
justMovedItem = null;
pendingAction = null;
updateActionButtonsUI();
// Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
isSingleWordMode = false;
singleWordActiveCode = '';
currentTargetWord = '';
if (isEnglishCode && val[0].toLowerCase() !== 'z') {
isSingleWordMode = true;
singleWordActiveCode = val.toLowerCase().substring(0, 4);
} else if (!isEnglishCode && val.length === 1) {
isSingleWordMode = true;
currentTargetWord = val; // Set the target word to highlight the Chinese char
// Reverse lookup code from charDefCodeMap
for (let [code, chars] of charDefCodeMap) {
if (chars.includes(val)) {
singleWordActiveCode = code;
break;
}
}
}
if (isSingleWordMode) {
// If already in single word mode, do not clear activeEditKeys to retain current edit rows
if (!wasSingleWordMode) {
activeEditKeys.clear(); // Only clear during initial session setup
}
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
// Do not overwrite if an original snapshot state already exists, ensuring Undo mechanism works correctly
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
// Original Phrase Logic
if (isEnglishCode) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
// Check input pattern profile
if (/^[a-zA-Z]+$/.test(val)) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
// CRITICAL FIX: Capture baseline state *before* pushing the new word
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
saveStateForUndo(`Added new word: ${val}`);
lines.push(val);
processContent(JSON.stringify(lines));
}
let code = 'z';
let c1 = ccharfirst.get(val[0]) || '';
if (val.length === 1) {
code += c1;
} else if (val.length === 2) {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharsecond.get(val[1]) || 'x';
code += c1 + c2 + c3;
} else {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharfirst.get(val[2]) || '';
code += c1 + c2 + c3;
}
currentEditCode = code.substring(0, 4);
}
}
if (currentEditCode.length >= 1) activeEditKeys.add('z');
if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);
// Standard initialization fallback block
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
}
// Keep input area visible allowing repeated inputs
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
// Toggle action buttons visibility based on single word vs phrase mode
if (isSingleWordMode) {
document.getElementById('btnMoveEnd').style.display = 'none';
document.getElementById('btnDelete').style.display = 'none';
} else {
document.getElementById('btnMoveEnd').style.display = 'inline-block';
document.getElementById('btnDelete').style.display = 'inline-block';
}
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
renderEditArea();
}
// Helper to restore state and refresh UI
function applyState(targetState) {
if (isSingleWordMode) {
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
}
function handleUndo() {
if (currentIndex < 0) return; // Allow undoing down to the initial state
let oldState = getSystemState();
currentIndex--; // Move pointer back
let targetState;
if (currentIndex === -1) {
// Apply the BEFORE state of the first action (Initial State)
targetState = historyStack[0];
} else {
// Apply the AFTER state of the action we stepped back to
targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
}
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function handleRedo() {
if (currentIndex >= historyStack.length - 1) return;
let oldState = getSystemState();
currentIndex++; // Move pointer forward
// Apply the AFTER state of the redone action
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function jumpToLog(index) {
if (index < 0 || index >= historyStack.length || index === currentIndex) return;
let oldState = getSystemState();
currentIndex = index; // Jump directly to the selected state's index
// Apply the AFTER state of the selected action log
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function saveStateForUndo(description = 'Manual Edit') {
// Truncate the history stack if the pointer is not at the end
if (currentIndex < historyStack.length - 1) {
historyStack = historyStack.slice(0, currentIndex + 1);
}
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
// Push the 'before' state
historyStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: description,
afterState: null // Will be populated after the current action completes
});
// Always point to the latest state after saving
currentIndex = historyStack.length - 1;
renderActionLogs();
// Asynchronously capture the state AFTER the synchronous action finishes
let capturedIndex = currentIndex;
setTimeout(() => {
if (historyStack[capturedIndex]) {
let afterLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let afterCharDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
afterCharDefMapCopy.set(k, [...v]);
}
historyStack[capturedIndex].afterState = {
lines: afterLines,
filecontent: filecontent,
charDefCodeMap: afterCharDefMapCopy
};
}
}, 0);
}
```
在單字模式下,undo,redo,點擊log-item,都無效。查找原因並修改代碼。
需要明確給出所要修改的地方以及要替換的代碼,不用給出所有的代碼。所有的注釋都要使用英文。
現在要改成,如果一開始輸入的是一個新詞,則log中會出現一個item,點擊undo時不會回到沒加新詞的狀態,也就是說historyStack至少存在一項,currentIndex最小只能是0而不能是-1。如果一開始輸入的是一個已有的詞,則log中會出現第一個item,它的內容是「begin with searching ...」。點擊undo時回到第一個item就不能再回退。注意只限於一開始時輸入已有的詞會在log中出現,即在editArea不存在時。
```
function handleEditSubmit() {
let val = document.getElementById('codeInput').value.trim();
if (!val) return;
// Record the single word mode state before execution to check for repeated inputs
let wasSingleWordMode = isSingleWordMode;
// Validation profile for the input string
let isEnglishCode = /^[a-zA-Z]+$/.test(val);
let isZCode = isEnglishCode && val[0].toLowerCase() === 'z';
let isSingleWordInput = (!isEnglishCode && val.length === 1) || (isEnglishCode && !isZCode);
let isPhraseInput = (!isEnglishCode && val.length > 1) || isZCode;
// Check input constraints if the editor interface is already operational
if (document.getElementById('editArea').style.display === 'block') {
if (isSingleWordMode && isPhraseInput) {
alert('Constraint Error: Single Word mode active. Input cannot contain phrases or z-starting codes.');
return;
}
if (!isSingleWordMode && isSingleWordInput) {
alert('Constraint Error: Phrase mode active. Input cannot contain single words or non-z codes.');
return;
}
}
document.getElementById('codeInput').value = '';
selectedEditItem = null;
justMovedItem = null;
pendingAction = null;
updateActionButtonsUI();
// Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
isSingleWordMode = false;
singleWordActiveCode = '';
currentTargetWord = '';
if (isEnglishCode && val[0].toLowerCase() !== 'z') {
isSingleWordMode = true;
singleWordActiveCode = val.toLowerCase().substring(0, 4);
} else if (!isEnglishCode && val.length === 1) {
isSingleWordMode = true;
currentTargetWord = val; // Set the target word to highlight the Chinese char
// Reverse lookup code from charDefCodeMap
for (let [code, chars] of charDefCodeMap) {
if (chars.includes(val)) {
singleWordActiveCode = code;
break;
}
}
}
if (isSingleWordMode) {
// If already in single word mode, do not clear activeEditKeys to retain current edit rows
if (!wasSingleWordMode) {
activeEditKeys.clear(); // Only clear during initial session setup
}
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
// Do not overwrite if an original snapshot state already exists, ensuring Undo mechanism works correctly
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
// Original Phrase Logic
if (isEnglishCode) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
// Check input pattern profile
if (/^[a-zA-Z]+$/.test(val)) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
// CRITICAL FIX: Capture baseline state *before* pushing the new word
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
saveStateForUndo(`Added new word: ${val}`);
lines.push(val);
processContent(JSON.stringify(lines));
}
let code = 'z';
let c1 = ccharfirst.get(val[0]) || '';
if (val.length === 1) {
code += c1;
} else if (val.length === 2) {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharsecond.get(val[1]) || 'x';
code += c1 + c2 + c3;
} else {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharfirst.get(val[2]) || '';
code += c1 + c2 + c3;
}
currentEditCode = code.substring(0, 4);
}
}
if (currentEditCode.length >= 1) activeEditKeys.add('z');
if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);
// Standard initialization fallback block
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
}
// Keep input area visible allowing repeated inputs
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
// Toggle action buttons visibility based on single word vs phrase mode
if (isSingleWordMode) {
document.getElementById('btnMoveEnd').style.display = 'none';
document.getElementById('btnDelete').style.display = 'none';
} else {
document.getElementById('btnMoveEnd').style.display = 'inline-block';
document.getElementById('btnDelete').style.display = 'inline-block';
}
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
renderEditArea();
}
// Helper to restore state and refresh UI
function applyState(targetState) {
if (isSingleWordMode) {
// Restore core data state for single word mode
filecontent = targetState.filecontent;
if (targetState.charDefCodeMap) {
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
}
// Re-populate active edit keys configuration
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
// Crucial: Refresh the edit workspace UI to reflect the restored state
renderEditArea();
} else {
// Original Phrase Logic
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
}
function handleUndo() {
if (currentIndex < 0) return; // Allow undoing down to the initial state
let oldState = getSystemState();
currentIndex--; // Move pointer back
let targetState;
if (currentIndex === -1) {
// Apply the BEFORE state of the first action (Initial State)
targetState = historyStack[0];
} else {
// Apply the AFTER state of the action we stepped back to
targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
}
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function handleRedo() {
if (currentIndex >= historyStack.length - 1) return;
let oldState = getSystemState();
currentIndex++; // Move pointer forward
// Apply the AFTER state of the redone action
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function jumpToLog(index) {
if (index < 0 || index >= historyStack.length || index === currentIndex) return;
let oldState = getSystemState();
currentIndex = index; // Jump directly to the selected state's index
// Apply the AFTER state of the selected action log
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function saveStateForUndo(description = 'Manual Edit') {
// Truncate the history stack if the pointer is not at the end
if (currentIndex < historyStack.length - 1) {
historyStack = historyStack.slice(0, currentIndex + 1);
}
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
// Push the 'before' state
historyStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: description,
afterState: null // Will be populated after the current action completes
});
// Always point to the latest state after saving
currentIndex = historyStack.length - 1;
renderActionLogs();
// Asynchronously capture the state AFTER the synchronous action finishes
let capturedIndex = currentIndex;
setTimeout(() => {
if (historyStack[capturedIndex]) {
let afterLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let afterCharDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
afterCharDefMapCopy.set(k, [...v]);
}
historyStack[capturedIndex].afterState = {
lines: afterLines,
filecontent: filecontent,
charDefCodeMap: afterCharDefMapCopy
};
}
}, 0);
}
```
當前的代碼,如果一開始輸入的是一個新詞,則log中會出現一個item,點擊undo時會回到沒加新詞的狀態。如果一開始輸入的是一個已有的詞,則log中不會出現item。
現在要改成,如果一開始輸入的是一個新詞,則log中會出現一個item,點擊undo時不會回到沒加新詞的狀態,也就是說historyStack至少存在一項,currentIndex最小只能是0而不能是-1。如果一開始輸入的是一個已有的詞,則log中會出現第一個item,它的內容是「begin with searching ...」。點擊undo時回到第一個item就不能再回退。注意只限於一開始時輸入已有的詞會在log中出現,即在editArea不存在時。
所有的注釋都要使用英文。
Here is the modified code to meet your requirements.
I have adjusted the logic so that any initial input (when `editArea` is not yet visible) will register as the foundational action in the history log. If it's a new word, it logs as "Added new word: ..."; if it's an existing word, it logs as "begin with searching ...". I also updated the `handleUndo` function to establish `0` as the absolute minimum `currentIndex`, ensuring the user cannot back out of the very first state (the initial new word or the initial search). All comments are provided in English.
```javascript
function handleEditSubmit() {
let val = document.getElementById('codeInput').value.trim();
if (!val) return;
// Check if the editor interface is hidden, indicating this is the initial input
let isInitialInput = document.getElementById('editArea').style.display !== 'block';
// Record the single word mode state before execution to check for repeated inputs
let wasSingleWordMode = isSingleWordMode;
// Validation profile for the input string
let isEnglishCode = /^[a-zA-Z]+$/.test(val);
let isZCode = isEnglishCode && val[0].toLowerCase() === 'z';
let isSingleWordInput = (!isEnglishCode && val.length === 1) || (isEnglishCode && !isZCode);
let isPhraseInput = (!isEnglishCode && val.length > 1) || isZCode;
// Check input constraints if the editor interface is already operational
if (!isInitialInput) {
if (isSingleWordMode && isPhraseInput) {
alert('Constraint Error: Single Word mode active. Input cannot contain phrases or z-starting codes.');
return;
}
if (!isSingleWordMode && isSingleWordInput) {
alert('Constraint Error: Phrase mode active. Input cannot contain single words or non-z codes.');
return;
}
}
// Determine if a new word is about to be added to the dictionary
let willAddNewWord = false;
if (!isEnglishCode && val.length > 1) {
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
willAddNewWord = true;
}
}
// If this is the initial input and it's an existing word/code, log the starting action
if (isInitialInput && !willAddNewWord) {
saveStateForUndo(`begin with searching ${val}`);
}
document.getElementById('codeInput').value = '';
selectedEditItem = null;
justMovedItem = null;
pendingAction = null;
updateActionButtonsUI();
// Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
isSingleWordMode = false;
singleWordActiveCode = '';
currentTargetWord = '';
if (isEnglishCode && val[0].toLowerCase() !== 'z') {
isSingleWordMode = true;
singleWordActiveCode = val.toLowerCase().substring(0, 4);
} else if (!isEnglishCode && val.length === 1) {
isSingleWordMode = true;
currentTargetWord = val; // Set the target word to highlight the Chinese char
// Reverse lookup code from charDefCodeMap
for (let [code, chars] of charDefCodeMap) {
if (chars.includes(val)) {
singleWordActiveCode = code;
break;
}
}
}
if (isSingleWordMode) {
// If already in single word mode, do not clear activeEditKeys to retain current edit rows
if (!wasSingleWordMode) {
activeEditKeys.clear(); // Only clear during initial session setup
}
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
// Do not overwrite if an original snapshot state already exists, ensuring Undo mechanism works correctly
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
// Original Phrase Logic
if (isEnglishCode) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
// Check input pattern profile
if (/^[a-zA-Z]+$/.test(val)) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
// CRITICAL FIX: Capture baseline state *before* pushing the new word
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
saveStateForUndo(`Added new word: ${val}`);
lines.push(val);
processContent(JSON.stringify(lines));
}
let code = 'z';
let c1 = ccharfirst.get(val[0]) || '';
if (val.length === 1) {
code += c1;
} else if (val.length === 2) {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharsecond.get(val[1]) || 'x';
code += c1 + c2 + c3;
} else {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharfirst.get(val[2]) || '';
code += c1 + c2 + c3;
}
currentEditCode = code.substring(0, 4);
}
}
if (currentEditCode.length >= 1) activeEditKeys.add('z');
if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);
// Standard initialization fallback block
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
}
// Keep input area visible allowing repeated inputs
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
// Toggle action buttons visibility based on single word vs phrase mode
if (isSingleWordMode) {
document.getElementById('btnMoveEnd').style.display = 'none';
document.getElementById('btnDelete').style.display = 'none';
} else {
document.getElementById('btnMoveEnd').style.display = 'inline-block';
document.getElementById('btnDelete').style.display = 'inline-block';
}
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
renderEditArea();
}
// Helper to restore state and refresh UI
function applyState(targetState) {
if (isSingleWordMode) {
// Restore core data state for single word mode
filecontent = targetState.filecontent;
if (targetState.charDefCodeMap) {
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
}
// Re-populate active edit keys configuration
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
// Crucial: Refresh the edit workspace UI to reflect the restored state
renderEditArea();
} else {
// Original Phrase Logic
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
}
function handleUndo() {
// Prevent undoing past the initial logged action (index 0).
// The user can no longer revert to the unlogged state (-1).
if (currentIndex <= 0) return;
let oldState = getSystemState();
currentIndex--; // Move pointer back
// Apply the AFTER state of the action we stepped back to
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function handleRedo() {
if (currentIndex >= historyStack.length - 1) return;
let oldState = getSystemState();
currentIndex++; // Move pointer forward
// Apply the AFTER state of the redone action
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function jumpToLog(index) {
if (index < 0 || index >= historyStack.length || index === currentIndex) return;
let oldState = getSystemState();
currentIndex = index; // Jump directly to the selected state's index
// Apply the AFTER state of the selected action log
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function saveStateForUndo(description = 'Manual Edit') {
// Truncate the history stack if the pointer is not at the end
if (currentIndex < historyStack.length - 1) {
historyStack = historyStack.slice(0, currentIndex + 1);
}
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
// Push the 'before' state
historyStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: description,
afterState: null // Will be populated after the current action completes
});
// Always point to the latest state after saving
currentIndex = historyStack.length - 1;
renderActionLogs();
// Asynchronously capture the state AFTER the synchronous action finishes
let capturedIndex = currentIndex;
setTimeout(() => {
if (historyStack[capturedIndex]) {
let afterLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let afterCharDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
afterCharDefMapCopy.set(k, [...v]);
}
historyStack[capturedIndex].afterState = {
lines: afterLines,
filecontent: filecontent,
charDefCodeMap: afterCharDefMapCopy
};
}
}, 0);
}
```
```javascript
function handleEditSubmit() {
let val = document.getElementById('codeInput').value.trim();
if (!val) return;
// Check if the editor interface is hidden, indicating this is the initial input
let isInitialInput = document.getElementById('editArea').style.display !== 'block';
// Record the single word mode state before execution to check for repeated inputs
let wasSingleWordMode = isSingleWordMode;
// Validation profile for the input string
let isEnglishCode = /^[a-zA-Z]+$/.test(val);
let isZCode = isEnglishCode && val[0].toLowerCase() === 'z';
let isSingleWordInput = (!isEnglishCode && val.length === 1) || (isEnglishCode && !isZCode);
let isPhraseInput = (!isEnglishCode && val.length > 1) || isZCode;
// Check input constraints if the editor interface is already operational
if (!isInitialInput) {
if (isSingleWordMode && isPhraseInput) {
alert('Constraint Error: Single Word mode active. Input cannot contain phrases or z-starting codes.');
return;
}
if (!isSingleWordMode && isSingleWordInput) {
alert('Constraint Error: Phrase mode active. Input cannot contain single words or non-z codes.');
return;
}
}
// Determine if a new word is about to be added to the dictionary
let willAddNewWord = false;
if (!isEnglishCode && val.length > 1) {
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
willAddNewWord = true;
}
}
// If this is the initial input and it's an existing word/code, log the starting action
if (isInitialInput && !willAddNewWord) {
saveStateForUndo(`begin with searching ${val}`);
}
document.getElementById('codeInput').value = '';
selectedEditItem = null;
justMovedItem = null;
pendingAction = null;
updateActionButtonsUI();
// Detect Single Word Mode (English without 'z' prefix, or a single Chinese char)
isSingleWordMode = false;
singleWordActiveCode = '';
currentTargetWord = '';
if (isEnglishCode && val[0].toLowerCase() !== 'z') {
isSingleWordMode = true;
singleWordActiveCode = val.toLowerCase().substring(0, 4);
} else if (!isEnglishCode && val.length === 1) {
isSingleWordMode = true;
currentTargetWord = val; // Set the target word to highlight the Chinese char
// Reverse lookup code from charDefCodeMap
for (let [code, chars] of charDefCodeMap) {
if (chars.includes(val)) {
singleWordActiveCode = code;
break;
}
}
}
if (isSingleWordMode) {
// If already in single word mode, do not clear activeEditKeys to retain current edit rows
if (!wasSingleWordMode) {
activeEditKeys.clear(); // Only clear during initial session setup
}
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
// Do not overwrite if an original snapshot state already exists, ensuring Undo mechanism works correctly
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
// Original Phrase Logic
if (isEnglishCode) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
// Check input pattern profile
if (/^[a-zA-Z]+$/.test(val)) {
if (val[0].toLowerCase() !== 'z') val = 'z' + val;
val = val.substring(0, 4).toLowerCase();
currentEditCode = val;
currentTargetWord = '';
} else {
currentTargetWord = val;
let lines = mcc0string.split('\n').filter(l => l.trim() !== '');
if (!lines.includes(val)) {
// CRITICAL FIX: Capture baseline state *before* pushing the new word
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
saveStateForUndo(`Added new word: ${val}`);
lines.push(val);
processContent(JSON.stringify(lines));
}
let code = 'z';
let c1 = ccharfirst.get(val[0]) || '';
if (val.length === 1) {
code += c1;
} else if (val.length === 2) {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharsecond.get(val[1]) || 'x';
code += c1 + c2 + c3;
} else {
let c2 = ccharfirst.get(val[1]) || '';
let c3 = ccharfirst.get(val[2]) || '';
code += c1 + c2 + c3;
}
currentEditCode = code.substring(0, 4);
}
}
if (currentEditCode.length >= 1) activeEditKeys.add('z');
if (currentEditCode.length >= 2) activeEditKeys.add(currentEditCode.substring(0, 2));
if (currentEditCode.length >= 3) activeEditKeys.add(currentEditCode.substring(0, 3));
if (currentEditCode.length >= 4) activeEditKeys.add(currentEditCode);
// Standard initialization fallback block
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
}
// Keep input area visible allowing repeated inputs
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
// Toggle action buttons visibility based on single word vs phrase mode
if (isSingleWordMode) {
document.getElementById('btnMoveEnd').style.display = 'none';
document.getElementById('btnDelete').style.display = 'none';
} else {
document.getElementById('btnMoveEnd').style.display = 'inline-block';
document.getElementById('btnDelete').style.display = 'inline-block';
}
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'block';
renderEditArea();
}
// Helper to restore state and refresh UI
function applyState(targetState) {
if (isSingleWordMode) {
// Restore core data state for single word mode
filecontent = targetState.filecontent;
if (targetState.charDefCodeMap) {
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
}
// Re-populate active edit keys configuration
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
// Crucial: Refresh the edit workspace UI to reflect the restored state
renderEditArea();
} else {
// Original Phrase Logic
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
}
function handleUndo() {
// Prevent undoing past the initial logged action (index 0).
// The user can no longer revert to the unlogged state (-1).
if (currentIndex <= 0) return;
let oldState = getSystemState();
currentIndex--; // Move pointer back
// Apply the AFTER state of the action we stepped back to
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function handleRedo() {
if (currentIndex >= historyStack.length - 1) return;
let oldState = getSystemState();
currentIndex++; // Move pointer forward
// Apply the AFTER state of the redone action
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function jumpToLog(index) {
if (index < 0 || index >= historyStack.length || index === currentIndex) return;
let oldState = getSystemState();
currentIndex = index; // Jump directly to the selected state's index
// Apply the AFTER state of the selected action log
let targetState = historyStack[currentIndex].afterState || historyStack[currentIndex];
applyState(targetState);
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function saveStateForUndo(description = 'Manual Edit') {
// Truncate the history stack if the pointer is not at the end
if (currentIndex < historyStack.length - 1) {
historyStack = historyStack.slice(0, currentIndex + 1);
}
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
// Push the 'before' state
historyStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: description,
afterState: null // Will be populated after the current action completes
});
// Always point to the latest state after saving
currentIndex = historyStack.length - 1;
renderActionLogs();
// Asynchronously capture the state AFTER the synchronous action finishes
let capturedIndex = currentIndex;
setTimeout(() => {
if (historyStack[capturedIndex]) {
let afterLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let afterCharDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
afterCharDefMapCopy.set(k, [...v]);
}
historyStack[capturedIndex].afterState = {
lines: afterLines,
filecontent: filecontent,
charDefCodeMap: afterCharDefMapCopy
};
}
}, 0);
}
function handleCancelEdit() {
// Revert to initial state before any edits in this session were made
if (historyStack.length > 0) {
applyState(historyStack[0]);
}
// Close the edit interface
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'none';
document.getElementById('codeInput').value = '';
// Reset tracking variables without updating originalSnapshotState
currentEditCode = '';
currentTargetWord = '';
selectedEditItem = null;
justMovedItem = null;
activeEditKeys.clear();
changedWords.clear();
pendingAction = null;
historyStack = [];
currentIndex = -1;
renderActionLogs();
updateActionButtonsUI();
}
function handleEditCloseOnly() {
document.getElementById('inputArea').style.display = 'block';
document.getElementById('editArea').style.display = 'none';
document.getElementById('codeInput').value = '';
originalSnapshotState = getSystemState();
currentEditCode = '';
currentTargetWord = '';
selectedEditItem = null;
justMovedItem = null;
activeEditKeys.clear();
changedWords.clear();
pendingAction = null;
historyStack = [];
currentIndex = -1;
renderActionLogs(); // Visually empty out the log UI on close
updateActionButtonsUI();
}
```
當我在單字模式中作了改動,點擊cancel或finish之後,如果再次進入詞語模式,對比欄全部變為空,這是不對的,程式應在上一次cancal或finish後,清空對比欄的數據,不能影響到下一次的進程。
Here are the specific modifications you need to make to replace the "Clear" button with the interactive `<h1>` title.
### 1. HTML Modification
Locate the `<button>` tag at the very beginning of your `<body>` and replace it with the new `<h1>` tag.
**Find this code:**
```html
<button id="clearButton" onclick="resetPage()">清空</button>
```
**Replace it with:**
```html
<h1 id="mainTitle" onclick="handleTitleClick()">哈哈倉頡碼表編輯統計工具</h1>
```
---
### 2. CSS Modification
Locate the styling for the old clear button in your `<style>` block and replace it with the styling for the new title.
**Find this code:**
```css
button#clearButton {
display: block;
width: 100%;
padding: 10px;
background-color: #4caf50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
background-color: #f44336; /* 紅色清空按鈕 */
}
```
**Replace it with:**
```css
/* Styles for the new H1 title */
h1#mainTitle {
text-align: center;
color: #333;
margin-top: 10px;
margin-bottom: 20px;
transition: color 0.2s;
user-select: none;
}
/* Active state when a file is loaded, making it act as a clear button */
h1#mainTitle.active-clear {
cursor: pointer;
color: #f44336; /* Red color to indicate clear/reset action */
}
h1#mainTitle.active-clear:hover {
color: #da190b;
text-decoration: underline;
}
```
---
### 3. JavaScript Modifications
You will need to add a new function to handle the title click and update two existing functions to toggle the clickable state of the title.
**A. Add the new click handler function**
Add this new function anywhere inside your `<script>` tag (for example, right above the `resetPage` function):
```javascript
// Trigger reset only if a file is currently loaded
function handleTitleClick() {
if (filecontent !== '') {
resetPage();
}
}
```
**B. Update `resetPage()` to remove the active state**
Locate the `resetPage` function and add the lines to reset the title's styling when the data is cleared.
**Find this code inside `resetPage(readfile = true)`:**
```javascript
if (readfile) {
filecontent = '';
originalFileName = ''; // Reset the stored filename
```
**Add the title reset logic right below it:**
```javascript
if (readfile) {
filecontent = '';
originalFileName = ''; // Reset the stored filename
// Remove active clear state from the title
document.getElementById('mainTitle').classList.remove('active-clear');
document.getElementById('mainTitle').title = '';
```
**C. Update `handleFile()` to activate the clear function**
Locate the `handleFile` function and add the lines to apply the clickable CSS class and hover tooltip when a file is successfully selected.
**Find this code inside `handleFile(event)`:**
```javascript
infoDiv.textContent = `File Name: ${file.name} (${file.size} bytes)`;
infoDiv.style.display = 'block';
```
**Add the title activation logic right below it:**
```javascript
infoDiv.textContent = `File Name: ${file.name} (${file.size} bytes)`;
infoDiv.style.display = 'block';
// Make the title act as the clear button now that a file is loaded
document.getElementById('mainTitle').classList.add('active-clear');
document.getElementById('mainTitle').title = 'Click to clear data and reset';
```