From bbe109e4e5e7c1513ae23f8e00467ee9a82151f2 Mon Sep 17 00:00:00 2001 From: Bruno Charest Date: Fri, 5 Jun 2026 23:09:45 -0400 Subject: [PATCH] fix: slash/QI keyboard nav + bubble position at selection Slash nav: moved from textarea keydown to slashSearch input keydown (textarea loses focus when slash menu opens). QI nav: document-level up/down/enter/esc with highlight. Bubble: positioned at selection start via caretToPixel instead of textarea bottom center. --- frontend/editor-poc.html | 63 ++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 16 deletions(-) diff --git a/frontend/editor-poc.html b/frontend/editor-poc.html index dd2093b..c87d328 100644 --- a/frontend/editor-poc.html +++ b/frontend/editor-poc.html @@ -702,10 +702,15 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig // ---- Bubble menu ---- function showBubble() { if (ta.selectionStart === ta.selectionEnd) { hideBubble(); return; } - var tr = ta.getBoundingClientRect(); - var top = tr.bottom + 6; - var left = tr.left + (tr.width / 2) - (bubble.offsetWidth / 2); - if (top + 36 > window.innerHeight) top = tr.top - 44; + // Position below the selection start, using caret pixel calc + var pt = caretToPixel(ta.selectionStart); + // Convert from editorWrap-relative to viewport-relative + var wr = editorWrap.getBoundingClientRect(); + var top = wr.top + pt.y + 4; + var left = wr.left + pt.x - 10; + // Fallback if caret calc gives odd values + if (top < 50) { var tr = ta.getBoundingClientRect(); top = tr.bottom + 6; left = tr.left + (tr.width / 2) - (bubble.offsetWidth / 2); } + if (top + 36 > window.innerHeight) top = top - 60; if (left < 6) left = 6; if (left + bubble.offsetWidth > window.innerWidth - 6) left = window.innerWidth - bubble.offsetWidth - 6; bubble.style.left = left + 'px'; bubble.style.top = top + 'px'; @@ -736,6 +741,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig // ---- Quick Insert ---- function showQI() { qiMenu.classList.remove('on'); + qiIdx = -1; qiItems = []; // Position below current line, left-aligned var pt = caretToPixel(null); pt.x = Math.max(pt.x - 10, 4); @@ -809,22 +815,12 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig // ---- Event handlers ---- ta.addEventListener('keydown', function(e) { - // Slash menu + // Slash menu trigger: type / at start of line if (e.key === '/') { var l = getLine(); var p = getPos(); var before = l.text.substring(0, p.s - l.start); if (before === '' || /^\s*$/.test(before)) setTimeout(showSlash, 20); } - if (slashVisible) { - if (e.key === 'Escape') { e.preventDefault(); hideSlash(); } - else if (e.key === 'ArrowDown') { e.preventDefault(); navSlash(1); } - else if (e.key === 'ArrowUp') { e.preventDefault(); navSlash(-1); } - else if (e.key === 'Enter') { - e.preventDefault(); - var vis = getVisibleSlash(); - if (slashIdx >= 0 && slashIdx < vis.length) selectSlashAction(vis[slashIdx].getAttribute('data-action')); - } - } if (e.ctrlKey && e.key === 'j') { e.preventDefault(); toggleAIPanel(); } if (e.ctrlKey && e.key === 's') { e.preventDefault(); forceSave(); } if (e.ctrlKey && e.key === 'b') { e.preventDefault(); wrapSelection('**'); } @@ -833,6 +829,41 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig if (e.altKey && e.key === 'i') { e.preventDefault(); showQI(); } }); + // Slash menu keyboard nav (bound to search input which has focus) + slashSearch.addEventListener('keydown', function(e) { + if (!slashVisible) return; + if (e.key === 'Escape') { e.preventDefault(); hideSlash(); ta.focus(); } + else if (e.key === 'ArrowDown') { e.preventDefault(); navSlash(1); } + else if (e.key === 'ArrowUp') { e.preventDefault(); navSlash(-1); } + else if (e.key === 'Enter') { + e.preventDefault(); + var vis = getVisibleSlash(); + if (slashIdx >= 0 && slashIdx < vis.length) selectSlashAction(vis[slashIdx].getAttribute('data-action')); + } + }); + + // Quick Insert keyboard nav (document-level since no input to focus) + var qiIdx = -1; + var qiItems = []; + document.addEventListener('keydown', function(e) { + if (!qiMenu.classList.contains('on')) return; + if (!qiItems.length) qiItems = Array.from(qiMenu.querySelectorAll('.qi-item')); + if (e.key === 'Escape') { e.preventDefault(); qiMenu.classList.remove('on'); ta.focus(); } + else if (e.key === 'ArrowDown') { e.preventDefault(); qiIdx = (qiIdx + 1) % qiItems.length; highlightQI(); } + else if (e.key === 'ArrowUp') { e.preventDefault(); qiIdx = (qiIdx - 1 + qiItems.length) % qiItems.length; highlightQI(); } + else if (e.key === 'Enter') { + e.preventDefault(); + if (qiIdx >= 0 && qiIdx < qiItems.length) { + var action = qiItems[qiIdx].getAttribute('data-qi'); + qiMenu.classList.remove('on'); + selectSlashAction(action); + } + } + }); + function highlightQI() { + qiItems.forEach(function(item, i) { item.style.background = i === qiIdx ? 'var(--bg-hover)' : ''; item.style.color = i === qiIdx ? 'var(--text)' : ''; }); + } + ta.addEventListener('keyup', function(e) { if (e.key === '/' && slashVisible) { var l = getLine(); var p = getPos(); @@ -879,7 +910,7 @@ body { font-family: var(--sans); background: var(--bg); color: var(--text); heig document.addEventListener('click', function(e) { if (slashVisible && !slashMenu.contains(e.target) && e.target !== ta) hideSlash(); if (bubble.classList.contains('on') && !bubble.contains(e.target)) hideBubble(); - if (qiMenu.classList.contains('on') && !qiMenu.contains(e.target) && e.target.id !== 'qi-btn') qiMenu.classList.remove('on'); + if (qiMenu.classList.contains('on') && !qiMenu.contains(e.target) && e.target.id !== 'qi-btn') { qiMenu.classList.remove('on'); qiIdx = -1; qiItems = []; } }); btnAI.addEventListener('click', toggleAIPanel);