Here are the required code modifications to implement all the requested features. Replace the respective blocks in your <script> section.
Add these declarations at the top of the <script> block, just below let isExportTextMode = false;.
JavaScript
let isEditMode = false;
let editUndoStack = [];
let editRedoStack = [];
let selectedPalettePiece = null;
let isTile0Selected = false;
let editMiniBoardState = 0;
let preEditFEN = '';
let editPickedPieceId = null;
// AutoPlay & Range states
let playIntervalId = null;
let playSpeed = 1.8;
let isAutoPlaying = false;
let isRangeMode = false;
let rangeStart = null;
let rangeEnd = null;
let rangeClicks = 0;
function disableUIForPlay(disabled) {
const panels = ['.ejceestool', '.ejceesstep', '.ejceessvg'];
panels.forEach(sel => {
const el = document.querySelector(sel);
if (el) el.style.pointerEvents = disabled ? 'none' : '';
});
}
function getStepElement(index) {
if (index === 0) return document.getElementById('record-start');
return document.getElementById(`step-record-${index}`);
}
function applyRangeBorders() {
clearRangeBorders();
if (rangeStart !== null) {
const el = getStepElement(rangeStart);
if (el) {
el.style.borderLeft = '2px solid lightgreen';
el.style.borderTop = '2px solid lightgreen';
}
}
if (rangeEnd !== null) {
const el = getStepElement(rangeEnd);
if (el) {
el.style.borderRight = '2px solid lightcoral';
el.style.borderBottom = '2px solid lightcoral';
}
}
}
function clearRangeBorders() {
document.querySelectorAll('.ejceesrcdstep, .ejceesrcdstart').forEach(el => {
el.style.borderLeft = '';
el.style.borderTop = '';
el.style.borderRight = '';
el.style.borderBottom = '';
});
}
function startAutoPlay() {
isAutoPlaying = true;
renderNoteUI();
disableUIForPlay(true);
if (isRangeMode && rangeStart !== null) {
if (currentStepIndex < rangeStart || currentStepIndex >= (rangeEnd !== null ? rangeEnd : rangeStart)) {
jumpToStep(rangeStart);
}
}
playIntervalId = setInterval(() => {
let targetEnd = (isRangeMode && rangeEnd !== null) ? rangeEnd : stepSlider.max;
if (currentStepIndex >= targetEnd) {
stopAutoPlay();
return;
}
jumpToStep(currentStepIndex + 1);
if (currentStepIndex >= targetEnd) {
stopAutoPlay();
}
}, playSpeed * 1000);
}
function stopAutoPlay() {
isAutoPlaying = false;
if (playIntervalId) {
clearInterval(playIntervalId);
playIntervalId = null;
}
disableUIForPlay(false);
renderNoteUI();
if (isRangeMode) {
applyRangeBorders();
}
}
In the getMoveNotation function, locate if (colPieces.length > 1) { and replace the inner block with:
JavaScript
if (colPieces.length > 1) {
if (['A', 'a', 'B', 'b'].includes(char)) {
prefix = name;
location = startCol;
} else {
// Normalize order: Front-to-Back
let sortedInCol = [...colPieces];
if (isRed) {
sortedInCol.sort((a, b) => a.y - b.y); // For Red, smaller Y is "Front"
} else {
sortedInCol.sort((a, b) => b.y - a.y); // For Black, larger Y is "Front"
}
const pIdx = sortedInCol.findIndex(p => p.id === pieceId);
// Determine Prefix (Front/Middle/Back or digits)
if (colPieces.length === 2) {
prefix = (pIdx === 0) ? '+' : '-';
} else if (colPieces.length === 3) {
prefix = (pIdx === 0) ? '+' : (pIdx === 1 ? '=' : '-');
} else {
prefix = (pIdx === 0) ? '+' : (pIdx === colPieces.length - 1 ? '-' : pIdx + 1);
}
// --- KEY FIX: Check for ambiguity across different columns ---
if (isPawn && multiPawnCols.length > 1 && prefix !== '=') {
location = startCol;
} else {
location = name;
}
}
} else {
prefix = name;
location = startCol;
}
In the deriveCoordsFromMove function, replace // Case 1: Standard Notation down to its if(candidate) check with:
JavaScript
if (/[a-zA-Z]/.test(char1) && /[0-9]/.test(char2)) {
// Case 1: Standard Notation (e.g., "C2=5", "P3+1", "n8+7", "A4+5")
pieceType = isRed ? char1.toUpperCase() : char1.toLowerCase();
const startCol = parseInt(char2, 10);
startX = isRed ? (9 - startCol) : (startCol - 1);
const candidates = myPieces.filter(p => p.char === pieceType && p.x === startX);
if (candidates.length === 1) {
startY = candidates[0].y;
} else if (candidates.length > 1) {
// Using movable boundaries to disambiguate A/a/B/b
const dir = isRed ? (action === '+' ? -1 : 1) : (action === '+' ? 1 : -1);
const dy = (pieceType.toLowerCase() === 'b') ? 2 : 1;
for (const cand of candidates) {
let testEndY = cand.y + dir * dy;
if (pieceType === 'A') {
if (testEndY >= 7 && testEndY <= 9) { startY = cand.y; break; }
} else if (pieceType === 'a') {
if (testEndY >= 0 && testEndY <= 2) { startY = cand.y; break; }
} else if (pieceType === 'B') {
if (testEndY >= 5 && testEndY <= 9) { startY = cand.y; break; }
} else if (pieceType === 'b') {
if (testEndY >= 0 && testEndY <= 4) { startY = cand.y; break; }
}
}
}
}
In the jumpToStep function, add the range interception logic at the very beginning:
JavaScript
function jumpToStep(index) {
if (isEditingComment) return;
if (isRangeMode && !isAutoPlaying) {
rangeClicks++;
if (rangeClicks === 1 || rangeClicks === 3) {
if (rangeClicks === 3) rangeClicks = 1;
rangeStart = index;
rangeEnd = null;
} else if (rangeClicks === 2) {
if (index < rangeStart) {
rangeEnd = rangeStart;
rangeStart = index;
} else {
rangeEnd = index;
}
}
applyRangeBorders();
return; // Intercept jump
}
const totalDepth = getPathDepth(historyFEN, currentBranch);
// ... rest of the original jumpToStep code ...
Replace the entire renderNoteUI() function with this:
JavaScript
function renderNoteUI() {
const commentDiv = document.querySelector('.ejceescomment');
const btnDiv = document.querySelector('.ejceestextbtn');
const node = getNodeAtStep(currentStepIndex);
if (!node) return;
if (isEditingComment) {
commentDiv.innerHTML = `<textarea class="ejceescomment-edit" placeholder="Enter comments here...">${node.c || ''}</textarea>`;
btnDiv.innerHTML = `
<div class="ejceestextbtninner">
<div class="note-btn btn-confirm" title="Confirm">
<svg viewBox="0 0 24 24"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="note-btn btn-cancel" title="Cancel">
<svg viewBox="0 0 24 24"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
</div>
`;
const textarea = document.querySelector('.ejceescomment-edit');
textarea.focus();
document.querySelector('.btn-confirm').addEventListener('click', () => {
node.c = textarea.value;
isEditingComment = false;
renderNoteUI();
renderRecordUI();
saveStateToUndo();
});
document.querySelector('.btn-cancel').addEventListener('click', () => {
isEditingComment = false;
renderNoteUI();
});
} else {
commentDiv.innerHTML = '';
commentDiv.textContent = node.c || '';
btnDiv.innerHTML = `
<div class="ejceestextbtninner">
<div class="note-btn" id="btn-del-move" title="Delete Move">
<svg viewBox="0 0 24 24"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>
</div>
<div class="note-btn" id="btn-range" title="Range Selection" style="${isRangeMode ? 'background-color:#4CAF50;' : ''}">
<svg viewBox="0 0 24 24"><path fill="${isRangeMode ? '#fff' : '#ccc'}" d="M7 6h4v2H9v8h2v2H7V6zm10 0h-4v2h2v8h-2v2h4V6z"/></svg>
</div>
<div class="note-btn" id="btn-autoplay" title="Auto Play">
${isAutoPlaying
? '<svg viewBox="0 0 24 24"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>'
: '<svg viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>'
}
</div>
<div class="note-btn" id="btn-speed" title="Move Speed" style="width: auto; padding: 0 4px; font-weight: bold;">
${playSpeed}s
</div>
</div>
`;
document.getElementById('btn-autoplay').addEventListener('click', () => {
if (isAutoPlaying) {
stopAutoPlay();
} else {
startAutoPlay();
}
});
document.getElementById('btn-del-move').addEventListener('click', deleteCurrentMoveAndAfter);
document.getElementById('btn-range').addEventListener('click', () => {
isRangeMode = !isRangeMode;
if (!isRangeMode) {
rangeStart = null;
rangeEnd = null;
rangeClicks = 0;
clearRangeBorders();
} else {
rangeStart = null;
rangeEnd = null;
rangeClicks = 0;
clearRangeBorders();
}
renderNoteUI();
});
document.getElementById('btn-speed').addEventListener('click', () => {
if (isAutoPlaying) return;
btnDiv.innerHTML = `
<div class="ejceestextbtninner" style="justify-content: flex-start; gap: 4px;">
<input type="text" id="speed-input" style="width: 36px; font-size: 12px; background: #222; color: #fff; border: 1px solid #555; text-align: center; outline: none;" value="${playSpeed}" maxlength="3">
<div class="note-btn speed-opt" data-val="1">1</div>
<div class="note-btn speed-opt" data-val="2">2</div>
<div class="note-btn speed-opt" data-val="3">3</div>
<div class="note-btn btn-confirm" id="speed-confirm" title="Confirm">
<svg viewBox="0 0 24 24"><path fill="#4CAF50" d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>
</div>
<div class="note-btn btn-cancel" id="speed-cancel" title="Cancel">
<svg viewBox="0 0 24 24"><path fill="#F44336" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
</div>
</div>
`;
document.querySelectorAll('.speed-opt').forEach(btn => {
btn.addEventListener('click', (e) => {
playSpeed = parseFloat(e.target.getAttribute('data-val'));
renderNoteUI();
});
});
document.getElementById('speed-confirm').addEventListener('click', () => {
let val = parseFloat(document.getElementById('speed-input').value);
if (isNaN(val) || val < 0.2 || val > 9) {
val = 1.8;
}
playSpeed = parseFloat(val.toFixed(1));
renderNoteUI();
});
document.getElementById('speed-cancel').addEventListener('click', () => {
renderNoteUI();
});
});
}
updateToolHighlights();
}
Replace the deleteCurrentMoveAndAfter() function:
JavaScript
function deleteCurrentMoveAndAfter() {
if (isRangeMode && rangeStart !== null) {
if (rangeStart === 0) return;
const parentNode = getNodeAtStep(rangeStart - 1);
if (!parentNode) return;
parentNode.v = [];
currentStepIndex = Math.max(0, rangeStart - 1);
stepSlider.value = currentStepIndex;
let forkIndex = -1;
let tempNode = historyFEN;
for (let j = 0; j < currentStepIndex; j++) {
if (tempNode.v && tempNode.v.length > 1) forkIndex++;
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkIndex] || 0) : 0;
tempNode = tempNode.v[choice];
}
forkIndex++;
currentBranch = currentBranch.slice(0, forkIndex);
isRangeMode = false;
rangeStart = null;
rangeEnd = null;
rangeClicks = 0;
clearRangeBorders();
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
return;
}
if (currentStepIndex === 0) return;
const parentNode = getNodeAtStep(currentStepIndex - 1);
if (!parentNode) return;
parentNode.v = [];
currentStepIndex = Math.max(0, currentStepIndex - 1);
stepSlider.value = currentStepIndex;
let forkIndex = -1;
let tempNode = historyFEN;
for (let j = 0; j < currentStepIndex; j++) {
if (tempNode.v && tempNode.v.length > 1) forkIndex++;
const choice = (tempNode.v && tempNode.v.length > 1) ? (currentBranch[forkIndex] || 0) : 0;
tempNode = tempNode.v[choice];
}
forkIndex++;
currentBranch = currentBranch.slice(0, forkIndex);
renderRecordUI();
renderNoteUI();
jumpToStep(currentStepIndex);
saveStateToUndo();
}
In bindEditUIEvents(), replace the char === 'tile0' condition block with the following logic to toggle the shape on click:
JavaScript
document.querySelectorAll('.edit-comment-item').forEach(item => {
item.addEventListener('click', () => {
const char = item.getAttribute('data-char');
if (char === 'tile0' || char === 'tile1') {
const newChar = char === 'tile0' ? 'tile1' : 'tile0';
item.setAttribute('data-char', newChar);
const useEl = item.querySelector('use');
if (useEl) {
useEl.setAttribute('href', `#${newChar}`);
useEl.setAttributeNS('http://www.w3.org/1999/xlink', 'href', `#${newChar}`);
}
} else if (char === 'K' || char === 'k') {
currentTurn = (char === 'K') ? 'w' : 'b';
saveEditState();
}
updateEditUI();
});
});
Replace the slider, minus, plus, and start event bindings with these implementations:
JavaScript
stepSlider.addEventListener('input', (e) => {
if (isEditingComment || isExportTextMode || isEditMode || isRangeMode) {
e.preventDefault();
e.target.value = currentStepIndex;
return;
}
jumpToStep(parseInt(e.target.value, 10));
});
document.querySelector('.ejceesstepminus').addEventListener('click', () => {
if (isEditingComment || isExportTextMode || isEditMode || isRangeMode) return;
jumpToStep(currentStepIndex - 1);
});
document.querySelector('.ejceesstepplus').addEventListener('click', () => {
if (isEditingComment || isExportTextMode || isEditMode || isRangeMode) return;
jumpToStep(currentStepIndex + 1);
});
document.querySelector('.ejceesrcdstart').addEventListener('click', ejceesrcdstartclick);
function ejceesrcdstartclick() {
if (isEditingComment || isExportTextMode || isEditMode) return;
jumpToStep(0);
}
Inside document.addEventListener('DOMContentLoaded', () => { ... }), update the tools (tool-new, tool-save, tool-exp-txt, tool-exp-svg) to handle extracting the active Range Selection.
Tool New: Replace toolNewBtn click listener with:
JavaScript
const toolNewBtn = document.getElementById('tool-new');
if (toolNewBtn) {
toolNewBtn.addEventListener('click', () => {
if (isExportTextMode) return;
if (isRangeMode && rangeStart !== null && rangeEnd !== null) {
const startNode = getNodeAtStep(rangeStart);
const startFen = startNode.fen;
let newHistory = { fen: startFen, move: null, lastMove: null, c: "", v: [] };
let curr = newHistory;
for (let i = rangeStart + 1; i <= rangeEnd; i++) {
const node = getNodeAtStep(i);
let newNode = { fen: node.fen, move: node.move, lastMove: node.lastMove, c: node.c, v: [] };
curr.v.push(newNode);
curr = newNode;
}
historyFEN = newHistory;
currentBranch = [];
currentStepIndex = 0;
isRangeMode = false;
rangeStart = null; rangeEnd = null; rangeClicks = 0;
clearRangeBorders();
stepSlider.max = getPathDepth(historyFEN, currentBranch) - 1;
stepSlider.value = 0;
renderRecordUI();
renderNoteUI();
loadFEN(startFen);
applyLastMoveVisuals(null);
saveStateToUndo();
return;
}
historyFEN = {
fen: INITIAL_FEN,
move: null,
lastMove: null,
c: "",
v: []
};
currentBranch = [];
currentStepIndex = 0;
stepSlider.max = 0;
stepSlider.value = 0;
renderRecordUI();
renderNoteUI();
highlightActiveStep(0);
loadFEN(INITIAL_FEN);
applyLastMoveVisuals(null);
saveStateToUndo();
});
}
Tool Save: Replace tool-save listener with:
JavaScript
document.getElementById('tool-save').addEventListener('click', () => {
if (isExportTextMode) return;
function simplify(node) {
let obj = node.move ? { m: node.move } : {};
if (node.c) obj.c = node.c;
if (node.v && node.v.length > 0) {
obj.v = node.v.map(child => simplify(child));
}
return obj;
}
let copy;
if (isRangeMode && rangeStart !== null && rangeEnd !== null) {
const startNode = getNodeAtStep(rangeStart);
let rangeHistory = { fen: startNode.fen, move: null, lastMove: null, c: "", v: [] };
let curr = rangeHistory;
for (let i = rangeStart + 1; i <= rangeEnd; i++) {
const node = getNodeAtStep(i);
let newNode = { fen: node.fen, move: node.move, lastMove: node.lastMove, c: node.c, v: [] };
curr.v.push(newNode);
curr = newNode;
}
copy = simplify(rangeHistory);
copy.fen = rangeHistory.fen;
isRangeMode = false;
rangeStart = null; rangeEnd = null; rangeClicks = 0;
clearRangeBorders();
renderNoteUI();
} else {
copy = simplify(historyFEN);
copy.fen = historyFEN.fen;
}
const blob = new Blob([JSON.stringify(copy)], { type: 'text/plain' });
const a = document.createElement('a');
const now = new Date();
const timestamp = now.getFullYear().toString().padStart(4, '0') +
(now.getMonth() + 1).toString().padStart(2, '0') +
now.getDate().toString().padStart(2, '0') +
now.getHours().toString().padStart(2, '0') +
now.getMinutes().toString().padStart(2, '0') +
now.getSeconds().toString().padStart(2, '0');
a.href = URL.createObjectURL(blob);
a.download = `ejcees_json_${timestamp}.txt`;
a.click();
});
Export Path Filter: Replace the getGamePath function entirely:
JavaScript
function getGamePath() {
let path = [];
let startIdx = (isRangeMode && rangeStart !== null) ? rangeStart : 0;
let endIdx = (isRangeMode && rangeEnd !== null) ? rangeEnd : getPathDepth(historyFEN, currentBranch) - 1;
let step = 0;
let currentNode = historyFEN;
let branchPtr = 0;
while (currentNode && currentNode.v && currentNode.v.length > 0) {
const choice = (currentNode.v.length > 1) ? (currentBranch[branchPtr++] || 0) : 0;
currentNode = currentNode.v[choice];
step++;
if (step > startIdx && step <= endIdx) {
path.push(currentNode);
}
if (step === endIdx) break;
}
return path;
}
function generateExportText(isEnglish) {
let path = getGamePath();
let startIdx = (isRangeMode && rangeStart !== null) ? rangeStart : 0;
let fen = getNodeAtStep(startIdx).fen;
let text = fen + '\n';
let isRedTurn = fen.split(' ')[1] === 'w';
let moveIdx = 0;
let moveNum = 1;
// ... rest of the original generateExportText
Tool Export Listeners: Replace their respective listeners to clear the range style upon clicking.
JavaScript
document.getElementById('tool-exp-txt').addEventListener('click', () => {
if (isExportTextMode || isEditingComment) return;
isExportTextMode = true;
updateToolHighlights();
renderExportTextUI();
if (isRangeMode) {
isRangeMode = false;
clearRangeBorders();
}
});
document.getElementById('tool-exp-svg').addEventListener('click', () => {
if (isRangeMode) {
isRangeMode = false;
clearRangeBorders();
renderNoteUI();
}
});