Reclaim Space for play/stop; narrow the keyboard focus guard
The real conflict wasn't the key: shortcuts bailed whenever ANY form control (slider/checkbox/menu/button) had focus, and those keep focus after use — so P/T/A/N seemed dead most of the time. - Guard now stands down only for text-entry fields (text/number/textarea/ contenteditable), so shortcuts work right after you touch a slider or checkbox. - Space always = play/stop (preventDefault so it won't scroll, toggle a focused checkbox, or re-fire a focused button) — the DAW standard, which also fixes the original Space/checkbox conflict. - Arrow keys still defer to a focused range slider / select. - Legend, help overlay, README updated back to Space. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9cb7c2c193
commit
c2a88e5014
2 changed files with 20 additions and 17 deletions
|
|
@ -105,7 +105,7 @@ In the set‑list panel's **⋯** menu:
|
|||
|
||||
| Key | Action |
|
||||
|-----|--------|
|
||||
| `P` | play / stop |
|
||||
| `Space` | play / stop (except while typing in a text field) |
|
||||
| `T` | tap tempo |
|
||||
| `↑` / `↓` | tempo ±1 (`Shift` = ±10) |
|
||||
| `A` | add meter lane |
|
||||
|
|
|
|||
35
index.html
35
index.html
|
|
@ -192,7 +192,7 @@
|
|||
<div class="row" style="align-items:baseline; justify-content:space-between; gap:14px; margin-bottom:12px">
|
||||
<h1 style="margin:0">Stackable Metronome <span class="lane-meta" id="appVersion" title="build version">v0.0.1-dev</span></h1>
|
||||
<div style="display:flex; align-items:center; gap:10px">
|
||||
<span class="kbd-legend">P play · T tap · ↑↓ tempo (⇧×10) · A add · N next · ? help</span>
|
||||
<span class="kbd-legend">Space play · T tap · ↑↓ tempo (⇧×10) · A add · N next · ? help</span>
|
||||
<button id="themeBtn" title="toggle light / dark theme">☀</button>
|
||||
<button id="helpBtn" title="keyboard shortcuts (?)">?</button>
|
||||
</div>
|
||||
|
|
@ -314,7 +314,7 @@
|
|||
<div class="overlay-box">
|
||||
<div class="tray-head"><h2 style="margin:0">Keyboard shortcuts</h2><button class="x" id="shortcutsClose" style="margin-left:0">✕</button></div>
|
||||
<table class="kbd-table">
|
||||
<tr><td><kbd>P</kbd></td><td>Play / stop</td></tr>
|
||||
<tr><td><kbd>Space</kbd></td><td>Play / stop (works everywhere except while typing in a text field)</td></tr>
|
||||
<tr><td><kbd>T</kbd></td><td>Tap tempo</td></tr>
|
||||
<tr><td><kbd>↑</kbd> <kbd>↓</kbd></td><td>Tempo ±1 BPM</td></tr>
|
||||
<tr><td><kbd>⇧↑</kbd> <kbd>⇧↓</kbd></td><td>Tempo ±10 BPM</td></tr>
|
||||
|
|
@ -1191,23 +1191,26 @@ $("shareQrExt").addEventListener("click", () => {
|
|||
window.open("https://api.qrserver.com/v1/create-qr-code/?size=320x320&data=" + encodeURIComponent($("shareUrl").value), "_blank", "noopener");
|
||||
});
|
||||
window.addEventListener("keydown", (e) => {
|
||||
const t = e.target;
|
||||
if (t && (t.tagName === "INPUT" || t.tagName === "SELECT" || t.tagName === "TEXTAREA" || t.isContentEditable)) return;
|
||||
const t = e.target, tag = t ? t.tagName : "", type = (t && t.type ? String(t.type) : "").toLowerCase();
|
||||
// Text entry is sacred — never hijack typing in a text field.
|
||||
if (t && (t.isContentEditable || tag === "TEXTAREA" ||
|
||||
(tag === "INPUT" && /^(text|number|search|email|url|tel|password)$/.test(type)))) return;
|
||||
const k = e.key;
|
||||
if (e.altKey && (k === "ArrowUp" || k === "ArrowDown")) { e.preventDefault(); moveActiveItem(k === "ArrowUp" ? -1 : 1); return; } // reorder selected item
|
||||
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
||||
if (k === "p" || k === "P") { e.preventDefault(); toggleTransport(); }
|
||||
else if (k === "t" || k === "T") { tapTempo(); }
|
||||
else if (k === "ArrowUp") { e.preventDefault(); setBpm(state.bpm + (e.shiftKey ? 10 : 1)); }
|
||||
else if (k === "ArrowDown") { e.preventDefault(); setBpm(state.bpm - (e.shiftKey ? 10 : 1)); }
|
||||
else if (k === "a" || k === "A") { addMeter("4", 1, "claves"); }
|
||||
else if (k === "n" || k === "N") { nextItem(); }
|
||||
else if (k === "?") { toggleShortcuts(true); }
|
||||
else if (k === "Escape") { if (!$("shareOverlay").hidden) $("shareOverlay").hidden = true; else if (!$("shortcutsOverlay").hidden) toggleShortcuts(false); }
|
||||
else if (k >= "1" && k <= "9") {
|
||||
const m = meters[+k - 1];
|
||||
if (m) setLaneEnabled(m, !m.enabled);
|
||||
}
|
||||
// Transport: Space always = play/stop. preventDefault so it never scrolls the
|
||||
// page, toggles a focused checkbox, or re-fires a focused button.
|
||||
if (k === " " || e.code === "Space") { e.preventDefault(); toggleTransport(); return; }
|
||||
// Leave arrow keys to a focused slider / menu so they still adjust it.
|
||||
const arrowCtrl = tag === "SELECT" || (tag === "INPUT" && type === "range");
|
||||
if (k === "ArrowUp") { if (arrowCtrl) return; e.preventDefault(); setBpm(state.bpm + (e.shiftKey ? 10 : 1)); return; }
|
||||
if (k === "ArrowDown") { if (arrowCtrl) return; e.preventDefault(); setBpm(state.bpm - (e.shiftKey ? 10 : 1)); return; }
|
||||
if (k === "t" || k === "T") { tapTempo(); return; }
|
||||
if (k === "a" || k === "A") { addMeter("4", 1, "claves"); return; }
|
||||
if (k === "n" || k === "N") { nextItem(); return; }
|
||||
if (k === "?") { toggleShortcuts(true); return; }
|
||||
if (k === "Escape") { if (!$("shareOverlay").hidden) $("shareOverlay").hidden = true; else if (!$("shortcutsOverlay").hidden) toggleShortcuts(false); return; }
|
||||
if (k >= "1" && k <= "9") { const m = meters[+k - 1]; if (m) setLaneEnabled(m, !m.enabled); }
|
||||
});
|
||||
|
||||
/* init */
|
||||
|
|
|
|||
Loading…
Reference in a new issue