``` function handleUndo() {
if (undoStack.length === 0) return;
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
let lastActionDesc = undoStack[undoStack.length - 1].desc;
redoStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: `Undo: ${lastActionDesc}`
});
let prevState = undoStack.pop();
let oldState = getSystemState();
if (isSingleWordMode) {
// --- SINGLE WORD MODE UNDO BRANCH ---
charDefCodeMap = new Map();
for (let [k, v] of prevState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
} else {
// --- EXISTING PHRASE MODE BRANCH ---
filecontent = prevState.filecontent;
processContent(JSON.stringify(prevState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function handleRedo() {
if (redoStack.length === 0) return;
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
let nextState = redoStack.pop();
undoStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: nextState.desc
});
let oldState = getSystemState();
if (isSingleWordMode) {
// --- SINGLE WORD MODE REDO BRANCH ---
charDefCodeMap = new Map();
for (let [k, v] of nextState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
} else {
// --- EXISTING PHRASE MODE BRANCH ---
filecontent = nextState.filecontent;
processContent(JSON.stringify(nextState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function jumpToLog(index) {
if (index < 0 || index >= undoStack.length) return;
// Current operational baseline state capture
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
// Target state represents the snapshot *before* the clicked action was performed.
// To arrive at the state *after* the action was performed, we need to apply the state
// that immediately follows it in the timeline, or stay on current state if it's the top.
let targetState;
if (index === undoStack.length - 1) {
// Already at the latest state executed, no content update needed
return;
} else {
// The state immediately following index represents the system condition AFTER index action completed
targetState = undoStack[index + 1];
}
// Transfer chronological items into the redo stack from the trailing end down to the split boundary
for (let i = undoStack.length - 1; i > index; i--) {
redoStack.push({
lines: i === undoStack.length - 1 ? currentLines : undoStack[i + 1].lines,
filecontent: i === undoStack.length - 1 ? filecontent : undoStack[i + 1].filecontent,
charDefCodeMap: i === undoStack.length - 1 ? charDefMapCopy : undoStack[i + 1].charDefCodeMap,
desc: undoStack[i].desc
});
}
// Keep the stack history slice up to and including the targeted executed action index
undoStack = undoStack.slice(0, index + 1);
let oldState = getSystemState();
if (isSingleWordMode) {
// --- SINGLE WORD MODE STATE RESTORATION ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
if (typeof runDomumorText === 'function') {
runDomumorText();
}
} else {
// --- PHRASE MODE STATE RESTORATION ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
// Draws the visual list of history operations
function renderActionLogs() {
const container = document.getElementById('actionLogContainer');
const list = document.getElementById('actionLogsList');
if (!container || !list) return;
if (undoStack.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
list.innerHTML = '';
undoStack.forEach((log, index) => {
let logDiv = document.createElement('div');
logDiv.className = 'log-item';
logDiv.textContent = log.desc;
logDiv.onclick = () => jumpToLog(index);
list.appendChild(logDiv);
});
}
```
分析以上四個函式,現在
let undoStack = [];
let redoStack = [];
已經替換為:
let historyStack = [];
let currentIndex = -1;
現在要改為,當前操作會在log的item高亮,undo和redo都只在log切換高亮的item。點擊item也是切換高亮。
需要明確給出所要修改的地方和替換的代碼,不用給出全部代碼。所有的注釋都要使用英文。
以下是針對使用單一 `historyStack` 和 `currentIndex` 的架構所需要替換的代碼部分。這種設計大大簡化了歷史記錄的切換,因為我們不再需要在兩個陣列之間搬運資料,只需移動指標並讀取對應的狀態即可。
請將您原有的四個函式替換為以下代碼。所有的註解已更改為英文。
### 1. `handleUndo`
**修改重點:** 移除操作 `undoStack` 和 `redoStack` 的推入與彈出邏輯,改為檢查並遞減 `currentIndex`,然後直接套用該索引的狀態。
```javascript
function handleUndo() {
// Check if we are already at the oldest state
if (currentIndex <= 0) return;
let oldState = getSystemState();
// Move the pointer back by one
currentIndex--;
let targetState = historyStack[currentIndex];
if (isSingleWordMode) {
// --- SINGLE WORD MODE UNDO BRANCH ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
} else {
// --- EXISTING PHRASE MODE BRANCH ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
```
### 2. `handleRedo`
**修改重點:** 同樣移除堆疊的搬運邏輯,改為檢查 `currentIndex` 是否到達頂端,遞增 `currentIndex` 並套用狀態。
```javascript
function handleRedo() {
// Check if we are already at the newest state
if (currentIndex >= historyStack.length - 1) return;
let oldState = getSystemState();
// Move the pointer forward by one
currentIndex++;
let targetState = historyStack[currentIndex];
if (isSingleWordMode) {
// --- SINGLE WORD MODE REDO BRANCH ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
} else {
// --- EXISTING PHRASE MODE BRANCH ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
```
### 3. `jumpToLog`
**修改重點:** 移除複雜的切片 (slice) 與搬移迴圈。現在只需要直接將 `currentIndex` 設為點擊的 `index`,並套用對應狀態。
```javascript
function jumpToLog(index) {
// Guard clause: prevent out of bounds or jumping to the current active state
if (index < 0 || index >= historyStack.length || index === currentIndex) return;
let oldState = getSystemState();
// Set the pointer to the clicked index
currentIndex = index;
let targetState = historyStack[currentIndex];
if (isSingleWordMode) {
// --- SINGLE WORD MODE STATE RESTORATION ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
if (typeof runDomumorText === 'function') {
runDomumorText();
}
} else {
// --- PHRASE MODE STATE RESTORATION ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
```
### 4. `renderActionLogs`
**修改重點:** 改為遍歷 `historyStack`。在建立 DOM 元素時,比對當前迴圈的 `index` 是否等於 `currentIndex`,若是,則加上高亮的 CSS class。
```javascript
// Draws the visual list of history operations
function renderActionLogs() {
const container = document.getElementById('actionLogContainer');
const list = document.getElementById('actionLogsList');
if (!container || !list) return;
if (historyStack.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
list.innerHTML = '';
historyStack.forEach((log, index) => {
let logDiv = document.createElement('div');
logDiv.className = 'log-item';
// Highlight the current state item
if (index === currentIndex) {
logDiv.classList.add('active-log'); // Make sure to define this in your CSS
}
logDiv.textContent = log.desc;
logDiv.onclick = () => jumpToLog(index);
list.appendChild(logDiv);
});
}
```
**額外提醒:** 別忘了在您的 CSS 檔案中加入 `.active-log` 類別,來定義高亮的樣式(例如改變背景顏色或文字顏色):
```css
.active-log {
background-color: #e0f7fa; /* Example highlight color */
font-weight: bold;
}
```
```
function handleUndo() {
// Check if we are already at the oldest state
if (currentIndex <= 0) return;
let oldState = getSystemState();
// Move the pointer back by one
currentIndex--;
let targetState = historyStack[currentIndex];
if (isSingleWordMode) {
// --- SINGLE WORD MODE UNDO BRANCH ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
} else {
// --- EXISTING PHRASE MODE BRANCH ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function handleRedo() {
// Check if we are already at the newest state
if (currentIndex >= historyStack.length - 1) return;
let oldState = getSystemState();
// Move the pointer forward by one
currentIndex++;
let targetState = historyStack[currentIndex];
if (isSingleWordMode) {
// --- SINGLE WORD MODE REDO BRANCH ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
} else {
// --- EXISTING PHRASE MODE BRANCH ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
function jumpToLog(index) {
// Guard clause: prevent out of bounds or jumping to the current active state
if (index < 0 || index >= historyStack.length || index === currentIndex) return;
let oldState = getSystemState();
// Set the pointer to the clicked index
currentIndex = index;
let targetState = historyStack[currentIndex];
if (isSingleWordMode) {
// --- SINGLE WORD MODE STATE RESTORATION ---
charDefCodeMap = new Map();
for (let [k, v] of targetState.charDefCodeMap.entries()) {
charDefCodeMap.set(k, [...v]);
}
rebuildSingleWordFileContent();
processContent();
if (typeof runDomumorText === 'function') {
runDomumorText();
}
} else {
// --- PHRASE MODE STATE RESTORATION ---
filecontent = targetState.filecontent;
processContent(JSON.stringify(targetState.lines));
}
detectAndRenderChanges(oldState, getSystemState());
justMovedItem = null;
renderActionLogs();
}
// Draws the visual list of history operations
function renderActionLogs() {
const container = document.getElementById('actionLogContainer');
const list = document.getElementById('actionLogsList');
if (!container || !list) return;
if (historyStack.length === 0) {
container.style.display = 'none';
return;
}
container.style.display = 'block';
list.innerHTML = '';
console.log('currentIndex', currentIndex);
historyStack.forEach((log, index) => {
let logDiv = document.createElement('div');
logDiv.className = 'log-item';
// Highlight the current state item
if (index === currentIndex) {
logDiv.classList.add('active-log'); // Make sure to define this in your CSS
}
logDiv.textContent = log.desc;
logDiv.onclick = () => jumpToLog(index);
list.appendChild(logDiv);
});
}
function saveStateForUndo(description = 'Manual Edit') {
let currentLines = mcc0string.split('\n').filter(l => l.trim() !== '');
let charDefMapCopy = new Map();
for (let [k, v] of charDefCodeMap.entries()) {
charDefMapCopy.set(k, [...v]);
}
historyStack.push({
lines: currentLines,
filecontent: filecontent,
charDefCodeMap: charDefMapCopy,
desc: description
});
currentIndex = historyStack.length - 1;
renderActionLogs(); // Update visual logs
}
```
改進handleUndo(),handleRedo(),jumpToLog(),renderActionLogs(),saveStateForUndo()函式:
當點擊某個log-item時,或者當某個log-item高亮時,應是走到當前log已執行的狀態。
當從中間的某個log-item發生新的操作時,應在historyStack中間截斷,之後再在後面增加新的操作。
需要明確給出所要修改的地方和替換的代碼,不用給出全部代碼。所有的注釋都要使用英文。
為了讓單字模式(Single Word mode)在連續輸入時也能像詞語模式一樣累積顯示(不清除先前的欄位),並且修復因此受影響的「更新比對(diff highlighting)」與復原機制基準,我們只需要修改 `handleEditSubmit` 函數中關於單字模式的初始化邏輯。
原先的代碼會在每次輸入時呼叫 `activeEditKeys.clear()` 清除先前的顯示列,並且無條件覆寫 `originalSnapshotState`(這會破壞「更新比對」的基準,導致高亮失效)。將其修改為只在第一次輸入時獲取狀態即可。
請在 JavaScript 區段中找到 `handleEditSubmit` 函數,並替換以下的代碼區塊:
**尋找以下代碼:**
```javascript
if (isSingleWordMode) {
activeEditKeys.clear(); // Only show sequences for this code
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
originalSnapshotState = getSystemState();
} else {
```
**替換為:**
```javascript
if (isSingleWordMode) {
// Do not clear activeEditKeys so previously opened columns remain visible for continuous inputs
for (let i = 1; i <= singleWordActiveCode.length; i++) {
activeEditKeys.add(singleWordActiveCode.substring(0, i));
}
// Only capture the baseline state if it hasn't been captured yet in this edit session.
// This preserves the initial baseline for "Cancel" and ensures diff highlighting works correctly.
if (!originalSnapshotState || originalSnapshotState.size === 0) {
originalSnapshotState = getSystemState();
}
} else {
```
透過移除 `activeEditKeys.clear()`,先前的字根欄位將會被保留並與新輸入的字根欄位並存;而透過加上 `!originalSnapshotState || originalSnapshotState.size === 0` 條件,能確保我們比對的基準點永遠是點擊 Finish/Cancel 前的最初狀態,這樣你在多次查詢與移動字元後,「save to undostack」與「更新比對」的結果就都能正確運作了。