@tiptap/react 3.20.1 → 3.20.3
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.
- package/package.json +7 -7
- package/src/menus/BubbleMenu.spec.ts +174 -0
- package/src/menus/BubbleMenu.tsx +12 -4
- package/src/menus/FloatingMenu.spec.ts +164 -0
- package/src/menus/FloatingMenu.tsx +12 -15
- package/src/menus/getAutoPluginKey.ts +5 -0
- package/src/menus/useMenuElementProps.ts +354 -0
- package/dist/index.cjs +0 -1195
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -534
- package/dist/index.d.ts +0 -534
- package/dist/index.js +0 -1131
- package/dist/index.js.map +0 -1
- package/dist/menus/index.cjs +0 -229
- package/dist/menus/index.cjs.map +0 -1
- package/dist/menus/index.d.cts +0 -19
- package/dist/menus/index.d.ts +0 -19
- package/dist/menus/index.js +0 -191
- package/dist/menus/index.js.map +0 -1
|
@@ -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
|
+
}
|