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.
This commit is contained in:
+47
-16
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user