better cursor restoration

Hunter
Monday, July 7th, 2025 at 6:04 PM

index.html+191-12

index.html
  4      <meta charset="UTF-8">
  5      <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6      <title>HTML Laboratory</title>
  7 +    <link rel="stylesheet" href="prism.css">
  8      <style>
  9          :root {
 10              --bg-color: #2d2d2d;
 11              --text-color: #f8f8f2;
    -            --editor-bg: #1e1e1e;
 12 +            --editor-bg: #272822;
 13              --preview-bg: #2d2d2d;
 14          }
 15  

 48              height: 100%;
 49              border: none;
 50              outline: none;
    -            font-family: monospace;
 51 +            font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
 52              font-size: 21px;
 53              padding: 15px;
 54              overflow: auto;
 55              background: var(--editor-bg);
 56              color: var(--text-color);
    -            spellcheck: false;
 57              tab-size: 4;
 58              white-space: pre-wrap;
 59              overflow-x: auto;
 60              display: block;
 61 +            line-height: 1.5;
 62 +        }
 63 +        
 64 +        /* Override Prism background to match our theme */
 65 +        #editor.language-html {
 66 +            background: var(--editor-bg) !important;
 67          }
 68  
 69          #editor:empty:before {

108  </head>
109  <body>
110      <div class="editor-pane">
    -        <code id="editor" contenteditable="true" spellcheck="false" data-placeholder="Type your HTML, CSS, and JavaScript here..."></code>
111 +        <code id="editor" class="language-html" contenteditable="true" spellcheck="false" data-placeholder="Type your HTML, CSS, and JavaScript here..."></code>
112      </div>
113      
114      <div class="preview-pane">

161                  e.preventDefault();
162                  
163                  const selection = window.getSelection();
164 +                if (selection.rangeCount === 0) return;
165 +                
166                  const range = selection.getRangeAt(0);
167                  
    -                // Insert tab character at cursor position
    -                const tabNode = document.createTextNode('\t');
    -                range.insertNode(tabNode);
168 +                // Get current cursor position in plain text
169 +                const cursorPos = getCaretOffset(editor, range.startContainer, range.startOffset);
170 +                
171 +                // Get the plain text content
172 +                const plainText = editor.textContent;
173 +                
174 +                // Insert tab at cursor position
175 +                const newText = plainText.substring(0, cursorPos) + '\t' + plainText.substring(cursorPos);
176 +                
177 +                // Update the plain text content first
178 +                editor.textContent = newText;
179 +                
180 +                // Store the new content as last highlighted to prevent immediate re-highlighting
181 +                editor.dataset.lastHighlighted = newText;
182                  
    -                // Move cursor to after the inserted tab
    -                range.setStartAfter(tabNode);
    -                range.setEndAfter(tabNode);
    -                selection.removeAllRanges();
    -                selection.addRange(range);
183 +                // Apply syntax highlighting
184 +                const highlighted = Prism.highlight(newText, Prism.languages.html, 'html');
185 +                editor.innerHTML = highlighted;
186 +                
187 +                // Restore cursor position after the tab
188 +                setTimeout(() => {
189 +                    restoreCaretPosition(editor, cursorPos + 1);
190 +                }, 0);
191                  
192                  // Trigger input event to update preview
193                  this.dispatchEvent(new Event('input'));

239          // Initialize when page loads
240          initialize();
241      </script>
242 +    <script src="prism.js"></script>
243 +    <script>
244 +        // Set Prism to manual mode to control highlighting
245 +        Prism.manual = true;
246 +        
247 +        // Function to apply syntax highlighting
248 +        function applySyntaxHighlighting() {
249 +            // Don't highlight if editor is not focused to avoid disrupting user
250 +            if (document.activeElement !== editor) {
251 +                return;
252 +            }
253 +            
254 +            // Save cursor position before any changes
255 +            const selection = window.getSelection();
256 +            let offset = 0;
257 +            let hasSelection = false;
258 +            
259 +            if (selection.rangeCount > 0) {
260 +                const range = selection.getRangeAt(0);
261 +                offset = getCaretOffset(editor, range.startContainer, range.startOffset);
262 +                hasSelection = true;
263 +            }
264 +            
265 +            // Get the current content
266 +            const code = editor.textContent;
267 +            
268 +            // Only apply highlighting if the content has actually changed
269 +            if (code === editor.dataset.lastHighlighted) {
270 +                return;
271 +            }
272 +            
273 +            // Apply new highlighting
274 +            const highlighted = Prism.highlight(code, Prism.languages.html, 'html');
275 +            
276 +            // Update content with highlighting
277 +            editor.innerHTML = highlighted;
278 +            
279 +            // Store the last highlighted content
280 +            editor.dataset.lastHighlighted = code;
281 +            
282 +            // Restore cursor position
283 +            if (hasSelection) {
284 +                // Use a small timeout to ensure DOM is updated
285 +                setTimeout(() => {
286 +                    restoreCaretPosition(editor, offset);
287 +                }, 0);
288 +            }
289 +        }
290 +        
291 +        // Helper function to get caret offset
292 +        function getCaretOffset(root, node, offset) {
293 +            try {
294 +                let textOffset = 0;
295 +                let walker = document.createTreeWalker(
296 +                    root,
297 +                    NodeFilter.SHOW_TEXT,
298 +                    null,
299 +                    false
300 +                );
301 +                
302 +                let currentNode;
303 +                while (currentNode = walker.nextNode()) {
304 +                    if (currentNode === node) {
305 +                        return textOffset + Math.min(offset, currentNode.textContent.length);
306 +                    }
307 +                    textOffset += currentNode.textContent.length;
308 +                }
309 +                
310 +                // If we can't find the node, return the total length
311 +                return textOffset;
312 +            } catch (e) {
313 +                console.warn('Error getting caret offset:', e);
314 +                return 0;
315 +            }
316 +        }
317 +        
318 +        // Helper function to restore caret position
319 +        function restoreCaretPosition(root, offset) {
320 +            try {
321 +                let textOffset = 0;
322 +                let walker = document.createTreeWalker(
323 +                    root,
324 +                    NodeFilter.SHOW_TEXT,
325 +                    null,
326 +                    false
327 +                );
328 +                
329 +                let currentNode;
330 +                while (currentNode = walker.nextNode()) {
331 +                    const nodeLength = currentNode.textContent.length;
332 +                    if (textOffset + nodeLength >= offset) {
333 +                        const range = document.createRange();
334 +                        const selection = window.getSelection();
335 +                        const nodeOffset = Math.min(Math.max(0, offset - textOffset), nodeLength);
336 +                        
337 +                        range.setStart(currentNode, nodeOffset);
338 +                        range.setEnd(currentNode, nodeOffset);
339 +                        selection.removeAllRanges();
340 +                        selection.addRange(range);
341 +                        return true;
342 +                    }
343 +                    textOffset += nodeLength;
344 +                }
345 +                
346 +                // If we can't find the exact position, put cursor at the end of the last text node
347 +                walker = document.createTreeWalker(
348 +                    root,
349 +                    NodeFilter.SHOW_TEXT,
350 +                    null,
351 +                    false
352 +                );
353 +                
354 +                let lastNode = null;
355 +                while (currentNode = walker.nextNode()) {
356 +                    lastNode = currentNode;
357 +                }
358 +                
359 +                if (lastNode) {
360 +                    const range = document.createRange();
361 +                    const selection = window.getSelection();
362 +                    range.setStart(lastNode, lastNode.textContent.length);
363 +                    range.setEnd(lastNode, lastNode.textContent.length);
364 +                    selection.removeAllRanges();
365 +                    selection.addRange(range);
366 +                    return true;
367 +                }
368 +            } catch (e) {
369 +                console.warn('Error restoring cursor position:', e);
370 +            }
371 +            return false;
372 +        }
373 +        
374 +        // Apply syntax highlighting with debounce
375 +        let highlightTimer;
376 +        function scheduleHighlighting() {
377 +            clearTimeout(highlightTimer);
378 +            highlightTimer = setTimeout(applySyntaxHighlighting, 150);
379 +        }
380 +        
381 +        // Update the input event listener to include syntax highlighting
382 +        editor.addEventListener('input', function() {
383 +            clearTimeout(updateTimer);
384 +            updateTimer = setTimeout(updatePreview, 500);
385 +            
386 +            // Save to localStorage on every change
387 +            saveToStorage();
388 +            
389 +            // Schedule syntax highlighting
390 +            scheduleHighlighting();
391 +        });
392 +        
393 +        // Apply initial syntax highlighting
394 +        setTimeout(() => {
395 +            applySyntaxHighlighting();
396 +            editor.focus();
397 +        }, 100);
398 +    </script>
399  </body>
400  </html>