-
-
Notifications
You must be signed in to change notification settings - Fork 243
/
Copy pathreact.ts
172 lines (156 loc) · 5.89 KB
/
react.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
// This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
import {
createElement as h, Fragment, KeyboardEvent, MutableRefObject, ReactElement, ReactNode,
useCallback, useEffect, useMemo, useRef, useState
} from 'react'
import { useIsMounted, useWindowSize, useMediaQuery } from 'usehooks-ts'
import { Callback, Falsy } from '.'
import _ from 'lodash'
export function useStateMounted<T>(init: T) {
const isMounted = useIsMounted()
const [v, set] = useState(init)
const ref = useRef(init)
ref.current = v
const setIfMounted = useCallback((newValue:T | ((previous:T)=>T)) => {
if (isMounted())
set(newValue)
}, [isMounted, set])
return [v, setIfMounted, { isMounted, get: () => ref.current }] as const
}
export function reactFilter(elements: any[]) {
return elements.filter(x=> x===0 || x && (!Array.isArray(x) || x.length))
}
export function reactJoin(joiner: string | ReactElement, elements: Parameters<typeof reactFilter>[0]) {
const ret = []
for (const x of reactFilter(elements))
ret.push(x, joiner)
ret.splice(-1,1)
return dontBotherWithKeys(ret)
}
export function dontBotherWithKeys(elements: ReactNode[]): (ReactNode|string)[] {
return elements.map((e,i)=>
!e || typeof e === 'string' ? e
: Array.isArray(e) ? dontBotherWithKeys(e)
: h(Fragment, { key:i, children:e }) )
}
export function useRequestRender() {
const [state, setState] = useState(0)
return Object.assign(useCallback(() => setState(x => x + 1), [setState]), { state })
}
/* the idea is that you need a job done by a worker, but the worker will execute only after it collected jobs for some time
by other "users" of the same worker, like other instances of the same component, but potentially also different components.
User of this hook will just be returned with the single result of its own job.
As an additional feature, results are cached. You can clear the cache by calling cache.clear()
*/
export function useBatch<Job=unknown,Result=unknown>(
worker: Falsy | ((jobs: Job[]) => Promise<Result[]>),
job: undefined | Job,
{ delay=0 }={}
) {
interface Env {
batch: Set<Job>
cache: Map<Job, Result | null>
waiter?: Promise<void>
}
const worker2env = (useBatch as any).worker2env ||= worker && new Map<typeof worker, Env>()
const env = worker2env && (worker2env.get(worker) || (() => {
const ret = { batch: new Set<Job>(), cache: new Map<Job, Result>() } as Env
worker2env.set(worker, ret)
return ret
})())
const requestRender = useRequestRender()
useEffect(() => {
worker && (env.waiter ||= new Promise<void>(resolve => {
setTimeout(async () => {
if (!env.batch.size)
return resolve()
const jobs = [...env.batch.values()]
env.batch.clear()
const res = await worker(jobs)
jobs.forEach((job, i) =>
env.cache.set(job, res[i] ?? null) )
env.waiter = undefined
resolve()
}, delay)
})).then(requestRender)
}, [worker])
const cached = env && env.cache.get(job) // don't use ?. as env can be falsy
if (env && cached === undefined)
env.batch.add(job)
return { data: cached, ...env } as Env & { data: Result | undefined | null } // so you can cache.clear
}
export function KeepInScreen({ margin, ...props }: any) {
const ref = useRef<HTMLDivElement>()
const [maxHeight, setMaxHeight] = useState<undefined | number>()
const size = useWindowSize()
useEffect(() => {
const el = ref.current
if (!el) return
const rect = el.getBoundingClientRect()
const doc = document.documentElement
const limit = window.innerHeight || doc.clientHeight
setMaxHeight(limit - rect?.top - margin)
}, [size])
return h('div', { ref, style: { maxHeight, overflow: 'auto' }, ...props })
}
export function useIsMobile() {
return useMediaQuery('(pointer:coarse)')
}
// calls back with [width, height]
export function useOnResize(cb: Callback<[number, number]>) {
const observer = useMemo(() =>
new ResizeObserver(_.debounce(([{contentRect: r}]) => cb([r.width, r.height]), 10)),
[])
return useMemo(() => ({
ref(el: any) {
observer.disconnect()
if (el)
observer.observe(el)
}
}), [observer])
}
export function useGetSize() {
const [size, setSize] = useState<[number,number]>()
const ref = useRef<HTMLElement>()
const props = useOnResize(setSize)
const propsRef = useCallback((el: any) => passRef(el, ref, props.ref), [props])
return useMemo(() => ({
w: size?.[0],
h: size?.[1],
ref,
props: {
...props,
ref: propsRef
}
}), [size, ref, propsRef])
}
export function useEffectOnce(cb: Callback, deps: any[]) {
const state = useRef<any>()
useEffect(() => {
if (_.isEqual(deps, state.current)) return
state.current = deps
cb(...deps)
}, deps)
}
type FunctionRef<T=HTMLElement> = (instance: (T | null)) => void
export function passRef<T=any>(el: T, ...refs: (MutableRefObject<T> | FunctionRef<T>)[]) {
for (const ref of refs)
if (_.isFunction(ref))
ref(el)
else if (ref)
ref.current = el
}
export function AriaOnly({ children }: { children?: ReactNode }) {
return children ? h('div', { className: 'ariaOnly' }, children) : null
}
export function noAriaTitle(title: string) {
return {
onMouseEnter(ev: any) {
ev.target.title = title
}
}
}
export const isMac = navigator.platform.match('Mac')
export function isCtrlKey(ev: KeyboardEvent) {
return (ev.ctrlKey || isMac && ev.metaKey) && ev.key
}