# Phase 1 Verification 7 - Text Selection Preservation **Test Scenario**: 1. User selects a sentence 2. Trigger analysis 3. Verify selection remains active and on same text --- ## Code Analysis: Selection Preservation ### Initial Setup ``` Text: "الحمد لله على نعمه / الله أكبر كبيرا" Selection: "لله على نعمه" (selected by user) └────────────┘ From char 5 to char 18 (13 characters) ``` ### STEP 1: User Makes Selection **Browser creates Range**: ``` range.startContainer = text node range.startOffset = 5 (character in text "لله على نعمه") range.endContainer = text node range.endOffset = 18 (end of selection) ``` **Browser state**: ```javascript selection.rangeCount = 1; // One range selection.isCollapsed = false; // Not just cursor ``` ### STEP 2: Trigger Analysis (User types or waits) The analyzeTextDelayed() fires after 500ms debounce. ### STEP 3: SAVE SELECTION (CRITICAL) **From editor.js:analyzeText(), Lines 90-91** ```javascript const savedSelection = saveSelection(); ``` **Executes: selection.js:saveSelection()** ```javascript function saveSelection() { const selection = window.getSelection(); if (selection.rangeCount === 0) { return null; } const range = selection.getRangeAt(0); const editor = document.getElementById('#editor-container'); try { const preCaretRange = range.cloneRange(); preCaretRange.selectNodeContents(editor); preCaretRange.setEnd(range.endContainer, range.endOffset); const offset = preCaretRange.toString().length; const isCollapsed = range.collapsed; let selectionStart = offset; // End position first let selectionEnd = offset; if (!isCollapsed) { // If there's a selection (NOT just cursor) const preCaretRangeStart = range.cloneRange(); preCaretRangeStart.selectNodeContents(editor); preCaretRangeStart.setEnd(range.startContainer, range.startOffset); selectionStart = preCaretRangeStart.toString().length; // Start position } return { selectionStart, // 5 selectionEnd, // 18 isCollapsed: false // Selection exists }; } catch (e) { console.warn('saveSelection failed:', e); return null; } } ``` **Breakdown for our example**: 1. `range.getRangeAt(0)` gets current selection range 2. Clone range and measure to END of selection → offset = 18 3. `isCollapsed = false` (there IS a selection, not just cursor) 4. Clone range and measure to START of selection → offset = 5 5. Return both start (5) and end (18) **Result**: ```javascript savedSelection = { selectionStart: 5, // "ـ" of "لله" selectionEnd: 18, // After "نعمه" isCollapsed: false // This is a selection, not cursor } ``` ### STEP 4: Call API & Render **From editor.js:analyzeText()** ```javascript const response = await fetch('/api/analyze', {...}); const data = await response.json(); const highlightedHtml = render({text, suggestions: data.suggestions}); ``` **Output**: New HTML with span elements ### STEP 5: Apply New HTML to DOM **From editor.js:analyzeText(), Line 119** ```javascript setEditorHTML(highlightedHtml); ``` **From selection.js:setEditorHTML()** ```javascript function setEditorHTML(html) { const editor = document.getElementById('editor-container'); editor.innerHTML = html; // ← DOM completely replaced } ``` **RESULT**: Old DOM destroyed, new DOM with spans created. Old selection is lost (rendered DOM is different). ### STEP 6: RESTORE SELECTION (THE FIX) **From editor.js:analyzeText(), Lines 122-126** ```javascript if (savedSelection) { restoreSelection(savedSelection); // ← Called here } else { setCaretOffset(currentCaretOffset); } ``` **Executes: selection.js:restoreSelection()** ```javascript function restoreSelection(savedSelection) { if (!savedSelection) return; const editor = document.getElementById('editor-container'); const selection = window.getSelection(); try { let charCount = 0; let nodeStack = [editor]; let node, foundStart = false, foundEnd = false; while (!foundEnd && (node = nodeStack.pop())) { if (node.nodeType === Node.TEXT_NODE) { const nextCharCount = charCount + node.length; // STEP 1: Find start of selection if ( !foundStart && savedSelection.selectionStart >= charCount && savedSelection.selectionStart <= nextCharCount ) { const range = document.createRange(); range.setStart(node, savedSelection.selectionStart - charCount); foundStart = true; // STEP 2: Check if end is also in this node (short selection) if (savedSelection.isCollapsed) { range.collapse(true); selection.removeAllRanges(); selection.addRange(range); return; } } // STEP 3: Find end of selection if ( foundStart && savedSelection.selectionEnd >= charCount && savedSelection.selectionEnd <= nextCharCount ) { const range = selection.getRangeAt(0); range.setEnd(node, savedSelection.selectionEnd - charCount); foundEnd = true; // ← Selection now spans from start to end } charCount = nextCharCount; } else { let i = node.childNodes.length; while (i--) { nodeStack.push(node.childNodes[i]); } } } if (foundStart && foundEnd) { selection.removeAllRanges(); selection.addRange(selection.getRangeAt(0)); // Ensure selection is active } } catch (e) { console.warn('restoreSelection failed:', e); } } ``` **Execution for our example**: ``` 1. savedSelection = {selectionStart: 5, selectionEnd: 18, isCollapsed: false} 2. Walk through new DOM text nodes 3. Count characters: - "الحمد " → charCount: 0-5 4. Find char 5 → Found in "لله على نعمه" text node - range.setStart(node, 0) // Start of "لله على نعمه" - foundStart = true 5. Continue counting: - "لله على نعمه" → charCount: 5-18 6. Find char 18 → Found in same node - range.setEnd(node, 13) // End of "لله على نعمه" - foundEnd = true 7. Apply range to selection: - selection.removeAllRanges() - selection.addRange(range) → User's selection is restored! ``` **Result**: Selection highlighting active from character 5 to 18 in new DOM --- ## Execution Trace: Step by Step ### Before Analysis ``` Text: "الحمد لله على نعمه / الله أكبر كبيرا" Selection: └──"لله على نعمه"──┘ Start: 5, End: 18 Visual: الحمد [لله على نعمه] / الله أكبر كبيرا ↑────────────↑ ``` ### During Analysis (DOM Changes) ``` Old DOM: