fix: slash/QI keyboard nav + bubble position at selection
CI / lint (push) Successful in 19s
CI / security (push) Successful in 9s
CI / test (push) Successful in 24s
CI / build (push) Successful in 2s

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.
This commit is contained in:
2026-06-05 23:09:45 -04:00
parent bc47f79378
commit bbe109e4e5
+47 -16
View File
@@ -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);