From aa510368f77ecd52858c26afa670c56656c7fe68 Mon Sep 17 00:00:00 2001 From: Daniel Kats Date: Tue, 2 Jun 2026 09:11:56 +0200 Subject: [PATCH] Highlight selected atoms in the XYZ text editor When editing coordinates in the XYZ editor's text mode, the lines belonging to currently selected atoms are now tinted via a highlight overlay behind the textarea, so it's easy to find and modify them. Entering edit mode no longer clears the selection, and selecting an atom (in 3D or the row view) updates the overlay live. Co-Authored-By: Claude Opus 4.8 (1M context) --- css/styles.css | 51 ++++++++++++++++++++++++++++++++++++++--- index.html | 5 +++- js/selection.js | 10 ++++++++ js/xyz-editor.js | 59 +++++++++++++++++++++++++++++++++++++++++++----- 4 files changed, 115 insertions(+), 10 deletions(-) diff --git a/css/styles.css b/css/styles.css index eaafb42..8adb14f 100644 --- a/css/styles.css +++ b/css/styles.css @@ -161,11 +161,56 @@ .xyz-content-edit { display: none; } - textarea.xyz-editor { + /* XYZ edit textarea + highlight overlay. The backdrop sits behind a + transparent-background textarea and tints the lines of selected + atoms; both share identical metrics so the text lines up exactly. */ + #xyz-edit-wrap { + position: relative; + margin: 10px 0; + } + #xyz-content-edit.xyz-editor, + #xyz-edit-backdrop { width: 100%; - min-height: 200px; + box-sizing: border-box; + margin: 0; + padding: 6px; + border: 1px solid #ccc; font-family: monospace; - margin: 10px 0; + font-size: 13px; + line-height: 1.4; + white-space: pre; + letter-spacing: normal; + tab-size: 4; + } + #xyz-content-edit.xyz-editor { + position: relative; + z-index: 1; + display: block; + min-height: 200px; + resize: vertical; + overflow: auto; + background: transparent; + color: #000; + } + #xyz-edit-backdrop { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 0; + overflow: hidden; + pointer-events: none; + border-color: transparent; + background: #fff; + color: transparent; + } + #xyz-edit-highlights { + min-height: 200px; + } + #xyz-edit-highlights .xyz-edit-hl { + background: #fff3a0; + border-radius: 2px; } .mode-buttons { margin: 10px 0; diff --git a/index.html b/index.html index 0269834..98ca1b7 100644 --- a/index.html +++ b/index.html @@ -160,7 +160,10 @@

XYZ Editor

Click atoms in the 3D structure or the rows below to select them.
- +
+
+ +
diff --git a/js/selection.js b/js/selection.js index 7ae084a..f654210 100644 --- a/js/selection.js +++ b/js/selection.js @@ -10,6 +10,16 @@ function updateSelectionUI() { const n = selectedAtoms.size; countEl.textContent = n > 0 ? `${n} selected` : ''; } + refreshEditHighlightsIfVisible(); +} + +// When the XYZ editor's text edit mode is open, keep its highlight overlay in +// sync with the current selection (e.g. atoms picked in the 3D structure). +function refreshEditHighlightsIfVisible() { + const wrap = document.getElementById('xyz-edit-wrap'); + if (wrap && wrap.style.display !== 'none' && typeof renderXYZEditHighlights === 'function') { + renderXYZEditHighlights(); + } } // Turn the 3D halo on/off for a single atom (atomIndex is 0-based). diff --git a/js/xyz-editor.js b/js/xyz-editor.js index 1d1d1bf..f6f7dd4 100644 --- a/js/xyz-editor.js +++ b/js/xyz-editor.js @@ -106,7 +106,9 @@ function showXYZEditor() { // Update textarea content const editArea = document.getElementById('xyz-content-edit'); editArea.value = lastXYZData; - editArea.style.display = 'none'; // Ensure we start in selection mode + initXYZEditHighlights(); + renderXYZEditHighlights(); + document.getElementById('xyz-edit-wrap').style.display = 'none'; // Ensure we start in selection mode xyzContent.style.display = 'block'; document.getElementById('toggle-edit-mode').textContent = 'Switch to Edit Mode'; @@ -126,16 +128,16 @@ function showXYZEditor() { function toggleEditMode() { const selectionView = document.getElementById('xyz-content'); - const editView = document.getElementById('xyz-content-edit'); + const editView = document.getElementById('xyz-edit-wrap'); const editButton = document.getElementById('toggle-edit-mode'); - + if (selectionView.style.display !== 'none') { - // Switch to edit mode + // Switch to edit mode. Keep the current selection so the chosen atoms + // stay highlighted in the textarea, making them easy to edit. selectionView.style.display = 'none'; editView.style.display = 'block'; editButton.textContent = 'Switch to Selection Mode'; - // Clear any existing selections (keeps state Set, rows and halos in sync) - clearAtomSelection(); + renderXYZEditHighlights(); } else { // Switch to selection mode selectionView.style.display = 'block'; @@ -146,6 +148,51 @@ function toggleEditMode() { } } +// Escape text so user-entered atom names/coordinates can't inject markup into +// the highlight overlay. +function escapeHtmlForHighlight(s) { + return s.replace(/&/g, '&').replace(//g, '>'); +} + +// Re-draw the edit-mode highlight overlay: tint the textarea lines that belong +// to currently selected atoms. Atom i (0-based) lives on text line i + 2 +// (line 0 = atom count, line 1 = comment). +function renderXYZEditHighlights() { + const textarea = document.getElementById('xyz-content-edit'); + const backdrop = document.getElementById('xyz-edit-backdrop'); + const highlights = document.getElementById('xyz-edit-highlights'); + if (!textarea || !backdrop || !highlights) return; + + const selectedLines = new Set(Array.from(selectedAtoms).map(i => i + 2)); + const lines = textarea.value.split('\n'); + highlights.innerHTML = lines.map((line, idx) => { + const safe = escapeHtmlForHighlight(line); + return selectedLines.has(idx) ? `${safe}` : safe; + }).join('\n'); + + // Keep the overlay scroll-aligned with the textarea. + backdrop.scrollTop = textarea.scrollTop; + backdrop.scrollLeft = textarea.scrollLeft; +} + +// Attach the textarea listeners that keep the highlight overlay in sync. Runs +// once; guarded by a data flag so repeated showXYZEditor() calls don't stack +// listeners. +function initXYZEditHighlights() { + const textarea = document.getElementById('xyz-content-edit'); + if (!textarea || textarea.dataset.hlInit) return; + textarea.dataset.hlInit = '1'; + + const backdrop = document.getElementById('xyz-edit-backdrop'); + textarea.addEventListener('input', renderXYZEditHighlights); + textarea.addEventListener('scroll', function() { + if (backdrop) { + backdrop.scrollTop = textarea.scrollTop; + backdrop.scrollLeft = textarea.scrollLeft; + } + }); +} + function updateStructure() { const editView = document.getElementById('xyz-content-edit'); try {