aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/static/menu.js
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static/menu.js')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/menu.js41
1 files changed, 33 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/menu.js b/packages/meshbay-hub/src/meshbay_hub/static/menu.js
index 4aa08ca..1c6a96a 100644
--- a/packages/meshbay-hub/src/meshbay_hub/static/menu.js
+++ b/packages/meshbay-hub/src/meshbay_hub/static/menu.js
@@ -19,9 +19,15 @@ import { Icon } from './icon.js';
* downward needs no flipping, no hover intent, and no separate mobile design.
*
* `items` is a flat list, each entry one of:
- * { label, icon?, onSelect } an action
- * { label, icon?, items, empty? } a submenu, expanded in place
+ * { label, icon?, onSelect } an action
+ * { label, icon?, items, empty? } a submenu, expanded in place
+ * { label, icon?, loadItems, empty? } the same, fetched when expanded
* { divider: true }
+ *
+ * `loadItems` exists for the one submenu whose contents are not already in
+ * hand: a playlist's tracklist, which is read out of IndexedDB. Reading every
+ * playlist's tracks to build a menu nobody may open would mean a ten-thousand
+ * track list read on every click of the button.
*/
// Kept away from the viewport edges; `.ctx-menu` sets the width this assumes.
@@ -54,25 +60,44 @@ function useMenu() {
function MenuItems({ items, depth, onClose }) {
const [expanded, setExpanded] = useState(null);
+ // index -> the items that came back, or 'loading'.
+ const [loaded, setLoaded] = useState({});
+
+ const toggle = useCallback((i, it) => {
+ if (expanded === i) { setExpanded(null); return; }
+ setExpanded(i);
+ if (!it.loadItems || loaded[i] !== undefined) return;
+ setLoaded((prev) => ({ ...prev, [i]: 'loading' }));
+ Promise.resolve(it.loadItems())
+ .then((rows) => setLoaded((prev) => ({ ...prev, [i]: rows || [] })))
+ // A submenu that cannot be filled renders as empty rather than as a
+ // spinner nothing will ever replace.
+ .catch(() => setLoaded((prev) => ({ ...prev, [i]: [] })));
+ }, [expanded, loaded]);
return html`
${items.map((it, i) => {
if (it.divider) return html`<div class="ctx-menu-divider" key=${`d${i}`}></div>`;
- if (it.items) {
+ if (it.items || it.loadItems) {
const open = expanded === i;
+ const rows = it.items || loaded[i];
+ const pending = rows === 'loading' || (it.loadItems && rows === undefined);
return html`
<button class="ctx-menu-item" key=${it.key || it.label}
style=${depth ? `padding-left: ${14 + depth * 14}px` : null}
- onClick=${(e) => { e.stopPropagation(); setExpanded(open ? null : i); }}>
+ onClick=${(e) => { e.stopPropagation(); toggle(i, it); }}>
${it.icon && html`<${Icon} name=${it.icon} cls="ctx-menu-icon" />`}
<span class="ctx-menu-label">${it.label}</span>
<${Icon} name="chevron" cls="ctx-menu-caret ${open ? 'flip' : ''}" />
</button>
- ${open && (it.items.length
- ? html`<${MenuItems} items=${it.items} depth=${depth + 1} onClose=${onClose} />`
- : html`<div class="ctx-menu-empty"
- style=${`padding-left: ${28 + depth * 14}px`}>${it.empty || ''}</div>`)}
+ ${open && (pending
+ ? html`<div class="ctx-menu-empty"
+ style=${`padding-left: ${28 + depth * 14}px`}><span class="spinner"></span></div>`
+ : (rows && rows.length
+ ? html`<${MenuItems} items=${rows} depth=${depth + 1} onClose=${onClose} />`
+ : html`<div class="ctx-menu-empty"
+ style=${`padding-left: ${28 + depth * 14}px`}>${it.empty || ''}</div>`))}
`;
}