@antscorp/antsomi-ui 1.3.7-beta.3 → 1.3.7-beta.31
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/es/components/atoms/InputDynamic/InputDynamic.js +3 -3
- package/es/components/atoms/index.d.ts +0 -2
- package/es/components/atoms/index.js +0 -2
- package/es/components/molecules/AddDynamicContent/AddDynamicContent.js +28 -2
- package/es/components/molecules/AddDynamicContent/constants.d.ts +2 -0
- package/es/components/molecules/AddDynamicContent/constants.js +2 -0
- package/es/components/molecules/EmojiPopover/EmojiPopover.d.ts +1 -0
- package/es/components/molecules/EmojiPopover/EmojiPopover.js +2 -2
- package/es/components/molecules/EmojiPopover/styled.d.ts +2 -2
- package/es/components/molecules/EmojiPopover/styled.js +1 -1
- package/es/components/molecules/SearchPopover/SearchPopover.js +2 -2
- package/es/components/molecules/SearchPopover/styled.d.ts +12 -1
- package/es/components/molecules/SearchPopover/styled.js +1 -2
- package/es/components/molecules/SearchPopover/types.d.ts +4 -3
- package/es/components/molecules/TagifyInput/TagifyInput.js +55 -19
- package/es/components/molecules/TagifyInput/types.d.ts +20 -1
- package/es/components/molecules/TagifyInput/utils.d.ts +6 -1
- package/es/components/molecules/TagifyInput/utils.js +19 -0
- package/es/components/molecules/VirtualizedMenu/VirtualizedMenu.d.ts +1 -0
- package/es/components/molecules/VirtualizedMenu/components/Item/Item.js +3 -7
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.d.ts +10 -10
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.js +46 -327
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/index.d.ts +9 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/index.js +5 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useFocusManagement.d.ts +23 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useFocusManagement.js +81 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.d.ts +24 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.js +32 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.d.ts +26 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.js +93 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useTreeState.d.ts +24 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useTreeState.js +94 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.d.ts +7 -0
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.js +132 -0
- package/es/components/molecules/VirtualizedMenu/styled.js +14 -3
- package/es/components/molecules/VirtualizedMenu/types.d.ts +1 -0
- package/es/components/molecules/VirtualizedMenu/utils.d.ts +1 -0
- package/es/components/molecules/VirtualizedMenu/utils.js +1 -0
- package/es/components/organism/ActivityTimeline/ActivityTimeline.js +3 -3
- package/es/components/organism/ActivityTimeline/constants.d.ts +9 -9
- package/es/components/organism/ActivityTimeline/constants.js +3 -3
- package/es/components/organism/ActivityTimeline/index.d.ts +530 -1
- package/es/components/organism/ActivityTimeline/index.js +9 -1
- package/es/components/organism/ActivityTimeline/utils.js +7 -6
- package/es/components/organism/TextEditor/TextEditor.d.ts +9 -2
- package/es/components/organism/TextEditor/TextEditor.js +181 -40
- package/es/components/organism/TextEditor/constants.d.ts +9 -0
- package/es/components/organism/TextEditor/constants.js +66 -0
- package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu-plugin.d.ts +6 -128
- package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu-plugin.js +7 -292
- package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu.d.ts +1 -1
- package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu.js +4 -0
- package/es/components/organism/TextEditor/extensions/Link.js +3 -3
- package/es/components/organism/TextEditor/extensions/ListItem.d.ts +10 -0
- package/es/components/organism/TextEditor/extensions/ListItem.js +93 -0
- package/es/components/organism/TextEditor/extensions/SmartTag.d.ts +5 -6
- package/es/components/organism/TextEditor/extensions/SmartTag.js +94 -2
- package/es/components/organism/TextEditor/hooks/useColorSet.js +7 -0
- package/es/components/organism/TextEditor/hooks/useMarkTracking.js +2 -1
- package/es/components/organism/TextEditor/index.d.ts +7 -5
- package/es/components/organism/TextEditor/index.scss +4 -7
- package/es/components/organism/TextEditor/provider.d.ts +1 -0
- package/es/components/organism/TextEditor/provider.js +6 -3
- package/es/components/organism/TextEditor/store.d.ts +11 -4
- package/es/components/organism/TextEditor/store.js +22 -2
- package/es/components/organism/TextEditor/stories/WithOldDynAndLink/froala-legacy-format.settings.json +95 -0
- package/es/components/organism/TextEditor/stories/WithOldDynAndLink/shared.d.ts +111 -0
- package/es/components/organism/TextEditor/stories/WithOldDynAndLink/shared.js +82 -0
- package/es/components/organism/TextEditor/stories/shared.d.ts +64 -0
- package/es/components/organism/TextEditor/stories/shared.js +57 -0
- package/es/components/organism/TextEditor/styled.d.ts +1 -1
- package/es/components/organism/TextEditor/styled.js +1 -0
- package/es/components/organism/TextEditor/types.d.ts +153 -9
- package/es/components/organism/TextEditor/types.js +1 -0
- package/es/components/organism/TextEditor/ui/BubbleMenu/BubbleMenu.d.ts +2 -1
- package/es/components/organism/TextEditor/ui/BubbleMenu/BubbleMenu.js +80 -51
- package/es/components/organism/TextEditor/ui/Emoji/EmojiList.js +1 -1
- package/es/components/organism/TextEditor/ui/Emoji/suggestion.d.ts +1 -1
- package/es/components/organism/TextEditor/ui/Emoji/suggestion.js +2 -2
- package/es/components/organism/TextEditor/ui/FontPopover/FontPopover.js +12 -2
- package/es/components/organism/TextEditor/ui/LinkInsertForm/LinkInsertForm.d.ts +16 -0
- package/es/components/organism/TextEditor/ui/LinkInsertForm/LinkInsertForm.js +61 -0
- package/es/components/organism/TextEditor/ui/LinkInsertForm/index.d.ts +2 -0
- package/es/components/organism/TextEditor/ui/LinkInsertForm/index.js +1 -0
- package/es/components/organism/TextEditor/ui/LinkPopover/LinkPopover.d.ts +9 -0
- package/es/components/organism/TextEditor/ui/LinkPopover/LinkPopover.js +126 -0
- package/es/components/organism/TextEditor/ui/LinkPopover/index.d.ts +2 -0
- package/es/components/organism/TextEditor/ui/LinkPopover/index.js +1 -0
- package/es/components/organism/TextEditor/ui/Popover/Popover.js +1 -1
- package/es/components/organism/TextEditor/ui/TextAlignSelect/TextAlignSelect.js +1 -3
- package/es/components/organism/TextEditor/ui/Toolbar/{Toolbar.d.ts → FormattingToolbar.d.ts} +3 -4
- package/es/components/organism/TextEditor/ui/Toolbar/FormattingToolbar.js +85 -0
- package/es/components/organism/TextEditor/ui/Toolbar/LinkPreviewToolbar.d.ts +10 -0
- package/es/components/organism/TextEditor/ui/Toolbar/LinkPreviewToolbar.js +39 -0
- package/es/components/organism/TextEditor/ui/Toolbar/actions/EmojiAction.js +4 -3
- package/es/components/organism/TextEditor/ui/Toolbar/actions/FontFamilyAction.d.ts +1 -0
- package/es/components/organism/TextEditor/ui/Toolbar/actions/FontFamilyAction.js +2 -2
- package/es/components/organism/TextEditor/ui/Toolbar/actions/FontSizeAction.js +6 -1
- package/es/components/organism/TextEditor/ui/Toolbar/index.d.ts +2 -1
- package/es/components/organism/TextEditor/ui/Toolbar/index.js +2 -1
- package/es/components/organism/TextEditor/utils/documentState.d.ts +14 -0
- package/es/components/organism/TextEditor/utils/documentState.js +25 -0
- package/es/components/organism/TextEditor/utils/htmlProcessing.js +60 -0
- package/es/components/organism/TextEditor/utils/link.d.ts +10 -1
- package/es/components/organism/TextEditor/utils/link.js +161 -7
- package/es/components/organism/TextEditor/utils/menu.js +2 -1
- package/es/components/organism/TextEditor/utils/selection.js +3 -2
- package/es/components/organism/TextEditor/utils/smartTag.js +2 -1
- package/es/components/organism/index.d.ts +1 -1
- package/es/hooks/index.d.ts +1 -1
- package/es/hooks/index.js +1 -1
- package/es/services/MediaTemplateDesign/UploadFile/index.js +4 -4
- package/es/types/index.d.ts +1 -1
- package/es/types/index.js +1 -1
- package/es/utils/common.d.ts +1 -1
- package/es/utils/common.js +3 -3
- package/package.json +23 -35
- package/es/components/organism/TextEditor/ui/BubbleToolbar/BubbleToolbar.d.ts +0 -1
- package/es/components/organism/TextEditor/ui/BubbleToolbar/BubbleToolbar.js +0 -1
- package/es/components/organism/TextEditor/ui/BubbleToolbar/index.d.ts +0 -0
- package/es/components/organism/TextEditor/ui/BubbleToolbar/index.js +0 -1
- package/es/components/organism/TextEditor/ui/Toolbar/Toolbar.js +0 -39
|
@@ -1,123 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { isTextSelection, posToDOMRect } from '@tiptap/core';
|
|
1
|
+
import { BubbleMenuView as TiptapBubbleMenuView } from '@tiptap/extension-bubble-menu';
|
|
3
2
|
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const bottom = Math.max(rect1.bottom, rect2.bottom);
|
|
8
|
-
const left = Math.min(rect1.left, rect2.left);
|
|
9
|
-
const right = Math.max(rect1.right, rect2.right);
|
|
10
|
-
const width = right - left;
|
|
11
|
-
const height = bottom - top;
|
|
12
|
-
const x = left;
|
|
13
|
-
const y = top;
|
|
14
|
-
return new DOMRect(x, y, width, height);
|
|
15
|
-
}
|
|
16
|
-
export class BubbleMenuView {
|
|
17
|
-
get middlewares() {
|
|
18
|
-
const middlewares = [];
|
|
19
|
-
if (this.floatingUIOptions.flip) {
|
|
20
|
-
middlewares.push(flip(typeof this.floatingUIOptions.flip !== 'boolean'
|
|
21
|
-
? this.floatingUIOptions.flip
|
|
22
|
-
: undefined));
|
|
23
|
-
}
|
|
24
|
-
if (this.floatingUIOptions.shift) {
|
|
25
|
-
middlewares.push(shift(typeof this.floatingUIOptions.shift !== 'boolean'
|
|
26
|
-
? this.floatingUIOptions.shift
|
|
27
|
-
: undefined));
|
|
28
|
-
}
|
|
29
|
-
if (this.floatingUIOptions.offset) {
|
|
30
|
-
middlewares.push(offset(typeof this.floatingUIOptions.offset !== 'boolean'
|
|
31
|
-
? this.floatingUIOptions.offset
|
|
32
|
-
: undefined));
|
|
33
|
-
}
|
|
34
|
-
if (this.floatingUIOptions.arrow) {
|
|
35
|
-
middlewares.push(arrow(this.floatingUIOptions.arrow));
|
|
36
|
-
}
|
|
37
|
-
if (this.floatingUIOptions.size) {
|
|
38
|
-
middlewares.push(size(typeof this.floatingUIOptions.size !== 'boolean'
|
|
39
|
-
? this.floatingUIOptions.size
|
|
40
|
-
: undefined));
|
|
41
|
-
}
|
|
42
|
-
if (this.floatingUIOptions.autoPlacement) {
|
|
43
|
-
middlewares.push(autoPlacement(typeof this.floatingUIOptions.autoPlacement !== 'boolean'
|
|
44
|
-
? this.floatingUIOptions.autoPlacement
|
|
45
|
-
: undefined));
|
|
46
|
-
}
|
|
47
|
-
if (this.floatingUIOptions.hide) {
|
|
48
|
-
middlewares.push(hide(typeof this.floatingUIOptions.hide !== 'boolean'
|
|
49
|
-
? this.floatingUIOptions.hide
|
|
50
|
-
: undefined));
|
|
51
|
-
}
|
|
52
|
-
if (this.floatingUIOptions.inline) {
|
|
53
|
-
middlewares.push(inline(typeof this.floatingUIOptions.inline !== 'boolean'
|
|
54
|
-
? this.floatingUIOptions.inline
|
|
55
|
-
: undefined));
|
|
56
|
-
}
|
|
57
|
-
return middlewares;
|
|
58
|
-
}
|
|
59
|
-
constructor({ editor, element, view, updateDelay = 250, resizeDelay = 60, container, shouldShow, options, }) {
|
|
60
|
-
this.preventHide = false;
|
|
61
|
-
this.isVisible = false;
|
|
62
|
-
this.floatingUIOptions = {
|
|
63
|
-
strategy: 'absolute',
|
|
64
|
-
placement: 'top',
|
|
65
|
-
offset: 8,
|
|
66
|
-
flip: {},
|
|
67
|
-
shift: {},
|
|
68
|
-
arrow: false,
|
|
69
|
-
size: false,
|
|
70
|
-
autoPlacement: false,
|
|
71
|
-
hide: false,
|
|
72
|
-
inline: false,
|
|
73
|
-
onShow: undefined,
|
|
74
|
-
onHide: undefined,
|
|
75
|
-
onUpdate: undefined,
|
|
76
|
-
onDestroy: undefined,
|
|
77
|
-
};
|
|
78
|
-
this.shouldShow = ({ view, state, from, to, }) => {
|
|
79
|
-
const { doc, selection } = state;
|
|
80
|
-
const { empty } = selection;
|
|
81
|
-
// Sometime check for `empty` is not enough.
|
|
82
|
-
// Doubleclick an empty paragraph returns a node size of 2.
|
|
83
|
-
// So we check also for an empty text size.
|
|
84
|
-
const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection);
|
|
85
|
-
// When clicking on a element inside the bubble menu the editor "blur" event
|
|
86
|
-
// is called and the bubble menu item is focussed. In this case we should
|
|
87
|
-
// consider the menu as part of the editor and keep showing the menu
|
|
88
|
-
const isChildOfMenu = this.element.contains(document.activeElement);
|
|
89
|
-
const hasEditorFocus = view.hasFocus() || isChildOfMenu;
|
|
90
|
-
if (!hasEditorFocus || empty || isEmptyTextBlock || !this.editor.isEditable) {
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
93
|
-
return true;
|
|
94
|
-
};
|
|
95
|
-
this.mousedownHandler = () => {
|
|
96
|
-
this.preventHide = true;
|
|
97
|
-
};
|
|
98
|
-
this.dragstartHandler = () => {
|
|
99
|
-
this.hide();
|
|
100
|
-
};
|
|
101
|
-
/**
|
|
102
|
-
* Handles the window resize event and element resize event to update the position of the bubble menu.
|
|
103
|
-
* It uses a debounce mechanism to prevent excessive updates.
|
|
104
|
-
* The delay is defined by the `resizeDelay` property.
|
|
105
|
-
*/
|
|
106
|
-
this.resizeHandler = (delay = this.resizeDelay) => {
|
|
107
|
-
if (this.resizeDebounceTimer) {
|
|
108
|
-
clearTimeout(this.resizeDebounceTimer);
|
|
109
|
-
}
|
|
110
|
-
this.resizeDebounceTimer = window.setTimeout(() => {
|
|
111
|
-
this.updatePosition();
|
|
112
|
-
}, delay);
|
|
113
|
-
};
|
|
114
|
-
/**
|
|
115
|
-
* Window resize handler that uses the default resize delay.
|
|
116
|
-
* This is a separate function to maintain the same reference for addEventListener/removeEventListener.
|
|
117
|
-
*/
|
|
118
|
-
this.windowResizeHandler = () => {
|
|
119
|
-
this.resizeHandler();
|
|
120
|
-
};
|
|
3
|
+
export class BubbleMenuView extends TiptapBubbleMenuView {
|
|
4
|
+
constructor(props) {
|
|
5
|
+
super(props);
|
|
121
6
|
/**
|
|
122
7
|
* Sets up ResizeObserver to watch for bubble menu element size changes.
|
|
123
8
|
* This ensures the bubble menu position is updated when the element resizes
|
|
@@ -131,7 +16,7 @@ export class BubbleMenuView {
|
|
|
131
16
|
// Timer for debouncing the visibility restoration after resize
|
|
132
17
|
let resizeTimer = null;
|
|
133
18
|
// Time to wait before showing the element again after resize stops
|
|
134
|
-
const DEBOUNCE_TIME =
|
|
19
|
+
const DEBOUNCE_TIME = this.resizeDelay + 50;
|
|
135
20
|
this.resizeObserver = new ResizeObserver(entries => {
|
|
136
21
|
for (const entry of entries) {
|
|
137
22
|
// Only handle resize events for the bubble menu element itself
|
|
@@ -145,7 +30,7 @@ export class BubbleMenuView {
|
|
|
145
30
|
clearTimeout(resizeTimer);
|
|
146
31
|
}
|
|
147
32
|
// Immediately update the position with no delay for responsive positioning
|
|
148
|
-
this.resizeHandler(
|
|
33
|
+
this.resizeHandler();
|
|
149
34
|
// Set up a debounced timer to restore visibility after resize activity stops
|
|
150
35
|
resizeTimer = setTimeout(() => {
|
|
151
36
|
// Use requestAnimationFrame to ensure the position update is complete
|
|
@@ -165,36 +50,6 @@ export class BubbleMenuView {
|
|
|
165
50
|
// Also observe the editor view DOM element for editor size changes
|
|
166
51
|
this.resizeObserver.observe(this.view.dom);
|
|
167
52
|
};
|
|
168
|
-
this.focusHandler = () => {
|
|
169
|
-
// we use `setTimeout` to make sure `selection` is already updated
|
|
170
|
-
setTimeout(() => this.update(this.editor.view));
|
|
171
|
-
};
|
|
172
|
-
this.blurHandler = ({ event }) => {
|
|
173
|
-
if (this.preventHide) {
|
|
174
|
-
this.preventHide = false;
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
if (event?.relatedTarget && this.element.parentNode?.contains(event.relatedTarget)) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
if (event?.relatedTarget === this.editor.view.dom) {
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
this.hide();
|
|
184
|
-
};
|
|
185
|
-
this.handleDebouncedUpdate = (view, oldState) => {
|
|
186
|
-
const selectionChanged = !oldState?.selection.eq(view.state.selection);
|
|
187
|
-
const docChanged = !oldState?.doc.eq(view.state.doc);
|
|
188
|
-
if (!selectionChanged && !docChanged) {
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
if (this.updateDebounceTimer) {
|
|
192
|
-
clearTimeout(this.updateDebounceTimer);
|
|
193
|
-
}
|
|
194
|
-
this.updateDebounceTimer = window.setTimeout(() => {
|
|
195
|
-
this.updateHandler(view, selectionChanged, docChanged, oldState);
|
|
196
|
-
}, this.updateDelay);
|
|
197
|
-
};
|
|
198
53
|
this.updateHandler = (view, selectionChanged, docChanged, oldState) => {
|
|
199
54
|
const { composing } = view;
|
|
200
55
|
const isSame = !selectionChanged && !docChanged;
|
|
@@ -209,155 +64,15 @@ export class BubbleMenuView {
|
|
|
209
64
|
this.show();
|
|
210
65
|
this.updatePosition();
|
|
211
66
|
};
|
|
212
|
-
this.editor = editor;
|
|
213
|
-
this.element = element;
|
|
214
|
-
this.view = view;
|
|
215
|
-
this.updateDelay = updateDelay;
|
|
216
|
-
this.resizeDelay = resizeDelay;
|
|
217
|
-
this.container = container;
|
|
218
|
-
this.floatingUIOptions = {
|
|
219
|
-
...this.floatingUIOptions,
|
|
220
|
-
...options,
|
|
221
|
-
};
|
|
222
|
-
this.element.tabIndex = 0;
|
|
223
|
-
if (shouldShow) {
|
|
224
|
-
this.shouldShow = shouldShow;
|
|
225
|
-
}
|
|
226
|
-
const containerElement = this.getContainer();
|
|
227
|
-
if (containerElement === document.body) {
|
|
228
|
-
this.element.style.zIndex = '9999';
|
|
229
|
-
}
|
|
230
|
-
this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true });
|
|
231
|
-
this.view.dom.addEventListener('dragstart', this.dragstartHandler);
|
|
232
|
-
this.editor.on('focus', this.focusHandler);
|
|
233
|
-
this.editor.on('blur', this.blurHandler);
|
|
234
|
-
window.addEventListener('resize', this.windowResizeHandler);
|
|
235
67
|
this.setupResizeObserver();
|
|
236
|
-
this.update(view, view.state);
|
|
237
|
-
if (this.getShouldShow()) {
|
|
238
|
-
this.show();
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
updatePosition() {
|
|
242
|
-
const { selection } = this.editor.state;
|
|
243
|
-
const domRect = posToDOMRect(this.view, selection.from, selection.to);
|
|
244
|
-
let virtualElement = {
|
|
245
|
-
getBoundingClientRect: () => domRect,
|
|
246
|
-
getClientRects: () => [domRect],
|
|
247
|
-
};
|
|
248
|
-
// this is a special case for cell selections
|
|
249
|
-
if (selection instanceof CellSelection) {
|
|
250
|
-
const { $anchorCell, $headCell } = selection;
|
|
251
|
-
const from = $anchorCell ? $anchorCell.pos : $headCell.pos;
|
|
252
|
-
const to = $headCell ? $headCell.pos : $anchorCell.pos;
|
|
253
|
-
const fromDOM = this.view.nodeDOM(from);
|
|
254
|
-
const toDOM = this.view.nodeDOM(to);
|
|
255
|
-
if (!fromDOM || !toDOM) {
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
const clientRect = fromDOM === toDOM
|
|
259
|
-
? fromDOM.getBoundingClientRect()
|
|
260
|
-
: combineDOMRects(fromDOM.getBoundingClientRect(), toDOM.getBoundingClientRect());
|
|
261
|
-
virtualElement = {
|
|
262
|
-
getBoundingClientRect: () => clientRect,
|
|
263
|
-
getClientRects: () => [clientRect],
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
computePosition(virtualElement, this.element, {
|
|
267
|
-
placement: this.floatingUIOptions.placement,
|
|
268
|
-
strategy: this.floatingUIOptions.strategy,
|
|
269
|
-
middleware: this.middlewares,
|
|
270
|
-
}).then(({ x, y, strategy }) => {
|
|
271
|
-
this.element.style.width = 'max-content';
|
|
272
|
-
this.element.style.position = strategy;
|
|
273
|
-
this.element.style.left = `${x}px`;
|
|
274
|
-
this.element.style.top = `${y}px`;
|
|
275
|
-
if (this.isVisible && this.floatingUIOptions.onUpdate) {
|
|
276
|
-
this.floatingUIOptions.onUpdate();
|
|
277
|
-
}
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
update(view, oldState) {
|
|
281
|
-
const { state } = view;
|
|
282
|
-
const hasValidSelection = state.selection.from !== state.selection.to;
|
|
283
|
-
if (this.updateDelay > 0 && hasValidSelection) {
|
|
284
|
-
this.handleDebouncedUpdate(view, oldState);
|
|
285
|
-
return;
|
|
286
|
-
}
|
|
287
|
-
const selectionChanged = !oldState?.selection.eq(view.state.selection);
|
|
288
|
-
const docChanged = !oldState?.doc.eq(view.state.doc);
|
|
289
|
-
this.updateHandler(view, selectionChanged, docChanged, oldState);
|
|
290
|
-
}
|
|
291
|
-
getShouldShow(oldState) {
|
|
292
|
-
const { state } = this.view;
|
|
293
|
-
const { selection } = state;
|
|
294
|
-
// support for CellSelections
|
|
295
|
-
const { ranges } = selection;
|
|
296
|
-
const from = Math.min(...ranges.map(range => range.$from.pos));
|
|
297
|
-
const to = Math.max(...ranges.map(range => range.$to.pos));
|
|
298
|
-
const shouldShow = this.shouldShow?.({
|
|
299
|
-
editor: this.editor,
|
|
300
|
-
element: this.element,
|
|
301
|
-
view: this.view,
|
|
302
|
-
state,
|
|
303
|
-
oldState,
|
|
304
|
-
from,
|
|
305
|
-
to,
|
|
306
|
-
});
|
|
307
|
-
return shouldShow;
|
|
308
|
-
}
|
|
309
|
-
getContainer() {
|
|
310
|
-
if (!this.container) {
|
|
311
|
-
return this.view.dom.parentElement;
|
|
312
|
-
}
|
|
313
|
-
if (typeof this.container === 'function') {
|
|
314
|
-
return this.container();
|
|
315
|
-
}
|
|
316
|
-
return this.container;
|
|
317
|
-
}
|
|
318
|
-
show() {
|
|
319
|
-
if (this.isVisible) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
this.element.style.visibility = 'visible';
|
|
323
|
-
this.element.style.opacity = '1';
|
|
324
|
-
const container = this.getContainer();
|
|
325
|
-
if (container) {
|
|
326
|
-
container.appendChild(this.element);
|
|
327
|
-
}
|
|
328
|
-
if (this.floatingUIOptions.onShow) {
|
|
329
|
-
this.floatingUIOptions.onShow();
|
|
330
|
-
}
|
|
331
|
-
this.isVisible = true;
|
|
332
|
-
}
|
|
333
|
-
hide() {
|
|
334
|
-
if (!this.isVisible) {
|
|
335
|
-
return;
|
|
336
|
-
}
|
|
337
|
-
this.element.style.visibility = 'hidden';
|
|
338
|
-
this.element.style.opacity = '0';
|
|
339
|
-
// remove from the parent element
|
|
340
|
-
this.element.remove();
|
|
341
|
-
if (this.floatingUIOptions.onHide) {
|
|
342
|
-
this.floatingUIOptions.onHide();
|
|
343
|
-
}
|
|
344
|
-
this.isVisible = false;
|
|
345
68
|
}
|
|
346
69
|
destroy() {
|
|
347
|
-
|
|
348
|
-
this.element.removeEventListener('mousedown', this.mousedownHandler, { capture: true });
|
|
349
|
-
this.view.dom.removeEventListener('dragstart', this.dragstartHandler);
|
|
350
|
-
window.removeEventListener('resize', this.windowResizeHandler);
|
|
351
|
-
this.editor.off('focus', this.focusHandler);
|
|
352
|
-
this.editor.off('blur', this.blurHandler);
|
|
70
|
+
super.destroy();
|
|
353
71
|
// Cleanup ResizeObserver
|
|
354
72
|
if (this.resizeObserver) {
|
|
355
73
|
this.resizeObserver.disconnect();
|
|
356
74
|
this.resizeObserver = undefined;
|
|
357
75
|
}
|
|
358
|
-
if (this.floatingUIOptions.onDestroy) {
|
|
359
|
-
this.floatingUIOptions.onDestroy();
|
|
360
|
-
}
|
|
361
76
|
}
|
|
362
77
|
}
|
|
363
78
|
export const BubbleMenuPlugin = (options) => new Plugin({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Extension } from '@tiptap/core';
|
|
2
|
-
import type
|
|
2
|
+
import { type BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu';
|
|
3
3
|
export type BubbleMenuOptions = Omit<BubbleMenuPluginProps, 'editor' | 'element'> & {
|
|
4
4
|
/**
|
|
5
5
|
* The DOM element that contains your menu.
|
|
@@ -11,6 +11,7 @@ export const BubbleMenu = Extension.create({
|
|
|
11
11
|
element: null,
|
|
12
12
|
pluginKey: 'bubbleMenu',
|
|
13
13
|
updateDelay: undefined,
|
|
14
|
+
appendTo: undefined,
|
|
14
15
|
shouldShow: null,
|
|
15
16
|
};
|
|
16
17
|
},
|
|
@@ -24,6 +25,9 @@ export const BubbleMenu = Extension.create({
|
|
|
24
25
|
editor: this.editor,
|
|
25
26
|
element: this.options.element,
|
|
26
27
|
updateDelay: this.options.updateDelay,
|
|
28
|
+
options: this.options.options,
|
|
29
|
+
appendTo: this.options.appendTo,
|
|
30
|
+
getReferencedVirtualElement: this.options.getReferencedVirtualElement,
|
|
27
31
|
shouldShow: this.options.shouldShow,
|
|
28
32
|
}),
|
|
29
33
|
];
|
|
@@ -3,7 +3,7 @@ import { CUSTOM_LINK_EXTENSION_NAME, LINK_TEXT_COLOR } from '../constants';
|
|
|
3
3
|
import { TextSelection } from '@tiptap/pm/state';
|
|
4
4
|
import { dataAttrArrayToObject } from '@antscorp/antsomi-ui/es/utils';
|
|
5
5
|
import { isEmpty, isEqual, map, toString } from 'lodash';
|
|
6
|
-
import { getLinkMarkRanges, getLinkRange, isLinkColor } from '../utils';
|
|
6
|
+
import { getLinkMarkRanges, getLinkRange, isLinkColor, textBetween } from '../utils';
|
|
7
7
|
import { mergeAdjacentRanges } from '../utils/shared';
|
|
8
8
|
export const CustomLink = TiptapLinkExtension.extend({
|
|
9
9
|
name: CUSTOM_LINK_EXTENSION_NAME,
|
|
@@ -40,7 +40,7 @@ export const CustomLink = TiptapLinkExtension.extend({
|
|
|
40
40
|
setCustomLink: ({ attrs, content }) => ({ state, chain }) => {
|
|
41
41
|
const { selection } = state;
|
|
42
42
|
const { from, to } = selection;
|
|
43
|
-
let linkContent =
|
|
43
|
+
let linkContent = textBetween(state, from, to);
|
|
44
44
|
if (content && linkContent !== content) {
|
|
45
45
|
linkContent = content;
|
|
46
46
|
}
|
|
@@ -146,7 +146,7 @@ export const CustomLink = TiptapLinkExtension.extend({
|
|
|
146
146
|
to: state.doc.content.size,
|
|
147
147
|
}).filter(range => predicate(range.mark.attrs));
|
|
148
148
|
mergeAdjacentRanges(linkMarkRanges, (existingRanges, candidateRange) => existingRanges.some(range => mergeAdjacentEqualFn(range.mark.attrs, candidateRange.mark.attrs))).forEach(range => {
|
|
149
|
-
const currentText =
|
|
149
|
+
const currentText = textBetween(state, range.from, range.to);
|
|
150
150
|
const updatedText = updated(currentText);
|
|
151
151
|
if (currentText === updatedText)
|
|
152
152
|
return;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ListItem extension that extends the default TipTap ListItem
|
|
3
|
+
* with custom Backspace behavior to prevent merging paragraphs into lists
|
|
4
|
+
*
|
|
5
|
+
* This extension only overrides keyboard shortcuts while maintaining
|
|
6
|
+
* all other default ListItem functionality (HTML parsing, rendering, etc.)
|
|
7
|
+
*
|
|
8
|
+
* @see https://www.tiptap.dev/api/nodes/list-item
|
|
9
|
+
*/
|
|
10
|
+
export declare const CustomListItem: import("@tiptap/core").Node<import("@tiptap/extension-list").ListItemOptions, any>;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { ListItem } from '@tiptap/extension-list-item';
|
|
2
|
+
/**
|
|
3
|
+
* Custom ListItem extension that extends the default TipTap ListItem
|
|
4
|
+
* with custom Backspace behavior to prevent merging paragraphs into lists
|
|
5
|
+
*
|
|
6
|
+
* This extension only overrides keyboard shortcuts while maintaining
|
|
7
|
+
* all other default ListItem functionality (HTML parsing, rendering, etc.)
|
|
8
|
+
*
|
|
9
|
+
* @see https://www.tiptap.dev/api/nodes/list-item
|
|
10
|
+
*/
|
|
11
|
+
export const CustomListItem = ListItem.extend({
|
|
12
|
+
addOptions() {
|
|
13
|
+
return {
|
|
14
|
+
HTMLAttributes: {
|
|
15
|
+
style: 'line-height:normal;',
|
|
16
|
+
},
|
|
17
|
+
bulletListTypeName: 'customUnorderedList',
|
|
18
|
+
orderedListTypeName: 'customOrderedList',
|
|
19
|
+
};
|
|
20
|
+
},
|
|
21
|
+
addKeyboardShortcuts() {
|
|
22
|
+
return {
|
|
23
|
+
// Keep default keyboard shortcuts from parent ListItem
|
|
24
|
+
Enter: () => this.editor.commands.splitListItem(this.name),
|
|
25
|
+
Tab: () => this.editor.commands.sinkListItem(this.name),
|
|
26
|
+
'Shift-Tab': () => this.editor.commands.liftListItem(this.name),
|
|
27
|
+
/**
|
|
28
|
+
* Custom Backspace handler to prevent merging empty paragraphs into lists
|
|
29
|
+
*
|
|
30
|
+
* Behavior:
|
|
31
|
+
* 1. Check if cursor is at the start of a paragraph
|
|
32
|
+
* 2. Check if the node before is a list (ordered or unordered)
|
|
33
|
+
* 3. If both conditions are true and paragraph is empty, delete the paragraph
|
|
34
|
+
* 4. Otherwise, use default backspace behavior
|
|
35
|
+
*/
|
|
36
|
+
Backspace: () => {
|
|
37
|
+
const { state } = this.editor;
|
|
38
|
+
const { selection } = state;
|
|
39
|
+
const { $from, empty } = selection;
|
|
40
|
+
// Only handle if selection is empty (cursor position, not text selection)
|
|
41
|
+
if (!empty) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Check if cursor is at the start of current node
|
|
45
|
+
const isAtStart = $from.parentOffset === 0;
|
|
46
|
+
if (!isAtStart) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
// Get current node (should be a paragraph)
|
|
50
|
+
const currentNode = $from.parent;
|
|
51
|
+
// Only handle paragraphs
|
|
52
|
+
if (currentNode.type.name !== 'paragraph') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
// Get the index of current paragraph in its parent (usually doc)
|
|
56
|
+
const paragraphDepth = $from.depth;
|
|
57
|
+
const indexInParent = $from.index(paragraphDepth - 1);
|
|
58
|
+
// Check if there's a node before (index > 0)
|
|
59
|
+
if (indexInParent === 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// Get parent node (usually doc) and the node before current paragraph
|
|
63
|
+
const parent = $from.node(paragraphDepth - 1);
|
|
64
|
+
const nodeBefore = parent.child(indexInParent - 1);
|
|
65
|
+
if (!nodeBefore) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// Check if the node before is a list (ordered or unordered)
|
|
69
|
+
const isListBefore = nodeBefore.type.name === this.options.orderedListTypeName ||
|
|
70
|
+
nodeBefore.type.name === this.options.bulletListTypeName;
|
|
71
|
+
if (!isListBefore) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
// Check if current paragraph is empty
|
|
75
|
+
const isParagraphEmpty = currentNode.content.size === 0;
|
|
76
|
+
if (!isParagraphEmpty) {
|
|
77
|
+
// If paragraph has content, allow default backspace to handle
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
// Calculate the exact positions of the paragraph node
|
|
81
|
+
// We need to delete from the position before the paragraph open tag
|
|
82
|
+
// to the position after the paragraph close tag
|
|
83
|
+
const paragraphPos = $from.before();
|
|
84
|
+
const paragraphEndPos = $from.after();
|
|
85
|
+
// Delete the entire paragraph node using deleteRange
|
|
86
|
+
return this.editor.commands.deleteRange({
|
|
87
|
+
from: paragraphPos,
|
|
88
|
+
to: paragraphEndPos,
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
});
|
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { Node } from '@tiptap/core';
|
|
2
2
|
import { Attrs } from '@tiptap/pm/model';
|
|
3
|
-
export type SmartTagAttrs = {
|
|
4
|
-
id: string;
|
|
5
|
-
} & Partial<{
|
|
6
|
-
content: string;
|
|
7
|
-
tag: string;
|
|
8
|
-
}>;
|
|
9
3
|
/**
|
|
10
4
|
* Represents the options for the SmartTag node.
|
|
11
5
|
*/
|
|
12
6
|
export interface SmartTagOptions {
|
|
13
7
|
HTMLAttributes: Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Custom function to generate unique IDs for smart tags
|
|
10
|
+
* If not provided, falls back to uniqid library
|
|
11
|
+
*/
|
|
12
|
+
generateSmartTagId?: () => string;
|
|
14
13
|
}
|
|
15
14
|
/**
|
|
16
15
|
* Represents the options for the SmartTag node.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { Node, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
import uniqid from 'uniqid';
|
|
4
|
+
import { isSmartTagNode } from '../types';
|
|
2
5
|
export const EXTENSION_NAME = 'smartTag';
|
|
3
6
|
/**
|
|
4
7
|
* Represents a dynamic tag node in the editor.
|
|
@@ -12,6 +15,7 @@ export const SmartTag = Node.create({
|
|
|
12
15
|
addOptions() {
|
|
13
16
|
return {
|
|
14
17
|
HTMLAttributes: {},
|
|
18
|
+
generateSmartTagId: () => uniqid(),
|
|
15
19
|
};
|
|
16
20
|
},
|
|
17
21
|
addAttributes() {
|
|
@@ -29,6 +33,11 @@ export const SmartTag = Node.create({
|
|
|
29
33
|
parseHTML: el => el.getAttribute('tag'),
|
|
30
34
|
renderHTML: attrs => (attrs.tag ? { tag: attrs.tag } : null),
|
|
31
35
|
},
|
|
36
|
+
// Transient attribute - not persisted to HTML, used for tracking paste operations
|
|
37
|
+
sourceId: {
|
|
38
|
+
default: null,
|
|
39
|
+
rendered: false, // Don't include in HTML output
|
|
40
|
+
},
|
|
32
41
|
};
|
|
33
42
|
},
|
|
34
43
|
parseHTML() {
|
|
@@ -58,8 +67,9 @@ export const SmartTag = Node.create({
|
|
|
58
67
|
return {
|
|
59
68
|
setSmartTag: attrs => ({ chain, state }) => {
|
|
60
69
|
const { selection, storedMarks } = state;
|
|
61
|
-
const { $from } = selection;
|
|
62
|
-
|
|
70
|
+
const { $from, from, to } = selection;
|
|
71
|
+
// Get marks from inside the selection to capture boundary marks
|
|
72
|
+
const marks = storedMarks || (from !== to ? state.doc.resolve(from + 1).marks() : $from.marks());
|
|
63
73
|
const smartTagNode = this.type.create(attrs);
|
|
64
74
|
const nodeWithMarks = marks.length > 0 ? smartTagNode.mark(marks) : smartTagNode;
|
|
65
75
|
return chain()
|
|
@@ -99,4 +109,86 @@ export const SmartTag = Node.create({
|
|
|
99
109
|
},
|
|
100
110
|
};
|
|
101
111
|
},
|
|
112
|
+
addProseMirrorPlugins() {
|
|
113
|
+
// Smart Tag Deduplication Plugin
|
|
114
|
+
// Responsibility: Set sourceId for pasted smart tags (for external component to handle cloning)
|
|
115
|
+
const deduplicationPlugin = new Plugin({
|
|
116
|
+
key: new PluginKey('smartTagDeduplication'),
|
|
117
|
+
appendTransaction: (transactions, oldState, newState) => {
|
|
118
|
+
// Only process if there was a paste or document change
|
|
119
|
+
const hasPaste = transactions.some(tr => tr.getMeta('paste'));
|
|
120
|
+
const hasDocChange = transactions.some(tr => tr.docChanged);
|
|
121
|
+
if (!hasPaste && !hasDocChange) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Build position mapping from oldState to newState
|
|
125
|
+
// This accounts for position shifts when pasting
|
|
126
|
+
const { mapping } = transactions[0];
|
|
127
|
+
// Build a map of existing smart tag IDs in oldState (before paste)
|
|
128
|
+
const oldStateIds = new Map();
|
|
129
|
+
oldState.doc.descendants((node, pos) => {
|
|
130
|
+
if (isSmartTagNode(node)) {
|
|
131
|
+
oldStateIds.set(node.attrs.id, { pos });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Collect all smart tag IDs in newState (after paste)
|
|
135
|
+
const newStateIds = new Map();
|
|
136
|
+
newState.doc.descendants((node, pos) => {
|
|
137
|
+
if (isSmartTagNode(node)) {
|
|
138
|
+
const { id } = node.attrs;
|
|
139
|
+
if (!newStateIds.has(id)) {
|
|
140
|
+
newStateIds.set(id, []);
|
|
141
|
+
}
|
|
142
|
+
newStateIds.get(id).push({ pos, node });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Find duplicates: IDs that appear multiple times in newState
|
|
146
|
+
const nodesToSetSourceId = new Map();
|
|
147
|
+
newStateIds.forEach((positions, id) => {
|
|
148
|
+
if (positions.length > 1) {
|
|
149
|
+
// This ID has duplicates
|
|
150
|
+
const oldInfo = oldStateIds.get(id);
|
|
151
|
+
if (oldInfo) {
|
|
152
|
+
// Map old position to new position accounting for document changes
|
|
153
|
+
const mappedPos = mapping.map(oldInfo.pos);
|
|
154
|
+
// Filter out the original node (by mapped position), keep only pasted nodes
|
|
155
|
+
const pastedNodes = positions.filter(({ pos }) => pos !== mappedPos);
|
|
156
|
+
if (pastedNodes.length > 0) {
|
|
157
|
+
nodesToSetSourceId.set(id, pastedNodes);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// No original in oldState - all are new (e.g., paste multiple new smart tags)
|
|
162
|
+
// Pick first as "original", rest get sourceId
|
|
163
|
+
const [_first, ...rest] = positions;
|
|
164
|
+
if (rest.length > 0) {
|
|
165
|
+
nodesToSetSourceId.set(id, rest);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// If no duplicates found, no action needed
|
|
171
|
+
if (nodesToSetSourceId.size === 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
// Set sourceId for pasted nodes (do NOT generate new IDs)
|
|
175
|
+
// External component will handle ID generation and attrs cloning
|
|
176
|
+
const { tr } = newState;
|
|
177
|
+
let hasChanges = false;
|
|
178
|
+
nodesToSetSourceId.forEach((positions, sourceId) => {
|
|
179
|
+
positions.forEach(({ pos, node }) => {
|
|
180
|
+
// Only set sourceId, keep original ID
|
|
181
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
182
|
+
...node.attrs,
|
|
183
|
+
sourceId,
|
|
184
|
+
id: this.options.generateSmartTagId?.() || uniqid(),
|
|
185
|
+
});
|
|
186
|
+
hasChanges = true;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
return hasChanges ? tr : null;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return [deduplicationPlugin];
|
|
193
|
+
},
|
|
102
194
|
});
|
|
@@ -2,6 +2,7 @@ import { useBroadcastedLocalStorage, useDeepCompareEffect } from '@antscorp/ants
|
|
|
2
2
|
import { useTextEditorStore } from '../provider';
|
|
3
3
|
import { ANTSOMI_COMPONENT_PREFIX_CLS } from '@antscorp/antsomi-ui/es/constants';
|
|
4
4
|
import { useCallback } from 'react';
|
|
5
|
+
import isEqual from 'react-fast-compare';
|
|
5
6
|
export const useColorSet = () => {
|
|
6
7
|
const colors = useTextEditorStore(s => s.colors);
|
|
7
8
|
const setColors = useTextEditorStore(s => s.setColors);
|
|
@@ -9,9 +10,15 @@ export const useColorSet = () => {
|
|
|
9
10
|
useDeepCompareEffect(() => {
|
|
10
11
|
if (storageColors === undefined && colors !== undefined) {
|
|
11
12
|
setStorageColors(colors);
|
|
13
|
+
return;
|
|
12
14
|
}
|
|
13
15
|
if (colors === undefined && storageColors !== undefined) {
|
|
14
16
|
setColors(storageColors);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
// Sync store → localStorage when both defined but different
|
|
20
|
+
if (colors !== undefined && storageColors !== undefined && !isEqual(colors, storageColors)) {
|
|
21
|
+
setStorageColors(colors);
|
|
15
22
|
}
|
|
16
23
|
}, [colors, storageColors, setColors, setStorageColors]);
|
|
17
24
|
const composeSetColors = useCallback((newColors) => {
|