@tiptap/react 3.20.2 → 3.20.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,354 @@
1
+ import type { CSSProperties, HTMLAttributes } from 'react'
2
+ import { useEffect, useLayoutEffect, useRef } from 'react'
3
+
4
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
5
+
6
+ type MenuElementProps = HTMLAttributes<HTMLDivElement>
7
+ type MenuSyntheticEvent = Event & {
8
+ nativeEvent: Event
9
+ currentTarget: HTMLDivElement
10
+ target: EventTarget | null
11
+ persist: () => void
12
+ isDefaultPrevented: () => boolean
13
+ isPropagationStopped: () => boolean
14
+ }
15
+ type MenuEventListener = (event: MenuSyntheticEvent) => void
16
+ type MenuNativeListener = (event: Event) => void
17
+ type MenuEventListenerOptions = {
18
+ capture?: boolean
19
+ }
20
+
21
+ type EventListenerEntry = {
22
+ eventName: string
23
+ listener: MenuNativeListener
24
+ options?: MenuEventListenerOptions
25
+ }
26
+
27
+ const PLUGIN_MANAGED_STYLE_PROPERTIES = new Set(['left', 'opacity', 'position', 'top', 'visibility', 'width'])
28
+ const UNITLESS_STYLE_PROPERTIES = new Set([
29
+ 'animationIterationCount',
30
+ 'aspectRatio',
31
+ 'borderImageOutset',
32
+ 'borderImageSlice',
33
+ 'borderImageWidth',
34
+ 'columnCount',
35
+ 'columns',
36
+ 'fillOpacity',
37
+ 'flex',
38
+ 'flexGrow',
39
+ 'flexShrink',
40
+ 'fontWeight',
41
+ 'gridArea',
42
+ 'gridColumn',
43
+ 'gridColumnEnd',
44
+ 'gridColumnStart',
45
+ 'gridRow',
46
+ 'gridRowEnd',
47
+ 'gridRowStart',
48
+ 'lineClamp',
49
+ 'lineHeight',
50
+ 'opacity',
51
+ 'order',
52
+ 'orphans',
53
+ 'scale',
54
+ 'stopOpacity',
55
+ 'strokeDasharray',
56
+ 'strokeDashoffset',
57
+ 'strokeMiterlimit',
58
+ 'strokeOpacity',
59
+ 'strokeWidth',
60
+ 'tabSize',
61
+ 'widows',
62
+ 'zIndex',
63
+ 'zoom',
64
+ ])
65
+ const ATTRIBUTE_EXCLUSIONS = new Set(['children', 'className', 'style'])
66
+ const DIRECT_PROPERTY_KEYS = new Set(['tabIndex'])
67
+ const FORWARDED_ATTRIBUTE_KEYS = new Set([
68
+ 'accessKey',
69
+ 'autoCapitalize',
70
+ 'contentEditable',
71
+ 'contextMenu',
72
+ 'dir',
73
+ 'draggable',
74
+ 'enterKeyHint',
75
+ 'hidden',
76
+ 'id',
77
+ 'lang',
78
+ 'nonce',
79
+ 'role',
80
+ 'slot',
81
+ 'spellCheck',
82
+ 'tabIndex',
83
+ 'title',
84
+ 'translate',
85
+ ])
86
+ const SPECIAL_EVENT_NAMES: Record<string, string> = {
87
+ Blur: 'focusout',
88
+ DoubleClick: 'dblclick',
89
+ Focus: 'focusin',
90
+ MouseEnter: 'mouseenter',
91
+ MouseLeave: 'mouseleave',
92
+ }
93
+
94
+ function isEventProp(key: string, value: unknown): value is MenuEventListener {
95
+ return /^on[A-Z]/.test(key) && typeof value === 'function'
96
+ }
97
+
98
+ function toAttributeName(key: string) {
99
+ if (key.startsWith('aria-') || key.startsWith('data-')) {
100
+ return key
101
+ }
102
+
103
+ return key
104
+ }
105
+
106
+ function isForwardedAttributeKey(key: string) {
107
+ return key.startsWith('aria-') || key.startsWith('data-') || FORWARDED_ATTRIBUTE_KEYS.has(key)
108
+ }
109
+
110
+ function toStylePropertyName(key: string) {
111
+ if (key.startsWith('--')) {
112
+ return key
113
+ }
114
+
115
+ return key.replace(/[A-Z]/g, match => `-${match.toLowerCase()}`)
116
+ }
117
+
118
+ function toEventConfig(key: string) {
119
+ const useCapture = key.endsWith('Capture')
120
+ const baseKey = useCapture ? key.slice(0, -7) : key
121
+ const reactEventName = baseKey.slice(2)
122
+ const eventName = SPECIAL_EVENT_NAMES[reactEventName] ?? reactEventName.toLowerCase()
123
+
124
+ return {
125
+ eventName,
126
+ options: useCapture ? { capture: true } : undefined,
127
+ }
128
+ }
129
+
130
+ function createSyntheticEvent(element: HTMLDivElement, nativeEvent: Event): MenuSyntheticEvent {
131
+ let defaultPrevented = nativeEvent.defaultPrevented
132
+ let propagationStopped = false
133
+ const syntheticEvent = Object.create(nativeEvent)
134
+
135
+ Object.defineProperties(syntheticEvent, {
136
+ nativeEvent: { value: nativeEvent },
137
+ currentTarget: { value: element },
138
+ target: { value: nativeEvent.target },
139
+ persist: { value: () => undefined },
140
+ isDefaultPrevented: { value: () => defaultPrevented },
141
+ isPropagationStopped: { value: () => propagationStopped },
142
+ preventDefault: {
143
+ value: () => {
144
+ defaultPrevented = true
145
+ nativeEvent.preventDefault()
146
+ },
147
+ },
148
+ stopPropagation: {
149
+ value: () => {
150
+ propagationStopped = true
151
+ nativeEvent.stopPropagation()
152
+ },
153
+ },
154
+ })
155
+
156
+ return syntheticEvent as MenuSyntheticEvent
157
+ }
158
+
159
+ function isDirectPropertyKey(key: string) {
160
+ return DIRECT_PROPERTY_KEYS.has(key)
161
+ }
162
+
163
+ function setDirectProperty(element: HTMLDivElement, key: string, value: unknown) {
164
+ if (key === 'tabIndex') {
165
+ element.tabIndex = Number(value)
166
+ return
167
+ }
168
+
169
+ ;(element as unknown as Record<string, unknown>)[key] = value
170
+ }
171
+
172
+ function clearDirectProperty(element: HTMLDivElement, key: string) {
173
+ if (key === 'tabIndex') {
174
+ element.removeAttribute('tabindex')
175
+ return
176
+ }
177
+
178
+ const propertyValue = (element as unknown as Record<string, unknown>)[key]
179
+
180
+ if (typeof propertyValue === 'boolean') {
181
+ ;(element as unknown as Record<string, unknown>)[key] = false
182
+ return
183
+ }
184
+
185
+ if (typeof propertyValue === 'number') {
186
+ ;(element as unknown as Record<string, unknown>)[key] = 0
187
+ return
188
+ }
189
+
190
+ ;(element as unknown as Record<string, unknown>)[key] = ''
191
+ }
192
+
193
+ function toStyleValue(styleName: string, value: string | number) {
194
+ if (
195
+ typeof value !== 'number' ||
196
+ value === 0 ||
197
+ styleName.startsWith('--') ||
198
+ UNITLESS_STYLE_PROPERTIES.has(styleName)
199
+ ) {
200
+ return String(value)
201
+ }
202
+
203
+ return `${value}px`
204
+ }
205
+
206
+ function removeStyleProperty(element: HTMLDivElement, styleName: string) {
207
+ if (PLUGIN_MANAGED_STYLE_PROPERTIES.has(styleName)) {
208
+ return
209
+ }
210
+
211
+ element.style.removeProperty(toStylePropertyName(styleName))
212
+ }
213
+
214
+ function applyStyleProperty(element: HTMLDivElement, styleName: string, value: string | number) {
215
+ if (PLUGIN_MANAGED_STYLE_PROPERTIES.has(styleName)) {
216
+ return
217
+ }
218
+
219
+ element.style.setProperty(toStylePropertyName(styleName), toStyleValue(styleName, value))
220
+ }
221
+
222
+ function syncAttributes(element: HTMLDivElement, prevProps: MenuElementProps, nextProps: MenuElementProps) {
223
+ const allKeys = new Set([...Object.keys(prevProps), ...Object.keys(nextProps)])
224
+
225
+ allKeys.forEach(key => {
226
+ if (
227
+ ATTRIBUTE_EXCLUSIONS.has(key) ||
228
+ !isForwardedAttributeKey(key) ||
229
+ isEventProp(key, prevProps[key as keyof MenuElementProps]) ||
230
+ isEventProp(key, nextProps[key as keyof MenuElementProps])
231
+ ) {
232
+ return
233
+ }
234
+
235
+ const prevValue = prevProps[key as keyof MenuElementProps]
236
+ const nextValue = nextProps[key as keyof MenuElementProps]
237
+
238
+ if (prevValue === nextValue) {
239
+ return
240
+ }
241
+
242
+ const attributeName = toAttributeName(key)
243
+
244
+ if (nextValue == null || nextValue === false) {
245
+ if (isDirectPropertyKey(key)) {
246
+ clearDirectProperty(element, key)
247
+ }
248
+
249
+ element.removeAttribute(attributeName)
250
+ return
251
+ }
252
+
253
+ if (nextValue === true) {
254
+ if (isDirectPropertyKey(key)) {
255
+ setDirectProperty(element, key, true)
256
+ }
257
+
258
+ element.setAttribute(attributeName, '')
259
+ return
260
+ }
261
+
262
+ if (isDirectPropertyKey(key)) {
263
+ setDirectProperty(element, key, nextValue)
264
+ return
265
+ }
266
+
267
+ element.setAttribute(attributeName, String(nextValue))
268
+ })
269
+ }
270
+
271
+ function syncClassName(element: HTMLDivElement, prevClassName?: string, nextClassName?: string) {
272
+ if (prevClassName === nextClassName) {
273
+ return
274
+ }
275
+
276
+ if (nextClassName) {
277
+ element.className = nextClassName
278
+ return
279
+ }
280
+
281
+ element.removeAttribute('class')
282
+ }
283
+
284
+ function syncStyles(
285
+ element: HTMLDivElement,
286
+ prevStyle: CSSProperties | undefined,
287
+ nextStyle: CSSProperties | undefined,
288
+ ) {
289
+ const previousStyle = prevStyle ?? {}
290
+ const currentStyle = nextStyle ?? {}
291
+ const allStyleNames = new Set([...Object.keys(previousStyle), ...Object.keys(currentStyle)])
292
+
293
+ allStyleNames.forEach(styleName => {
294
+ const prevValue = previousStyle[styleName as keyof CSSProperties]
295
+ const nextValue = currentStyle[styleName as keyof CSSProperties]
296
+
297
+ if (prevValue === nextValue) {
298
+ return
299
+ }
300
+
301
+ if (nextValue == null) {
302
+ removeStyleProperty(element, styleName)
303
+ return
304
+ }
305
+
306
+ applyStyleProperty(element, styleName, nextValue as string | number)
307
+ })
308
+ }
309
+
310
+ function syncEventListeners(element: HTMLDivElement, prevListeners: EventListenerEntry[], nextProps: MenuElementProps) {
311
+ prevListeners.forEach(({ eventName, listener, options }) => {
312
+ element.removeEventListener(eventName, listener, options)
313
+ })
314
+
315
+ const nextListeners: EventListenerEntry[] = []
316
+
317
+ Object.entries(nextProps).forEach(([key, value]) => {
318
+ if (!isEventProp(key, value)) {
319
+ return
320
+ }
321
+
322
+ const { eventName, options } = toEventConfig(key)
323
+ const listener: MenuNativeListener = event => {
324
+ value(createSyntheticEvent(element, event))
325
+ }
326
+
327
+ element.addEventListener(eventName, listener, options)
328
+ nextListeners.push({ eventName, listener, options })
329
+ })
330
+
331
+ return nextListeners
332
+ }
333
+
334
+ export function useMenuElementProps(element: HTMLDivElement, props: MenuElementProps) {
335
+ const previousPropsRef = useRef<MenuElementProps>({})
336
+ const listenersRef = useRef<EventListenerEntry[]>([])
337
+
338
+ useIsomorphicLayoutEffect(() => {
339
+ const previousProps = previousPropsRef.current
340
+
341
+ syncClassName(element, previousProps.className, props.className)
342
+ syncStyles(element, previousProps.style, props.style)
343
+ syncAttributes(element, previousProps, props)
344
+ listenersRef.current = syncEventListeners(element, listenersRef.current, props)
345
+ previousPropsRef.current = props
346
+
347
+ return () => {
348
+ listenersRef.current.forEach(({ eventName, listener, options }) => {
349
+ element.removeEventListener(eventName, listener, options)
350
+ })
351
+ listenersRef.current = []
352
+ }
353
+ }, [element, props])
354
+ }