mirror of
https://github.com/amitwh/markdown-converter.git
synced 2026-10-01 09:19:34 +05:30
feat(flowchart): keyboard shortcuts overlay (C19)
After 17 commits of feature work the standalone window had a lot of
hidden keyboard shortcuts (Cmd+Z, Cmd+C/V/D, Delete, Alt+drag, etc.)
that no user could discover. This adds:
- 'Shortcuts (?)' toolbar button next to Export Visio.
- '?' key (or Shift+/) toggles the overlay; Esc closes it.
- A read-only modal that lists every canvas-relevant shortcut in
a 2-column table (Action / Shortcut) with <kbd> tags for the
key combos, styled to look like physical keys.
- Click on the dimmed overlay backdrop also closes it.
- Reuses the existing .fc-modal fade + scale-in animation.
The overlay sits inside #fc-left so it overlays the canvas, not the
side panel. Hidden by default via the HTML hidden attribute.
Implementation: btnHelp / shortcutsModal / shortcutsOverlay /
shortcutsClose are added to the bundle's els bag. A small
showShortcutsOverlay / hideShortcutsOverlay pair toggles their
.hidden. The keydown handler gets two new branches at the top:
- Esc: close the overlay if open.
- ? (computed from ev.key === '?' or 'Shift+/', no modifier
keys held): toggle the overlay.
Both branches return early so the existing Cmd+Z / Cmd+C / etc.
handlers below them still work.
Tests: 3 new (1386 total) covering the toolbar toggle, the
? + Esc keyboard flow, and that every shortcut the user can
trigger is listed in the modal.
Amit Haridas
This commit is contained in:
@@ -256,6 +256,50 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
/* v4.13.0 — keyboard shortcuts modal + kbd tags. The modal
|
||||||
|
reuses the existing .fc-modal animation; the table lays
|
||||||
|
out Action / Shortcut columns, with <kbd> tags for the
|
||||||
|
key combos styled to look like physical keys. */
|
||||||
|
.fc-shortcuts-modal {
|
||||||
|
max-width: 460px;
|
||||||
|
width: 90vw;
|
||||||
|
}
|
||||||
|
.fc-shortcuts-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 12px 0 16px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.fc-shortcuts-table th {
|
||||||
|
text-align: left;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid var(--fc-border, #d1d5db);
|
||||||
|
color: #4b5563;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
.fc-shortcuts-table td {
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
.fc-shortcuts-table td:last-child {
|
||||||
|
text-align: right;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
kbd {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 1px 6px;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, monospace;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border: 1px solid #d1d5db;
|
||||||
|
border-bottom-width: 2px;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 1px;
|
||||||
|
}
|
||||||
/* v4.10.0 — preview-render info card explaining the layout */
|
/* v4.10.0 — preview-render info card explaining the layout */
|
||||||
.fc-preview-render-note {
|
.fc-preview-render-note {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
@@ -394,6 +438,80 @@
|
|||||||
<div class="fc-split">
|
<div class="fc-split">
|
||||||
<div class="fc-left">
|
<div class="fc-left">
|
||||||
<div class="fc-canvas-host" id="canvas-host"></div>
|
<div class="fc-canvas-host" id="canvas-host"></div>
|
||||||
|
<!-- v4.13.0 — keyboard shortcuts overlay. Triggered by the
|
||||||
|
"Shortcuts (?)" toolbar button or the ? key. Hidden by
|
||||||
|
default; .open toggles visibility + the fade-in animation. -->
|
||||||
|
<div class="fc-modal-overlay" id="fc-shortcuts-overlay" hidden></div>
|
||||||
|
<div
|
||||||
|
class="fc-modal fc-shortcuts-modal"
|
||||||
|
id="fc-shortcuts-modal"
|
||||||
|
role="dialog"
|
||||||
|
aria-labelledby="fc-shortcuts-title"
|
||||||
|
hidden
|
||||||
|
>
|
||||||
|
<h2 id="fc-shortcuts-title">Keyboard Shortcuts</h2>
|
||||||
|
<table class="fc-shortcuts-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Action</th>
|
||||||
|
<th>Shortcut</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Undo</td>
|
||||||
|
<td><kbd>Ctrl/⌘</kbd>+<kbd>Z</kbd></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Redo</td>
|
||||||
|
<td><kbd>Ctrl/⌘</kbd>+<kbd>Shift</kbd>+<kbd>Z</kbd></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Copy selection</td>
|
||||||
|
<td><kbd>Ctrl/⌘</kbd>+<kbd>C</kbd></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Paste</td>
|
||||||
|
<td><kbd>Ctrl/⌘</kbd>+<kbd>V</kbd></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Duplicate</td>
|
||||||
|
<td><kbd>Ctrl/⌘</kbd>+<kbd>D</kbd></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Delete selected</td>
|
||||||
|
<td><kbd>Delete</kbd> / <kbd>Backspace</kbd></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Connect from a node</td>
|
||||||
|
<td><kbd>Alt</kbd>+drag</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Pan view</td>
|
||||||
|
<td><kbd>Shift</kbd>+drag on background</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Multi-select</td>
|
||||||
|
<td><kbd>Shift</kbd>+click / drag-rect</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Zoom in / out</td>
|
||||||
|
<td><kbd>Ctrl</kbd>+wheel</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Reset All</td>
|
||||||
|
<td>(toolbar button)</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Show this overlay</td>
|
||||||
|
<td><kbd>?</kbd></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div class="fc-modal-actions">
|
||||||
|
<button class="fc-btn primary" id="fc-shortcuts-close">Close (Esc)</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!--
|
<!--
|
||||||
v4.11.0 — button-driven UI. Every mutation (add/delete node,
|
v4.11.0 — button-driven UI. Every mutation (add/delete node,
|
||||||
add/delete edge, change label, change kind) goes through the
|
add/delete edge, change label, change kind) goes through the
|
||||||
@@ -552,6 +670,13 @@
|
|||||||
>
|
>
|
||||||
Export Visio
|
Export Visio
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="fc-btn"
|
||||||
|
id="fc-btn-help"
|
||||||
|
title="Show keyboard shortcuts (press ? any time)"
|
||||||
|
>
|
||||||
|
Shortcuts (?)
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<span id="fc-status" aria-live="polite"></span>
|
<span id="fc-status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1212,6 +1212,10 @@
|
|||||||
btnDistributeH: document.getElementById('fc-btn-distribute-h'),
|
btnDistributeH: document.getElementById('fc-btn-distribute-h'),
|
||||||
btnDistributeV: document.getElementById('fc-btn-distribute-v'),
|
btnDistributeV: document.getElementById('fc-btn-distribute-v'),
|
||||||
btnSelectAll: document.getElementById('fc-btn-select-all'),
|
btnSelectAll: document.getElementById('fc-btn-select-all'),
|
||||||
|
btnHelp: document.getElementById('fc-btn-help'),
|
||||||
|
shortcutsModal: document.getElementById('fc-shortcuts-modal'),
|
||||||
|
shortcutsOverlay: document.getElementById('fc-shortcuts-overlay'),
|
||||||
|
shortcutsClose: document.getElementById('fc-shortcuts-close'),
|
||||||
btnUndo: document.getElementById('fc-btn-undo'),
|
btnUndo: document.getElementById('fc-btn-undo'),
|
||||||
btnRedo: document.getElementById('fc-btn-redo'),
|
btnRedo: document.getElementById('fc-btn-redo'),
|
||||||
historyCount: document.getElementById('fc-history-count'),
|
historyCount: document.getElementById('fc-history-count'),
|
||||||
@@ -2069,6 +2073,24 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v4.13.0 — Keyboard shortcuts overlay. Triggered by the toolbar
|
||||||
|
// button or the ? key. Esc closes. Wired as a separate modal (not
|
||||||
|
// via promptInline / confirmInline) because those are single-input
|
||||||
|
// forms; this one is a read-only table.
|
||||||
|
function showShortcutsOverlay() {
|
||||||
|
if (els.shortcutsModal) els.shortcutsModal.hidden = false;
|
||||||
|
if (els.shortcutsOverlay) els.shortcutsOverlay.hidden = false;
|
||||||
|
}
|
||||||
|
function hideShortcutsOverlay() {
|
||||||
|
if (els.shortcutsModal) els.shortcutsModal.hidden = true;
|
||||||
|
if (els.shortcutsOverlay) els.shortcutsOverlay.hidden = true;
|
||||||
|
}
|
||||||
|
if (els.btnHelp) els.btnHelp.addEventListener('click', showShortcutsOverlay);
|
||||||
|
if (els.shortcutsClose) els.shortcutsClose.addEventListener('click', hideShortcutsOverlay);
|
||||||
|
if (els.shortcutsOverlay) {
|
||||||
|
els.shortcutsOverlay.addEventListener('click', hideShortcutsOverlay);
|
||||||
|
}
|
||||||
|
|
||||||
// v4.13.0 — Open from .mmd/.md file. Pops a system Open dialog,
|
// v4.13.0 — Open from .mmd/.md file. Pops a system Open dialog,
|
||||||
// strips the ```mermaid fence (if any), parses via fromMermaid()
|
// strips the ```mermaid fence (if any), parses via fromMermaid()
|
||||||
// (inlined above) and replaces the current graph. Confirms
|
// (inlined above) and replaces the current graph. Confirms
|
||||||
@@ -2150,10 +2172,29 @@
|
|||||||
// Keyboard shortcuts — Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z. v4.13.0 — added
|
// Keyboard shortcuts — Ctrl/Cmd+Z / Ctrl/Cmd+Shift+Z. v4.13.0 — added
|
||||||
// copy/paste/duplicate (Cmd+C / Cmd+V / Cmd+D) plus Delete/Backspace
|
// copy/paste/duplicate (Cmd+C / Cmd+V / Cmd+D) plus Delete/Backspace
|
||||||
// for the canvas-owned selection (replaces the old DOM .selected read).
|
// for the canvas-owned selection (replaces the old DOM .selected read).
|
||||||
|
// v4.13.0 — ? opens the shortcuts overlay; Esc closes it.
|
||||||
let _clipboard = null; // module-level clipboard for Cmd+C/V/D
|
let _clipboard = null; // module-level clipboard for Cmd+C/V/D
|
||||||
document.addEventListener('keydown', (ev) => {
|
document.addEventListener('keydown', (ev) => {
|
||||||
|
// Esc closes the shortcuts overlay regardless of meta state.
|
||||||
|
if (ev.key === 'Escape' && els.shortcutsModal && !els.shortcutsModal.hidden) {
|
||||||
|
hideShortcutsOverlay();
|
||||||
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// ? toggles the overlay. Use Shift+/ since ? requires Shift on US
|
||||||
|
// keyboards; this catches both the literal character and the key.
|
||||||
|
const isMeta = ev.ctrlKey || ev.metaKey;
|
||||||
|
if ((ev.key === '?' || (ev.key === '/' && ev.shiftKey)) && !isMeta && !ev.altKey) {
|
||||||
|
if (els.shortcutsModal && els.shortcutsModal.hidden) {
|
||||||
|
showShortcutsOverlay();
|
||||||
|
} else {
|
||||||
|
hideShortcutsOverlay();
|
||||||
|
}
|
||||||
|
ev.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_store) return;
|
if (!_store) return;
|
||||||
const meta = ev.ctrlKey || ev.metaKey;
|
const meta = isMeta;
|
||||||
if (meta && !ev.shiftKey && ev.key.toLowerCase() === 'z') {
|
if (meta && !ev.shiftKey && ev.key.toLowerCase() === 'z') {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
_store.undo();
|
_store.undo();
|
||||||
|
|||||||
@@ -744,4 +744,51 @@ describe('flowchart-bundle: Export Image buttons (v4.13.0)', () => {
|
|||||||
expect(apiMock.saveFile).toHaveBeenCalledTimes(1);
|
expect(apiMock.saveFile).toHaveBeenCalledTimes(1);
|
||||||
expect(document.getElementById('fc-status').textContent).toBe('Export cancelled');
|
expect(document.getElementById('fc-status').textContent).toBe('Export cancelled');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// v4.13.0 — keyboard shortcuts overlay. Triggered by the toolbar
|
||||||
|
// button or the ? key, dismissed by the Close button or Esc.
|
||||||
|
describe('shortcuts overlay (v4.13.0)', () => {
|
||||||
|
test('toolbar button toggles the overlay open', async () => {
|
||||||
|
await loadBundle();
|
||||||
|
const modal = document.getElementById('fc-shortcuts-modal');
|
||||||
|
expect(modal.hidden).toBe(true);
|
||||||
|
document.getElementById('fc-btn-help').click();
|
||||||
|
expect(modal.hidden).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('? key opens the overlay, Esc closes it', async () => {
|
||||||
|
await loadBundle();
|
||||||
|
const modal = document.getElementById('fc-shortcuts-modal');
|
||||||
|
expect(modal.hidden).toBe(true);
|
||||||
|
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: '?' }));
|
||||||
|
expect(modal.hidden).toBe(false);
|
||||||
|
|
||||||
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }));
|
||||||
|
expect(modal.hidden).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('overlay lists the canvas-relevant shortcuts', async () => {
|
||||||
|
await loadBundle();
|
||||||
|
document.getElementById('fc-btn-help').click();
|
||||||
|
const table = document.querySelector('#fc-shortcuts-modal .fc-shortcuts-table');
|
||||||
|
expect(table).not.toBeNull();
|
||||||
|
const rows = Array.from(table.querySelectorAll('tbody tr')).map(
|
||||||
|
(tr) => tr.children[0].textContent
|
||||||
|
);
|
||||||
|
expect(rows).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
'Undo',
|
||||||
|
'Redo',
|
||||||
|
'Copy selection',
|
||||||
|
'Paste',
|
||||||
|
'Duplicate',
|
||||||
|
'Delete selected',
|
||||||
|
'Connect from a node',
|
||||||
|
'Multi-select',
|
||||||
|
'Show this overlay',
|
||||||
|
])
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user