Commit ·
0f88e4f
1
Parent(s): afb0c39
feat: Add custom Undo/Redo for corrections and dismissals - Ctrl+Z undoes correction/dismiss (restores previous editor state) - Ctrl+Y or Ctrl+Shift+Z redoes - pushUndoState() called before every applyCorrection, applyAlternative, dismissSuggestion, applyAll - 50-level undo stack - Only intercepts when custom stack has entries (falls through to browser native otherwise)
Browse files- src/js/editor.js +55 -0
src/js/editor.js
CHANGED
|
@@ -7,6 +7,44 @@ let _lastInputTime = 0;
|
|
| 7 |
const ANALYZE_DEBOUNCE_MS = 1000;
|
| 8 |
const MAX_ANALYZE_LENGTH = 5000;
|
| 9 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
// Dismissed words whitelist — words the user chose to keep as-is
|
| 11 |
const _dismissedWords = new Set(
|
| 12 |
JSON.parse(localStorage.getItem('bayan_dismissed_words') || '[]')
|
|
@@ -54,6 +92,19 @@ function initEditor() {
|
|
| 54 |
|
| 55 |
document.addEventListener('keydown', (e) => {
|
| 56 |
if (e.key === 'Escape') hideTooltip();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
});
|
| 58 |
|
| 59 |
document.addEventListener('click', (e) => {
|
|
@@ -336,6 +387,7 @@ function hideTooltip() {
|
|
| 336 |
}
|
| 337 |
|
| 338 |
function applySuggestionAtOffsets(suggestion) {
|
|
|
|
| 339 |
// Find the error span in the DOM and replace its text content
|
| 340 |
// This preserves formatting (bold, italic, etc.) around/inside the span
|
| 341 |
const idx = (window.currentSuggestions || []).indexOf(suggestion);
|
|
@@ -383,6 +435,7 @@ function applyCorrection() {
|
|
| 383 |
}
|
| 384 |
|
| 385 |
function applyAlternativeCorrection(suggestion, correctionText) {
|
|
|
|
| 386 |
const idx = (window.currentSuggestions || []).indexOf(suggestion);
|
| 387 |
const errorSpan = idx >= 0 ? document.querySelector(`[data-suggestion-id="${idx}"]`) : null;
|
| 388 |
|
|
@@ -418,6 +471,7 @@ function applyAlternativeCorrection(suggestion, correctionText) {
|
|
| 418 |
}
|
| 419 |
|
| 420 |
function dismissSuggestion(suggestion) {
|
|
|
|
| 421 |
// Add the word to dismissed whitelist so it's never flagged again
|
| 422 |
if (suggestion.original) {
|
| 423 |
_dismissedWords.add(suggestion.original);
|
|
@@ -464,6 +518,7 @@ function applyAllSuggestions() {
|
|
| 464 |
const suggestions = [...(window.currentSuggestions || [])].sort((a, b) => b.start - a.start);
|
| 465 |
if (suggestions.length === 0) return;
|
| 466 |
if (!confirm('هل تريد تطبيق جميع التصحيحات (' + suggestions.length + ')؟')) return;
|
|
|
|
| 467 |
|
| 468 |
let text = getEditorText();
|
| 469 |
suggestions.forEach((s) => {
|
|
|
|
| 7 |
const ANALYZE_DEBOUNCE_MS = 1000;
|
| 8 |
const MAX_ANALYZE_LENGTH = 5000;
|
| 9 |
|
| 10 |
+
// ── Custom Undo/Redo Stack ──
|
| 11 |
+
const _undoStack = [];
|
| 12 |
+
const _redoStack = [];
|
| 13 |
+
const _MAX_UNDO = 50;
|
| 14 |
+
|
| 15 |
+
function pushUndoState() {
|
| 16 |
+
const editor = getEditorElement();
|
| 17 |
+
if (!editor) return;
|
| 18 |
+
const html = editor.innerHTML;
|
| 19 |
+
// Avoid duplicate consecutive entries
|
| 20 |
+
if (_undoStack.length > 0 && _undoStack[_undoStack.length - 1] === html) return;
|
| 21 |
+
_undoStack.push(html);
|
| 22 |
+
if (_undoStack.length > _MAX_UNDO) _undoStack.shift();
|
| 23 |
+
_redoStack.length = 0; // Clear redo on new action
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
function editorUndo() {
|
| 27 |
+
const editor = getEditorElement();
|
| 28 |
+
if (!editor || _undoStack.length === 0) return false;
|
| 29 |
+
_redoStack.push(editor.innerHTML);
|
| 30 |
+
editor.innerHTML = _undoStack.pop();
|
| 31 |
+
updateEditorStats();
|
| 32 |
+
updatePlaceholder();
|
| 33 |
+
analyzeTextDelayed();
|
| 34 |
+
return true;
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
function editorRedo() {
|
| 38 |
+
const editor = getEditorElement();
|
| 39 |
+
if (!editor || _redoStack.length === 0) return false;
|
| 40 |
+
_undoStack.push(editor.innerHTML);
|
| 41 |
+
editor.innerHTML = _redoStack.pop();
|
| 42 |
+
updateEditorStats();
|
| 43 |
+
updatePlaceholder();
|
| 44 |
+
analyzeTextDelayed();
|
| 45 |
+
return true;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
// Dismissed words whitelist — words the user chose to keep as-is
|
| 49 |
const _dismissedWords = new Set(
|
| 50 |
JSON.parse(localStorage.getItem('bayan_dismissed_words') || '[]')
|
|
|
|
| 92 |
|
| 93 |
document.addEventListener('keydown', (e) => {
|
| 94 |
if (e.key === 'Escape') hideTooltip();
|
| 95 |
+
// Custom Undo/Redo for corrections
|
| 96 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
| 97 |
+
if (_undoStack.length > 0) {
|
| 98 |
+
e.preventDefault();
|
| 99 |
+
editorUndo();
|
| 100 |
+
}
|
| 101 |
+
}
|
| 102 |
+
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
|
| 103 |
+
if (_redoStack.length > 0) {
|
| 104 |
+
e.preventDefault();
|
| 105 |
+
editorRedo();
|
| 106 |
+
}
|
| 107 |
+
}
|
| 108 |
});
|
| 109 |
|
| 110 |
document.addEventListener('click', (e) => {
|
|
|
|
| 387 |
}
|
| 388 |
|
| 389 |
function applySuggestionAtOffsets(suggestion) {
|
| 390 |
+
pushUndoState(); // Save state before correction
|
| 391 |
// Find the error span in the DOM and replace its text content
|
| 392 |
// This preserves formatting (bold, italic, etc.) around/inside the span
|
| 393 |
const idx = (window.currentSuggestions || []).indexOf(suggestion);
|
|
|
|
| 435 |
}
|
| 436 |
|
| 437 |
function applyAlternativeCorrection(suggestion, correctionText) {
|
| 438 |
+
pushUndoState(); // Save state before correction
|
| 439 |
const idx = (window.currentSuggestions || []).indexOf(suggestion);
|
| 440 |
const errorSpan = idx >= 0 ? document.querySelector(`[data-suggestion-id="${idx}"]`) : null;
|
| 441 |
|
|
|
|
| 471 |
}
|
| 472 |
|
| 473 |
function dismissSuggestion(suggestion) {
|
| 474 |
+
pushUndoState(); // Save state before dismiss
|
| 475 |
// Add the word to dismissed whitelist so it's never flagged again
|
| 476 |
if (suggestion.original) {
|
| 477 |
_dismissedWords.add(suggestion.original);
|
|
|
|
| 518 |
const suggestions = [...(window.currentSuggestions || [])].sort((a, b) => b.start - a.start);
|
| 519 |
if (suggestions.length === 0) return;
|
| 520 |
if (!confirm('هل تريد تطبيق جميع التصحيحات (' + suggestions.length + ')؟')) return;
|
| 521 |
+
pushUndoState(); // Save state before applying all
|
| 522 |
|
| 523 |
let text = getEditorText();
|
| 524 |
suggestions.forEach((s) => {
|