commit 0a97f9d6d44c91c0bcd213a3efb86a45d818947a
parent 90539b71553039ec82f5fd1c7457214482dc6ba3
Author: Hunter
Date:   Mon,  7 Jul 2025 18:04:08 -0400

better cursor restoration

Diffstat:
Mindex.html | 203++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
1 file changed, 191 insertions(+), 12 deletions(-)

diff --git a/index.html b/index.html @@ -4,11 +4,12 @@ <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>HTML Laboratory</title> + <link rel="stylesheet" href="prism.css"> <style> :root { --bg-color: #2d2d2d; --text-color: #f8f8f2; - --editor-bg: #1e1e1e; + --editor-bg: #272822; --preview-bg: #2d2d2d; } @@ -47,17 +48,22 @@ height: 100%; border: none; outline: none; - font-family: monospace; + font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; font-size: 21px; padding: 15px; overflow: auto; background: var(--editor-bg); color: var(--text-color); - spellcheck: false; tab-size: 4; white-space: pre-wrap; overflow-x: auto; display: block; + line-height: 1.5; + } + + /* Override Prism background to match our theme */ + #editor.language-html { + background: var(--editor-bg) !important; } #editor:empty:before { @@ -102,7 +108,7 @@ </head> <body> <div class="editor-pane"> - <code id="editor" contenteditable="true" spellcheck="false" data-placeholder="Type your HTML, CSS, and JavaScript here..."></code> + <code id="editor" class="language-html" contenteditable="true" spellcheck="false" data-placeholder="Type your HTML, CSS, and JavaScript here..."></code> </div> <div class="preview-pane"> @@ -155,17 +161,33 @@ e.preventDefault(); const selection = window.getSelection(); + if (selection.rangeCount === 0) return; + const range = selection.getRangeAt(0); - // Insert tab character at cursor position - const tabNode = document.createTextNode('\t'); - range.insertNode(tabNode); + // Get current cursor position in plain text + const cursorPos = getCaretOffset(editor, range.startContainer, range.startOffset); + + // Get the plain text content + const plainText = editor.textContent; + + // Insert tab at cursor position + const newText = plainText.substring(0, cursorPos) + '\t' + plainText.substring(cursorPos); + + // Update the plain text content first + editor.textContent = newText; + + // Store the new content as last highlighted to prevent immediate re-highlighting + editor.dataset.lastHighlighted = newText; - // Move cursor to after the inserted tab - range.setStartAfter(tabNode); - range.setEndAfter(tabNode); - selection.removeAllRanges(); - selection.addRange(range); + // Apply syntax highlighting + const highlighted = Prism.highlight(newText, Prism.languages.html, 'html'); + editor.innerHTML = highlighted; + + // Restore cursor position after the tab + setTimeout(() => { + restoreCaretPosition(editor, cursorPos + 1); + }, 0); // Trigger input event to update preview this.dispatchEvent(new Event('input')); @@ -217,5 +239,162 @@ // Initialize when page loads initialize(); </script> + <script src="prism.js"></script> + <script> + // Set Prism to manual mode to control highlighting + Prism.manual = true; + + // Function to apply syntax highlighting + function applySyntaxHighlighting() { + // Don't highlight if editor is not focused to avoid disrupting user + if (document.activeElement !== editor) { + return; + } + + // Save cursor position before any changes + const selection = window.getSelection(); + let offset = 0; + let hasSelection = false; + + if (selection.rangeCount > 0) { + const range = selection.getRangeAt(0); + offset = getCaretOffset(editor, range.startContainer, range.startOffset); + hasSelection = true; + } + + // Get the current content + const code = editor.textContent; + + // Only apply highlighting if the content has actually changed + if (code === editor.dataset.lastHighlighted) { + return; + } + + // Apply new highlighting + const highlighted = Prism.highlight(code, Prism.languages.html, 'html'); + + // Update content with highlighting + editor.innerHTML = highlighted; + + // Store the last highlighted content + editor.dataset.lastHighlighted = code; + + // Restore cursor position + if (hasSelection) { + // Use a small timeout to ensure DOM is updated + setTimeout(() => { + restoreCaretPosition(editor, offset); + }, 0); + } + } + + // Helper function to get caret offset + function getCaretOffset(root, node, offset) { + try { + let textOffset = 0; + let walker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let currentNode; + while (currentNode = walker.nextNode()) { + if (currentNode === node) { + return textOffset + Math.min(offset, currentNode.textContent.length); + } + textOffset += currentNode.textContent.length; + } + + // If we can't find the node, return the total length + return textOffset; + } catch (e) { + console.warn('Error getting caret offset:', e); + return 0; + } + } + + // Helper function to restore caret position + function restoreCaretPosition(root, offset) { + try { + let textOffset = 0; + let walker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let currentNode; + while (currentNode = walker.nextNode()) { + const nodeLength = currentNode.textContent.length; + if (textOffset + nodeLength >= offset) { + const range = document.createRange(); + const selection = window.getSelection(); + const nodeOffset = Math.min(Math.max(0, offset - textOffset), nodeLength); + + range.setStart(currentNode, nodeOffset); + range.setEnd(currentNode, nodeOffset); + selection.removeAllRanges(); + selection.addRange(range); + return true; + } + textOffset += nodeLength; + } + + // If we can't find the exact position, put cursor at the end of the last text node + walker = document.createTreeWalker( + root, + NodeFilter.SHOW_TEXT, + null, + false + ); + + let lastNode = null; + while (currentNode = walker.nextNode()) { + lastNode = currentNode; + } + + if (lastNode) { + const range = document.createRange(); + const selection = window.getSelection(); + range.setStart(lastNode, lastNode.textContent.length); + range.setEnd(lastNode, lastNode.textContent.length); + selection.removeAllRanges(); + selection.addRange(range); + return true; + } + } catch (e) { + console.warn('Error restoring cursor position:', e); + } + return false; + } + + // Apply syntax highlighting with debounce + let highlightTimer; + function scheduleHighlighting() { + clearTimeout(highlightTimer); + highlightTimer = setTimeout(applySyntaxHighlighting, 150); + } + + // Update the input event listener to include syntax highlighting + editor.addEventListener('input', function() { + clearTimeout(updateTimer); + updateTimer = setTimeout(updatePreview, 500); + + // Save to localStorage on every change + saveToStorage(); + + // Schedule syntax highlighting + scheduleHighlighting(); + }); + + // Apply initial syntax highlighting + setTimeout(() => { + applySyntaxHighlighting(); + editor.focus(); + }, 100); + </script> </body> </html> \ No newline at end of file