diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d59aa8..19080ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -498,6 +498,12 @@ Foundational work: faster reviews, narrower diffs, and a safety net under the pa * Opening **Now Playing** (sidebar, mobile route, or queue info panel) switches to the queue server before Subsonic metadata loads, with per-server fetch caches so artist/album cards do not show the wrong library. * **Scrobble**, **now-playing report**, and **savePlayQueue** sync use the queue server as well; removing that server profile clears `queueServerId`; the mini-player bridge receives `queueServerId`; enqueue/play-next from another browsed server is blocked with a toast. +### UI — selectstart blocker no longer throws on Text node targets + +**By [@cucadmuh](https://github.com/cucadmuh), PR [#718](https://github.com/Psychotoxical/psysonic/pull/718)** + +* The global **`selectstart`** handler assumed `event.target` was always an **`Element`**. Starting selection inside text (e.g. Now Playing **`[data-selectable]`** copy) could pass a **Text** node and crash with **`target.closest is not a function`**. The handler now resolves Text nodes to their parent element before applying allow/deny rules. + ## [1.45.0] - 2026-05-04 ## Added diff --git a/src/hooks/useGlobalDndAndSelectionBlockers.ts b/src/hooks/useGlobalDndAndSelectionBlockers.ts index 56df62e9..b5255cde 100644 --- a/src/hooks/useGlobalDndAndSelectionBlockers.ts +++ b/src/hooks/useGlobalDndAndSelectionBlockers.ts @@ -1,5 +1,20 @@ import { useEffect } from 'react'; +/** `Event.target` may be a Text node — only Elements expose `closest` / `tagName`. */ +function eventTargetElement(e: Event): Element | null { + const t = e.target; + if (!t) return null; + if (t instanceof Element) return t; + if (!(t instanceof Node)) return null; + const parent = t.parentNode; + return parent instanceof Element ? parent : null; +} + +function isTextInputElement(el: Element): boolean { + const tag = el.tagName; + return tag === 'INPUT' || tag === 'TEXTAREA' || (el as HTMLElement).isContentEditable; +} + /** * Globally tame Linux/WebKitGTK + Wayland behaviour that the page-level * CSS / Tauri config can't reach: @@ -29,16 +44,20 @@ export function useGlobalDndAndSelectionBlockers(): void { const blockSelectAll = (e: KeyboardEvent) => { if ((e.ctrlKey || e.metaKey) && e.key === 'a') { - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return; + const el = eventTargetElement(e); + if (el && isTextInputElement(el)) return; e.preventDefault(); } }; const blockSelectStart = (e: Event) => { - const target = e.target as HTMLElement; - if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable) return; - if ((target as HTMLElement).closest('[data-selectable]')) return; + const el = eventTargetElement(e); + if (!el) { + e.preventDefault(); + return; + } + if (isTextInputElement(el)) return; + if (el.closest('[data-selectable]')) return; e.preventDefault(); };