better cursor restoration
Hunter
Monday, July 7th, 2025 at 6:04 PM
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>