dx: better typing for domOn
Massimo Melina committed
Jan 5, 2026 at 12:04 UTC
ee0cee87e0cacd329721ded617419c864aa18aff
3 files changed
+32
-7
frontend/src/show.ts
+1
-1
@@ -116,7 +116,7 @@ export function fileShow(entry: DirEntry, { startPlaying=false, startShuffle=fal
116
}
117
if (showElement instanceof HTMLMediaElement) {
118
showElement.play().catch(playFailed)
119
- return domOn('ended', goNext, { target: showElement as any })
119
+ return domOn('ended', goNext, { target: showElement })
120
}
121
if (!autoPlaying) return // we reached here because of the justOpen, but we are not interested in images
122
// we are supposedly showing an image
shared/index.ts
+27
-3
@@ -56,10 +56,33 @@ export function buildUrlQueryString(params: Dict) { // not using URLSearchParams
56
return '?' + Object.entries(params).filter(pair => pair[1] !== undefined).map(pair => pair.map(x => encodeURIComponent(x).replaceAll('%2F','/')).join('=') ).join('&')
57
}
58
59
-export function domOn<K extends keyof WindowEventMap>(eventName: K, cb: (ev: WindowEventMap[K]) => void, { target=window }={}) {
59
+type DomOnEventMap<T> =
60
+ T extends Window ? WindowEventMap :
61
+ T extends Document ? DocumentEventMap :
62
+ T extends HTMLMediaElement ? HTMLMediaElementEventMap :
63
+ T extends HTMLElement ? HTMLElementEventMap :
64
+ T extends SVGElement ? SVGElementEventMap :
65
+ T extends Element ? ElementEventMap :
66
+ T extends MediaQueryList ? MediaQueryListEventMap :
67
+ T extends EventTarget ? Record<string, Event> :
68
+ never
69
+
70
+type DomOnTarget<O> = O extends { target?: infer T } ? T : Window
71
+type DomOnEventMapFor<O> = DomOnEventMap<DomOnTarget<O>>
72
+
73
+// default target is `window`
74
+export function domOn<
75
+ O extends boolean | undefined | { target?: EventTarget } & AddEventListenerOptions = undefined,
76
+ K extends keyof DomOnEventMapFor<O> & string = keyof DomOnEventMapFor<O> & string
77
+>(
78
+ eventName: K,
79
+ cb: (ev: DomOnEventMapFor<O>[K]) => void,
80
+ options?: O
81
+) {
82
+ const target = options && 'target' in options ? options.target : window
83
if (!target) return
61
- target.addEventListener(eventName, cb)
62
- return () => target.removeEventListener(eventName, cb)
84
+ target.addEventListener(eventName, cb as EventListener, options)
85
+ return () => target.removeEventListener(eventName, cb as EventListener, options)
86
}
87
88
export function restartAnimation(e: HTMLElement | null | undefined, animation: string) {
@@ -182,6 +205,7 @@ export function createDurationFormatter({ locale=undefined, unitDisplay='narrow'
205
}
206
207
export async function copyTextToClipboard(text: string) {
208
+ text = String(text)
209
try {
210
await navigator.clipboard.writeText(text) // this method works only in https and localhost
211
}
shared/react.ts
+4
-3
@@ -210,7 +210,7 @@ export function isCtrlKey(ev: KeyboardEvent) {
210
}
211
212
export function useAutoScroll(dependency: any) {
213
- const ref = useRef<any>()
213
+ const ref = useRef<HTMLElement | null>(null)
214
const lastScrollListenerRef = useRef<any>()
215
const [goBottom, setGoBottom] = useState(true)
216
useEffect(() => {
@@ -218,12 +218,13 @@ export function useAutoScroll(dependency: any) {
218
if (goBottom)
219
el?.scrollTo(0, el.scrollHeight)
220
}, [goBottom, dependency])
221
- return useCallback((el: any) => {
221
+ return useCallback((el: HTMLElement | null) => {
222
ref.current = el
223
// reinstall listener
224
lastScrollListenerRef.current?.()
225
+ if (!el) return
226
lastScrollListenerRef.current = domOn('scroll', ev => {
226
- const el = ev.target as HTMLDivElement
227
+ const el = ev.target as HTMLElement
228
if (!el) return
229
setGoBottom(el.scrollTop + el.clientHeight >= el.scrollHeight - 3)
230
}, { target: el })