mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-11 19:29:33 +00:00
v3 library: native right-click context menu on cards (#688)
* feat(v3): native right-click context menu on library cards Right-clicking a song card now opens the same overflow (more) menu at the pointer, so the actions (Play, Add to playlist, Save for later, and any plugin card actions) are reachable without aiming for the small button. openCardMenu gains an optional pointer position (fixed + viewport-clamped when opened via right-click, the existing card-anchored absolute otherwise). Adds a "Save for later" row to the menu too, closing the gap where Save was only the inline button. Shares one menu definition + the libraryCardActions registry, so plugin actions appear in both paths. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QbexxfTt8q2tAn436MqGWF * fix(v3): valid JS emoji escape in card-menu toast + dismiss orphaned menu on re-render (PR #688 review) Replace the Python-style `\U0001f3b5` escape (invalid in JS, rendered literal text) with the 🎵 emoji character in the playlists-updated toast. Also tear down any open card context menu at the start of render(), the full filter/scan-refresh path: the right-click menu is appended to document.body, so replacing root.innerHTML would otherwise leave it floating orphaned until the next document click. Reuses _closeCardMenu. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
byrongamatos
parent
80caf78306
commit
fee85a14e7
+23
-4
@@ -833,7 +833,7 @@
|
||||
// Per-card action menu, built from the ui.library-card-injection registry
|
||||
// (core Edit/Retune + any plugin-registered actions).
|
||||
let _closeCardMenu = null; // tears down the currently-open card menu + its document closer
|
||||
function openCardMenu(cardEl, song, anchorBtn) {
|
||||
function openCardMenu(cardEl, song, anchorBtn, pos) {
|
||||
// Fully close any already-open menu first — removing just the DOM node
|
||||
// (as before) would orphan its document-level click closer.
|
||||
if (_closeCardMenu) _closeCardMenu();
|
||||
@@ -843,7 +843,9 @@
|
||||
// Undefined placement defaults to the menu.
|
||||
const items = (reg ? reg.list(song) : []).filter((a) => !a.placement || a.placement === 'menu');
|
||||
const menu = document.createElement('div');
|
||||
menu.className = 'v3-card-menu absolute top-10 right-2 z-30 min-w-[10rem] bg-fb-card border border-fb-border/60 rounded-lg shadow-xl py-1 text-sm';
|
||||
// pos (right-click) positions the menu at the pointer via `fixed`;
|
||||
// the ⋮ button keeps the absolute top-right anchor.
|
||||
menu.className = 'v3-card-menu z-30 min-w-[10rem] bg-fb-card border border-fb-border/60 rounded-lg shadow-xl py-1 text-sm ' + (pos ? 'fixed' : 'absolute top-10 right-2');
|
||||
// Multi-chart entries (P5d): a grouped grid row carries chart_count +
|
||||
// work_key (P5a annotation), so the menu knows inline whether this card
|
||||
// stands for versions. Local library only — the work API is local.
|
||||
@@ -862,6 +864,7 @@
|
||||
...(state.provider === 'local' && song.is_split
|
||||
? [{ id: '__unsplit', label: 'Rejoin other versions' }] : []),
|
||||
{ id: '__playlist', label: 'Add to playlist' },
|
||||
{ id: '__save', label: 'Save for later' },
|
||||
...items.map((a) => ({ id: a.id, label: a.label, destructive: a.destructive, enabled: a.enabled, plugin: a.pluginId })),
|
||||
];
|
||||
menu.innerHTML = rows.map((r) =>
|
||||
@@ -869,7 +872,17 @@
|
||||
(r.enabled === false ? 'opacity-40 cursor-not-allowed ' : '') +
|
||||
(r.destructive ? 'text-fb-accent' : 'text-fb-text') + '">' + esc(r.label) +
|
||||
(r.plugin && r.plugin !== 'core' ? '<span class="text-[10px] text-fb-textDim ml-1">' + esc(r.plugin) + '</span>' : '') + '</button>').join('');
|
||||
cardEl.appendChild(menu);
|
||||
if (pos) {
|
||||
// Right-click: position the menu at the pointer (fixed, viewport-
|
||||
// relative), clamped so it never spills off the right/bottom edge.
|
||||
document.body.appendChild(menu);
|
||||
const x = Math.min(pos.x, window.innerWidth - menu.offsetWidth - 8);
|
||||
const y = Math.min(pos.y, window.innerHeight - menu.offsetHeight - 8);
|
||||
menu.style.left = Math.max(8, x) + 'px';
|
||||
menu.style.top = Math.max(8, y) + 'px';
|
||||
} else {
|
||||
cardEl.appendChild(menu);
|
||||
}
|
||||
// Tear down BOTH the menu and its document-level closer together, so a
|
||||
// menu-item click doesn't leave the closer attached (it would otherwise
|
||||
// leak, retaining this menu's closures until the next document click).
|
||||
@@ -890,6 +903,7 @@
|
||||
return;
|
||||
}
|
||||
if (id === '__playlist') { await addFilenamesToPlaylist([song.filename]); return; }
|
||||
if (id === '__save') { if (window.v3Saved) await window.v3Saved.toggle(song.filename); return; }
|
||||
if (reg) await reg.run(id, song, { source: 'v3-songs' });
|
||||
}));
|
||||
// Tree rows ride the (ungrouped) artists endpoint, so they don't carry
|
||||
@@ -1169,6 +1183,8 @@
|
||||
playCard(playTarget); // local → play; unsynced remote → sync then play
|
||||
}));
|
||||
el.querySelector('[data-menu]')?.addEventListener('click', (e) => { e.stopPropagation(); openCardMenu(el, song, e.currentTarget); });
|
||||
// Native right-click opens the same overflow menu at the pointer.
|
||||
el.addEventListener('contextmenu', (e) => { e.preventDefault(); openCardMenu(el, song, null, { x: e.clientX, y: e.clientY }); });
|
||||
el.querySelectorAll('[data-arr]').forEach((ab) => ab.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const idx = ab.getAttribute('data-arr');
|
||||
@@ -1341,7 +1357,7 @@
|
||||
}
|
||||
if (window.v3Playlists) { try { window.v3Playlists.refresh(); } catch (e) { /* */ } }
|
||||
if (acts.length && window.fbNotify) {
|
||||
try { window.fbNotify.show({ title: 'Playlists updated', message: 'Updated ' + acts.length + ' playlist' + (acts.length === 1 ? '' : 's'), icon: '\U0001f3b5' }); } catch (e) { /* */ }
|
||||
try { window.fbNotify.show({ title: 'Playlists updated', message: 'Updated ' + acts.length + ' playlist' + (acts.length === 1 ? '' : 's'), icon: '🎵' }); } catch (e) { /* */ }
|
||||
}
|
||||
done(acts.length ? true : null);
|
||||
}
|
||||
@@ -2538,6 +2554,9 @@
|
||||
async function render() {
|
||||
const root = document.getElementById('v3-songs');
|
||||
if (!root) return;
|
||||
// A full re-render (filter/scan refresh) replaces the DOM the card menu
|
||||
// anchored to; dismiss any open menu first so it can't float orphaned.
|
||||
if (_closeCardMenu) _closeCardMenu();
|
||||
// Restore last-used sort/format/view/filters once, before building the
|
||||
// toolbar so its selects reflect the saved choice (default: Artist A–Z).
|
||||
if (!_prefsRestored) { applySavedPrefs(); _prefsRestored = true; }
|
||||
|
||||
Reference in New Issue
Block a user