@liveblocks/react-tiptap 2.16.0-toolbars1 → 2.16.0-toolbars2

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.
@@ -1,13 +1,13 @@
1
1
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
2
2
  import { inline, flip, hide, shift, limitShift, offset, size, autoUpdate, useFloating } from '@floating-ui/react-dom';
3
3
  import { useLayoutEffect } from '@liveblocks/react/_private';
4
- import { useRefs, TooltipProvider } from '@liveblocks/react-ui/_private';
4
+ import { useInitial, useRefs, TooltipProvider } from '@liveblocks/react-ui/_private';
5
5
  import { useEditorState, isTextSelection } from '@tiptap/react';
6
6
  import { forwardRef, useRef, useState, useEffect, useCallback, useMemo } from 'react';
7
7
  import { createPortal } from 'react-dom';
8
8
  import { classNames } from '../classnames.mjs';
9
9
  import { EditorProvider } from '../context.mjs';
10
- import { FloatingToolbarContext } from './FloatingToolbarContext.mjs';
10
+ import { FloatingToolbarContext, FloatingToolbarExternal } from './shared.mjs';
11
11
  import { Toolbar, applyToolbarSlot } from './Toolbar.mjs';
12
12
 
13
13
  const FLOATING_TOOLBAR_COLLISION_PADDING = 10;
@@ -27,270 +27,295 @@ function DefaultFloatingToolbarContent({ editor }) {
27
27
  ]
28
28
  });
29
29
  }
30
- const FloatingToolbar = forwardRef(
31
- ({
32
- children = DefaultFloatingToolbarContent,
33
- before,
34
- after,
35
- position = "top",
36
- offset: sideOffset = 6,
37
- editor,
38
- onPointerDown,
39
- onFocus,
40
- onBlur,
41
- className,
42
- ...props
43
- }, forwardedRef) => {
44
- const toolbarRef = useRef(null);
45
- const [isPointerDown, setPointerDown] = useState(false);
46
- const [isFocused, setFocused] = useState(false);
47
- const [isManuallyClosed, setManuallyClosed] = useState(false);
48
- const isEditable = useEditorState({
30
+ const FloatingToolbar = Object.assign(
31
+ forwardRef(
32
+ ({
33
+ children = DefaultFloatingToolbarContent,
34
+ before,
35
+ after,
36
+ position = "top",
37
+ offset: sideOffset = 6,
49
38
  editor,
50
- equalityFn: Object.is,
51
- selector: (ctx) => ctx.editor?.isEditable ?? false
52
- }) ?? false;
53
- const hasSelectionRange = useEditorState({
54
- editor,
55
- equalityFn: Object.is,
56
- selector: (ctx) => {
57
- const editor2 = ctx.editor;
58
- if (!editor2) {
59
- return false;
60
- }
61
- const { doc, selection } = editor2.state;
62
- const { empty, ranges } = selection;
63
- const from = Math.min(...ranges.map((range) => range.$from.pos));
64
- const to = Math.max(...ranges.map((range) => range.$to.pos));
65
- if (empty) {
66
- return false;
39
+ onPointerDown,
40
+ onFocus,
41
+ onBlur,
42
+ className,
43
+ ...props
44
+ }, forwardedRef) => {
45
+ const toolbarRef = useRef(null);
46
+ const externalIds = useInitial(() => /* @__PURE__ */ new Set());
47
+ const [isPointerDown, setPointerDown] = useState(false);
48
+ const [isFocused, setFocused] = useState(false);
49
+ const [isManuallyClosed, setManuallyClosed] = useState(false);
50
+ const isEditable = useEditorState({
51
+ editor,
52
+ equalityFn: Object.is,
53
+ selector: (ctx) => ctx.editor?.isEditable ?? false
54
+ }) ?? false;
55
+ const hasSelectionRange = useEditorState({
56
+ editor,
57
+ equalityFn: Object.is,
58
+ selector: (ctx) => {
59
+ const editor2 = ctx.editor;
60
+ if (!editor2) {
61
+ return false;
62
+ }
63
+ const { doc, selection } = editor2.state;
64
+ const { empty, ranges } = selection;
65
+ const from = Math.min(...ranges.map((range) => range.$from.pos));
66
+ const to = Math.max(...ranges.map((range) => range.$to.pos));
67
+ if (empty) {
68
+ return false;
69
+ }
70
+ return isTextSelection(selection) && doc.textBetween(from, to).length > 0;
67
71
  }
68
- return isTextSelection(selection) && doc.textBetween(from, to).length > 0;
69
- }
70
- }) ?? false;
71
- const isOpen = isFocused && !isPointerDown && hasSelectionRange && !isManuallyClosed;
72
- const [delayedIsOpen, setDelayedIsOpen] = useState(isOpen);
73
- const delayedIsOpenTimeoutRef = useRef();
74
- useEffect(() => {
75
- if (!editor) {
76
- return;
77
- }
78
- setManuallyClosed(false);
79
- const handleSelectionChange = () => {
80
- setManuallyClosed(false);
81
- };
82
- editor.on("selectionUpdate", handleSelectionChange);
83
- return () => {
84
- editor.off("selectionUpdate", handleSelectionChange);
85
- };
86
- }, [isFocused, hasSelectionRange, editor]);
87
- useEffect(() => {
88
- if (!editor) {
89
- return;
90
- }
91
- const handleFocus2 = () => {
92
- setFocused(true);
93
- };
94
- const handleBlur2 = (event) => {
95
- if (event.relatedTarget && toolbarRef.current?.contains(event.relatedTarget)) {
72
+ }) ?? false;
73
+ const isOpen = isFocused && !isPointerDown && hasSelectionRange && !isManuallyClosed;
74
+ const [delayedIsOpen, setDelayedIsOpen] = useState(isOpen);
75
+ const delayedIsOpenTimeoutRef = useRef();
76
+ useEffect(() => {
77
+ if (!editor) {
96
78
  return;
97
79
  }
98
- if (event.relatedTarget === editor.view.dom) {
80
+ setManuallyClosed(false);
81
+ const handleSelectionChange = () => {
82
+ setManuallyClosed(false);
83
+ };
84
+ editor.on("selectionUpdate", handleSelectionChange);
85
+ return () => {
86
+ editor.off("selectionUpdate", handleSelectionChange);
87
+ };
88
+ }, [isFocused, hasSelectionRange, editor]);
89
+ useEffect(() => {
90
+ if (!editor) {
99
91
  return;
100
92
  }
101
- setFocused(false);
102
- };
103
- editor.view.dom.addEventListener("focus", handleFocus2);
104
- editor.view.dom.addEventListener("blur", handleBlur2);
105
- return () => {
106
- editor.view.dom.removeEventListener("focus", handleFocus2);
107
- editor.view.dom.removeEventListener("blur", handleBlur2);
108
- };
109
- }, [editor]);
110
- const handleFocus = useCallback(
111
- (event) => {
112
- onFocus?.(event);
113
- if (!event.isDefaultPrevented()) {
93
+ const handleFocus2 = () => {
114
94
  setFocused(true);
115
- }
116
- },
117
- [onFocus]
118
- );
119
- const handleBlur = useCallback(
120
- (event) => {
121
- onBlur?.(event);
122
- if (!event.isDefaultPrevented()) {
95
+ };
96
+ const handleBlur2 = (event) => {
123
97
  if (event.relatedTarget && toolbarRef.current?.contains(event.relatedTarget)) {
124
98
  return;
125
99
  }
126
- if (event.relatedTarget === editor?.view.dom) {
100
+ if (event.relatedTarget === editor.view.dom) {
127
101
  return;
128
102
  }
103
+ for (const externalId of externalIds) {
104
+ if (document.getElementById(externalId)?.contains(event.relatedTarget)) {
105
+ return;
106
+ }
107
+ }
129
108
  setFocused(false);
109
+ };
110
+ editor.view.dom.addEventListener("focus", handleFocus2);
111
+ editor.view.dom.addEventListener("blur", handleBlur2);
112
+ return () => {
113
+ editor.view.dom.removeEventListener("focus", handleFocus2);
114
+ editor.view.dom.removeEventListener("blur", handleBlur2);
115
+ };
116
+ }, [editor, externalIds]);
117
+ const handleFocus = useCallback(
118
+ (event) => {
119
+ onFocus?.(event);
120
+ if (!event.isDefaultPrevented()) {
121
+ setFocused(true);
122
+ }
123
+ },
124
+ [onFocus]
125
+ );
126
+ const handleBlur = useCallback(
127
+ (event) => {
128
+ onBlur?.(event);
129
+ if (!event.isDefaultPrevented()) {
130
+ if (event.relatedTarget && toolbarRef.current?.contains(event.relatedTarget)) {
131
+ return;
132
+ }
133
+ if (event.relatedTarget === editor?.view.dom) {
134
+ return;
135
+ }
136
+ for (const externalId of externalIds) {
137
+ if (document.getElementById(externalId)?.contains(event.relatedTarget)) {
138
+ return;
139
+ }
140
+ }
141
+ setFocused(false);
142
+ }
143
+ },
144
+ [onBlur, editor, externalIds]
145
+ );
146
+ useEffect(() => {
147
+ if (isOpen) {
148
+ delayedIsOpenTimeoutRef.current = window.setTimeout(() => {
149
+ setDelayedIsOpen(true);
150
+ }, FLOATING_TOOLBAR_OPEN_DELAY);
151
+ } else {
152
+ setDelayedIsOpen(false);
130
153
  }
131
- },
132
- [onBlur, editor]
133
- );
134
- useEffect(() => {
135
- if (isOpen) {
136
- delayedIsOpenTimeoutRef.current = window.setTimeout(() => {
137
- setDelayedIsOpen(true);
138
- }, FLOATING_TOOLBAR_OPEN_DELAY);
139
- } else {
140
- setDelayedIsOpen(false);
141
- }
142
- return () => {
143
- window.clearTimeout(delayedIsOpenTimeoutRef.current);
144
- };
145
- }, [isOpen]);
146
- const floatingOptions = useMemo(() => {
147
- const detectOverflowOptions = {
148
- padding: FLOATING_TOOLBAR_COLLISION_PADDING
149
- };
150
- return {
151
- strategy: "fixed",
152
- placement: position,
153
- middleware: [
154
- inline(detectOverflowOptions),
155
- flip({ ...detectOverflowOptions, crossAxis: false }),
156
- hide(detectOverflowOptions),
157
- shift({
158
- ...detectOverflowOptions,
159
- limiter: limitShift()
160
- }),
161
- offset(sideOffset),
162
- size(detectOverflowOptions)
163
- ],
164
- whileElementsMounted: (...args) => {
165
- return autoUpdate(...args, {
166
- animationFrame: true
167
- });
154
+ return () => {
155
+ window.clearTimeout(delayedIsOpenTimeoutRef.current);
156
+ };
157
+ }, [isOpen]);
158
+ const floatingOptions = useMemo(() => {
159
+ const detectOverflowOptions = {
160
+ padding: FLOATING_TOOLBAR_COLLISION_PADDING
161
+ };
162
+ return {
163
+ strategy: "fixed",
164
+ placement: position,
165
+ middleware: [
166
+ inline(detectOverflowOptions),
167
+ flip({ ...detectOverflowOptions, crossAxis: false }),
168
+ hide(detectOverflowOptions),
169
+ shift({
170
+ ...detectOverflowOptions,
171
+ limiter: limitShift()
172
+ }),
173
+ offset(sideOffset),
174
+ size(detectOverflowOptions)
175
+ ],
176
+ whileElementsMounted: (...args) => {
177
+ return autoUpdate(...args, {
178
+ animationFrame: true
179
+ });
180
+ }
181
+ };
182
+ }, [position, sideOffset]);
183
+ const {
184
+ refs: { setReference, setFloating },
185
+ strategy,
186
+ x,
187
+ y,
188
+ isPositioned
189
+ } = useFloating({
190
+ ...floatingOptions,
191
+ open: delayedIsOpen
192
+ });
193
+ const mergedRefs = useRefs(forwardedRef, toolbarRef, setFloating);
194
+ const handlePointerDown = useCallback(
195
+ (event) => {
196
+ onPointerDown?.(event);
197
+ event.stopPropagation();
198
+ if (event.target === toolbarRef.current) {
199
+ event.preventDefault();
200
+ }
201
+ },
202
+ [onPointerDown]
203
+ );
204
+ useEffect(() => {
205
+ if (!editor || !isEditable) {
206
+ return;
168
207
  }
169
- };
170
- }, [position, sideOffset]);
171
- const {
172
- refs: { setReference, setFloating },
173
- strategy,
174
- x,
175
- y,
176
- isPositioned
177
- } = useFloating({
178
- ...floatingOptions,
179
- open: delayedIsOpen
180
- });
181
- const mergedRefs = useRefs(forwardedRef, toolbarRef, setFloating);
182
- const handlePointerDown = useCallback(
183
- (event) => {
184
- onPointerDown?.(event);
185
- event.stopPropagation();
186
- if (event.target === toolbarRef.current) {
187
- event.preventDefault();
208
+ const handlePointerDown2 = () => {
209
+ setPointerDown(true);
210
+ };
211
+ const handlePointerUp = () => {
212
+ setPointerDown(false);
213
+ };
214
+ document.addEventListener("pointerdown", handlePointerDown2);
215
+ document.addEventListener("pointercancel", handlePointerUp);
216
+ document.addEventListener("pointerup", handlePointerUp);
217
+ return () => {
218
+ document.removeEventListener("pointerdown", handlePointerDown2);
219
+ document.removeEventListener("pointercancel", handlePointerUp);
220
+ document.removeEventListener("pointerup", handlePointerUp);
221
+ };
222
+ }, [editor, isEditable]);
223
+ useLayoutEffect(() => {
224
+ if (!editor || !delayedIsOpen) {
225
+ return;
188
226
  }
189
- },
190
- [onPointerDown]
191
- );
192
- useEffect(() => {
193
- if (!editor || !isEditable) {
194
- return;
195
- }
196
- const handlePointerDown2 = () => {
197
- setPointerDown(true);
198
- };
199
- const handlePointerUp = () => {
200
- setPointerDown(false);
201
- };
202
- document.addEventListener("pointerdown", handlePointerDown2);
203
- document.addEventListener("pointercancel", handlePointerUp);
204
- document.addEventListener("pointerup", handlePointerUp);
205
- return () => {
206
- document.removeEventListener("pointerdown", handlePointerDown2);
207
- document.removeEventListener("pointercancel", handlePointerUp);
208
- document.removeEventListener("pointerup", handlePointerUp);
209
- };
210
- }, [editor, isEditable]);
211
- useLayoutEffect(() => {
212
- if (!editor || !delayedIsOpen) {
213
- return;
214
- }
215
- const updateSelectionReference = () => {
216
- const domSelection = window.getSelection();
217
- if (editor.state.selection.empty || !domSelection || !domSelection.rangeCount) {
218
- setReference(null);
219
- } else {
220
- const domRange = domSelection.getRangeAt(0);
221
- setReference(domRange);
227
+ const updateSelectionReference = () => {
228
+ const domSelection = window.getSelection();
229
+ if (editor.state.selection.empty || !domSelection || !domSelection.rangeCount) {
230
+ setReference(null);
231
+ } else {
232
+ const domRange = domSelection.getRangeAt(0);
233
+ setReference(domRange);
234
+ }
235
+ };
236
+ editor.on("transaction", updateSelectionReference);
237
+ updateSelectionReference();
238
+ return () => {
239
+ editor.off("transaction", updateSelectionReference);
240
+ };
241
+ }, [editor, delayedIsOpen, setReference]);
242
+ useEffect(() => {
243
+ if (!editor || !delayedIsOpen) {
244
+ return;
222
245
  }
223
- };
224
- editor.on("transaction", updateSelectionReference);
225
- updateSelectionReference();
226
- return () => {
227
- editor.off("transaction", updateSelectionReference);
228
- };
229
- }, [editor, delayedIsOpen, setReference]);
230
- useEffect(() => {
246
+ const handleKeyDown = (event) => {
247
+ if (event.target !== editor.view.dom && event.defaultPrevented) {
248
+ return;
249
+ }
250
+ if (event.key === "Escape") {
251
+ event.preventDefault();
252
+ event.stopPropagation();
253
+ editor.commands.focus();
254
+ setManuallyClosed(true);
255
+ }
256
+ };
257
+ editor.view.dom.addEventListener("keydown", handleKeyDown);
258
+ return () => {
259
+ editor.view.dom.removeEventListener("keydown", handleKeyDown);
260
+ };
261
+ }, [editor, delayedIsOpen]);
262
+ const close = useCallback(() => {
263
+ setManuallyClosed(true);
264
+ }, [setManuallyClosed]);
265
+ const registerExternal = useCallback(
266
+ (id) => {
267
+ externalIds.add(id);
268
+ return () => {
269
+ externalIds.delete(id);
270
+ };
271
+ },
272
+ [externalIds]
273
+ );
231
274
  if (!editor || !delayedIsOpen) {
232
- return;
275
+ return null;
233
276
  }
234
- const handleKeyDown = (event) => {
235
- if (event.target !== editor.view.dom && event.defaultPrevented) {
236
- return;
237
- }
238
- if (event.key === "Escape") {
239
- event.preventDefault();
240
- event.stopPropagation();
241
- editor.commands.focus();
242
- setManuallyClosed(true);
243
- }
244
- };
245
- editor.view.dom.addEventListener("keydown", handleKeyDown);
246
- return () => {
247
- editor.view.dom.removeEventListener("keydown", handleKeyDown);
248
- };
249
- }, [editor, delayedIsOpen]);
250
- const close = useCallback(() => {
251
- setManuallyClosed(true);
252
- }, [setManuallyClosed]);
253
- if (!editor || !delayedIsOpen) {
254
- return null;
255
- }
256
- const slotProps = { editor };
257
- return createPortal(
258
- /* @__PURE__ */ jsx(TooltipProvider, {
259
- children: /* @__PURE__ */ jsx(EditorProvider, {
260
- editor,
261
- children: /* @__PURE__ */ jsx(FloatingToolbarContext.Provider, {
262
- value: { close },
263
- children: /* @__PURE__ */ jsxs("div", {
264
- role: "toolbar",
265
- "aria-label": "Floating toolbar",
266
- "aria-orientation": "horizontal",
267
- className: classNames(
268
- "lb-root lb-portal lb-elevation lb-tiptap-floating-toolbar lb-tiptap-toolbar",
269
- className
270
- ),
271
- ref: mergedRefs,
272
- style: {
273
- position: strategy,
274
- top: 0,
275
- left: 0,
276
- transform: isPositioned ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)` : "translate3d(0, -200%, 0)",
277
- minWidth: "max-content"
278
- },
279
- onPointerDown: handlePointerDown,
280
- onFocus: handleFocus,
281
- onBlur: handleBlur,
282
- ...props,
283
- children: [
284
- applyToolbarSlot(before, slotProps),
285
- applyToolbarSlot(children, slotProps),
286
- applyToolbarSlot(after, slotProps)
287
- ]
277
+ const slotProps = { editor };
278
+ return createPortal(
279
+ /* @__PURE__ */ jsx(TooltipProvider, {
280
+ children: /* @__PURE__ */ jsx(EditorProvider, {
281
+ editor,
282
+ children: /* @__PURE__ */ jsx(FloatingToolbarContext.Provider, {
283
+ value: { close, registerExternal },
284
+ children: /* @__PURE__ */ jsxs("div", {
285
+ role: "toolbar",
286
+ "aria-label": "Floating toolbar",
287
+ "aria-orientation": "horizontal",
288
+ className: classNames(
289
+ "lb-root lb-portal lb-elevation lb-tiptap-floating-toolbar lb-tiptap-toolbar",
290
+ className
291
+ ),
292
+ ref: mergedRefs,
293
+ style: {
294
+ position: strategy,
295
+ top: 0,
296
+ left: 0,
297
+ transform: isPositioned ? `translate3d(${Math.round(x)}px, ${Math.round(y)}px, 0)` : "translate3d(0, -200%, 0)",
298
+ minWidth: "max-content"
299
+ },
300
+ onPointerDown: handlePointerDown,
301
+ onFocus: handleFocus,
302
+ onBlur: handleBlur,
303
+ ...props,
304
+ children: [
305
+ applyToolbarSlot(before, slotProps),
306
+ applyToolbarSlot(children, slotProps),
307
+ applyToolbarSlot(after, slotProps)
308
+ ]
309
+ })
288
310
  })
289
311
  })
290
- })
291
- }),
292
- document.body
293
- );
312
+ }),
313
+ document.body
314
+ );
315
+ }
316
+ ),
317
+ {
318
+ External: FloatingToolbarExternal
294
319
  }
295
320
  );
296
321