150 lines
5.9 KiB
JavaScript
150 lines
5.9 KiB
JavaScript
/**
|
|
* Shortcuts-cheatsheet popover — floating frosted-glass list of every
|
|
* editor keyboard shortcut, anchored above the topbar keyboard icon
|
|
* (drops below if there's no room above). Drag the header to move;
|
|
* Esc or click outside dismisses; position is persisted in
|
|
* localStorage so re-opening restores where the user left it.
|
|
*
|
|
* Public API: `toggleShortcuts(show?)` — true/false to force a state,
|
|
* undefined to toggle.
|
|
*
|
|
* @returns {{ toggleShortcuts: (show?: boolean) => void }}
|
|
*/
|
|
import { shortcutsPopupHTML } from './build/popups.js';
|
|
|
|
export function createShortcutsPopover() {
|
|
let pop = null;
|
|
let outside = null;
|
|
|
|
function ensurePopover() {
|
|
if (pop) return pop;
|
|
const el = document.createElement('div');
|
|
el.id = 'ge-shortcuts-popover';
|
|
el.style.cssText = [
|
|
'position:fixed', 'z-index:10000', 'display:none',
|
|
// Frosted-glass background: semi-transparent + heavy blur of
|
|
// what's behind. Layered with an inner translucent veil so
|
|
// light themes also read clearly without losing the see-through
|
|
// feel.
|
|
'background:color-mix(in srgb, var(--panel, #1a1a1a) 55%, transparent)',
|
|
'backdrop-filter:blur(18px) saturate(150%)',
|
|
'-webkit-backdrop-filter:blur(18px) saturate(150%)',
|
|
'color:var(--fg,#eee)',
|
|
'border:1px solid color-mix(in srgb, var(--fg, #eee) 18%, transparent)',
|
|
'border-radius:12px',
|
|
'box-shadow:0 14px 36px rgba(0,0,0,0.5), inset 0 1px 0 color-mix(in srgb, var(--fg, #fff) 8%, transparent)',
|
|
'padding:12px 14px', 'min-width:540px', 'max-width:min(720px,92vw)',
|
|
'font-size:12px', 'line-height:1.5',
|
|
].join(';');
|
|
el.innerHTML = shortcutsPopupHTML();
|
|
document.body.appendChild(el);
|
|
el.querySelector('#ge-shortcuts-close').addEventListener('click', () => toggleShortcuts(false));
|
|
|
|
// Drag by the header handle. Position survives across opens
|
|
// (localStorage).
|
|
const handle = el.querySelector('#ge-shortcuts-handle');
|
|
if (handle) {
|
|
let drag = null;
|
|
handle.addEventListener('pointerdown', (e) => {
|
|
if (e.target.closest('#ge-shortcuts-close')) return;
|
|
const r = el.getBoundingClientRect();
|
|
drag = { dx: e.clientX - r.left, dy: e.clientY - r.top, w: r.width, h: r.height };
|
|
handle.setPointerCapture(e.pointerId);
|
|
handle.style.cursor = 'grabbing';
|
|
// Mark as user-positioned so subsequent toggles don't re-anchor.
|
|
el.dataset.userPositioned = '1';
|
|
e.preventDefault();
|
|
});
|
|
handle.addEventListener('pointermove', (e) => {
|
|
if (!drag) return;
|
|
let left = e.clientX - drag.dx;
|
|
let top = e.clientY - drag.dy;
|
|
const m = 4;
|
|
left = Math.max(m, Math.min(left, window.innerWidth - drag.w - m));
|
|
top = Math.max(m, Math.min(top, window.innerHeight - drag.h - m));
|
|
el.style.left = left + 'px';
|
|
el.style.top = top + 'px';
|
|
});
|
|
const endDrag = () => {
|
|
if (!drag) return;
|
|
drag = null;
|
|
handle.style.cursor = 'grab';
|
|
try {
|
|
localStorage.setItem('ge-shortcuts-pos', JSON.stringify({
|
|
left: el.style.left, top: el.style.top,
|
|
}));
|
|
} catch {}
|
|
};
|
|
handle.addEventListener('pointerup', endDrag);
|
|
handle.addEventListener('pointercancel', endDrag);
|
|
}
|
|
pop = el;
|
|
return pop;
|
|
}
|
|
|
|
function positionPopover(el, anchor) {
|
|
// Place ABOVE the anchor, horizontally centred but clamped to
|
|
// viewport. Falls back to BELOW if there's no room above.
|
|
el.style.display = 'block'; // need a layout pass for accurate size
|
|
const ar = anchor.getBoundingClientRect();
|
|
const pr = el.getBoundingClientRect();
|
|
const margin = 8;
|
|
let left = ar.left + (ar.width / 2) - (pr.width / 2);
|
|
let top = ar.top - pr.height - margin;
|
|
if (top < margin) top = ar.bottom + margin;
|
|
left = Math.max(margin, Math.min(left, window.innerWidth - pr.width - margin));
|
|
top = Math.max(margin, Math.min(top, window.innerHeight - pr.height - margin));
|
|
el.style.left = left + 'px';
|
|
el.style.top = top + 'px';
|
|
}
|
|
|
|
function toggleShortcuts(show) {
|
|
const el = ensurePopover();
|
|
const open = show === undefined ? el.style.display === 'none' : show;
|
|
if (open) {
|
|
// Restore the user's last-dragged position if any; otherwise
|
|
// anchor above the button.
|
|
let saved = null;
|
|
try { saved = JSON.parse(localStorage.getItem('ge-shortcuts-pos') || 'null'); } catch {}
|
|
if (saved && saved.left && saved.top) {
|
|
el.style.display = 'block';
|
|
el.style.left = saved.left;
|
|
el.style.top = saved.top;
|
|
// Re-clamp in case the viewport changed since the user dragged.
|
|
requestAnimationFrame(() => {
|
|
const r = el.getBoundingClientRect();
|
|
const m = 4;
|
|
if (r.right > window.innerWidth) el.style.left = (window.innerWidth - r.width - m) + 'px';
|
|
if (r.bottom > window.innerHeight) el.style.top = (window.innerHeight - r.height - m) + 'px';
|
|
if (r.left < 0) el.style.left = m + 'px';
|
|
if (r.top < 0) el.style.top = m + 'px';
|
|
});
|
|
} else {
|
|
const anchor = document.getElementById('ge-shortcuts-btn');
|
|
if (anchor) positionPopover(el, anchor);
|
|
else el.style.display = 'block';
|
|
}
|
|
// Defer outside-click so the click that opened us doesn't close us.
|
|
outside = (e) => {
|
|
if (el.contains(e.target)) return;
|
|
if (e.target.closest('#ge-shortcuts-btn')) return;
|
|
toggleShortcuts(false);
|
|
};
|
|
setTimeout(() => document.addEventListener('mousedown', outside, true), 0);
|
|
} else {
|
|
el.style.display = 'none';
|
|
if (outside) {
|
|
document.removeEventListener('mousedown', outside, true);
|
|
outside = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** True when the popover is currently visible. */
|
|
function isOpen() {
|
|
return !!(pop && pop.style.display && pop.style.display !== 'none');
|
|
}
|
|
|
|
return { toggleShortcuts, isOpen };
|
|
}
|