@antscorp/antsomi-ui 1.3.7-beta.2 → 1.3.7-beta.21

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.
Files changed (43) hide show
  1. package/es/components/atoms/index.d.ts +0 -2
  2. package/es/components/atoms/index.js +0 -2
  3. package/es/components/molecules/TagifyInput/TagifyInput.js +48 -18
  4. package/es/components/molecules/TagifyInput/types.d.ts +20 -1
  5. package/es/components/molecules/TagifyInput/utils.d.ts +3 -1
  6. package/es/components/molecules/TagifyInput/utils.js +9 -0
  7. package/es/components/organism/ActivityTimeline/ActivityTimeline.js +3 -3
  8. package/es/components/organism/ActivityTimeline/components/ItemEvent/ItemEvent.js +7 -1
  9. package/es/components/organism/ActivityTimeline/components/ItemGroupEvent/ItemGroupEvent.js +14 -2
  10. package/es/components/organism/ActivityTimeline/constants.d.ts +9 -9
  11. package/es/components/organism/ActivityTimeline/constants.js +3 -3
  12. package/es/components/organism/ActivityTimeline/index.d.ts +530 -1
  13. package/es/components/organism/ActivityTimeline/index.js +9 -1
  14. package/es/components/organism/ActivityTimeline/utils.d.ts +7 -1
  15. package/es/components/organism/ActivityTimeline/utils.js +10 -7
  16. package/es/components/organism/TextEditor/TextEditor.d.ts +8 -2
  17. package/es/components/organism/TextEditor/TextEditor.js +76 -3
  18. package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu-plugin.js +3 -2
  19. package/es/components/organism/TextEditor/extensions/Link.js +3 -3
  20. package/es/components/organism/TextEditor/extensions/SmartTag.d.ts +0 -6
  21. package/es/components/organism/TextEditor/extensions/SmartTag.js +3 -2
  22. package/es/components/organism/TextEditor/hooks/useMarkTracking.js +2 -1
  23. package/es/components/organism/TextEditor/index.d.ts +6 -5
  24. package/es/components/organism/TextEditor/stories/WithOldDynAndLink/settings.json +95 -0
  25. package/es/components/organism/TextEditor/types.d.ts +75 -2
  26. package/es/components/organism/TextEditor/types.js +1 -0
  27. package/es/components/organism/TextEditor/utils/documentState.d.ts +14 -0
  28. package/es/components/organism/TextEditor/utils/documentState.js +25 -0
  29. package/es/components/organism/TextEditor/utils/htmlProcessing.js +60 -0
  30. package/es/components/organism/TextEditor/utils/link.d.ts +10 -1
  31. package/es/components/organism/TextEditor/utils/link.js +161 -7
  32. package/es/components/organism/TextEditor/utils/menu.js +2 -1
  33. package/es/components/organism/TextEditor/utils/selection.js +3 -2
  34. package/es/components/organism/TextEditor/utils/smartTag.js +2 -1
  35. package/es/components/organism/index.d.ts +1 -1
  36. package/es/hooks/index.d.ts +1 -1
  37. package/es/hooks/index.js +1 -1
  38. package/es/services/MediaTemplateDesign/UploadFile/index.js +4 -4
  39. package/es/types/index.d.ts +1 -1
  40. package/es/types/index.js +1 -1
  41. package/es/utils/common.d.ts +1 -1
  42. package/es/utils/common.js +3 -3
  43. package/package.json +11 -23
@@ -28,13 +28,14 @@ import { SmartTag } from './extensions/SmartTag';
28
28
  import { TextTransform } from './extensions/TextTransform';
29
29
  import { CustomUnorderedList } from './extensions/UnorderedList';
30
30
  import { ClearFormatting } from './extensions/ClearFormatting';
31
- import { useTextEditorStore } from './provider';
31
+ import { TextEditorProvider, useTextEditorStore } from './provider';
32
32
  import { StyledEditorContent } from './styled';
33
+ import { isSmartTagNode, } from './types';
33
34
  import { emojiSuggestion } from './ui/Emoji';
34
35
  import { Toolbar } from './ui/Toolbar';
35
- import { analyzeFont, defaultShouldShowBubbleMenu, handleLinkAction, handleSmartTagAction, isShowLinkToolbar, safeParseHTMLContent, htmlSerializerForOutput, } from './utils';
36
+ import { analyzeFont, defaultShouldShowBubbleMenu, getAllFullLinkGroups, handleLinkAction, handleSmartTagAction, isShowLinkToolbar, safeParseHTMLContent, htmlSerializerForOutput, } from './utils';
36
37
  import { BubbleMenu } from './ui/BubbleMenu';
37
- export const TextEditor = memo(forwardRef((props, ref) => {
38
+ const TextEditorInternal = memo(forwardRef((props, ref) => {
38
39
  const { id, className, config, editable = true, initialContent = '', dataAttributes, onUpdateDebounced = 400, defaultTextStyle: defaultTextStyleProp, linkHandler: outerLinkHandler, smartTagHandler: outerSmartTagHandler, bubbleMenuProps, style, render, onUpdate, onFocus, onChangeFont, } = props;
39
40
  const isShowBubbleMenu = useTextEditorStore(state => state.isShowBubbleMenu);
40
41
  const setBubbleMenuContainer = useTextEditorStore(state => state.setBubbleMenuContainer);
@@ -203,6 +204,23 @@ export const TextEditor = memo(forwardRef((props, ref) => {
203
204
  editor?.chain().blur().setTextSelection({ from: 0, to: 0 }).run();
204
205
  }
205
206
  }, [editor]);
207
+ const handleEachLinkGroup = useCallback(callback => {
208
+ if (!editor)
209
+ return;
210
+ const linkGroups = getAllFullLinkGroups(editor.state);
211
+ linkGroups.forEach(callback);
212
+ }, [editor]);
213
+ const handleEachSmartag = useCallback(callback => {
214
+ if (!editor)
215
+ return;
216
+ const { state } = editor;
217
+ const { doc } = state;
218
+ doc.descendants(node => {
219
+ if (isSmartTagNode(node)) {
220
+ callback(node.attrs);
221
+ }
222
+ });
223
+ }, [editor]);
206
224
  const handleBubbleMenuRef = useCallback((htmlElement) => {
207
225
  setBubbleMenuContainer(htmlElement);
208
226
  }, [setBubbleMenuContainer]);
@@ -226,6 +244,8 @@ export const TextEditor = memo(forwardRef((props, ref) => {
226
244
  })
227
245
  .run();
228
246
  },
247
+ eachLinkGroup: handleEachLinkGroup,
248
+ eachSmartag: handleEachSmartag,
229
249
  setSmartTag: handleSetSmartTag,
230
250
  deleteSmartTag: handleDeleteSmartTag,
231
251
  updateSmartTagAttrs: handleUpdateSmartTagAttrs,
@@ -256,3 +276,56 @@ export const TextEditor = memo(forwardRef((props, ref) => {
256
276
  });
257
277
  } })) })] }));
258
278
  }));
279
+ TextEditorInternal.displayName = 'TextEditorInternal';
280
+ const TextEditorWithProvider = forwardRef((props, ref) => {
281
+ const { onChangeColors, ...editorProps } = props;
282
+ const providerRef = useRef(null);
283
+ const editorRef = useRef(null);
284
+ useImperativeHandle(ref, () => ({
285
+ // Forward TextEditorRef methods - accessed at call time
286
+ get style() {
287
+ return editorRef.current?.style;
288
+ },
289
+ get editor() {
290
+ return editorRef.current?.editor ?? null;
291
+ },
292
+ get setLink() {
293
+ return editorRef.current.setLink;
294
+ },
295
+ get deleteLink() {
296
+ return editorRef.current.deleteLink;
297
+ },
298
+ get updateLinkAttrs() {
299
+ return editorRef.current.updateLinkAttrs;
300
+ },
301
+ get updateLinkText() {
302
+ return editorRef.current.updateLinkText;
303
+ },
304
+ get eachLinkGroup() {
305
+ return editorRef.current.eachLinkGroup;
306
+ },
307
+ get eachSmartag() {
308
+ return editorRef.current.eachSmartag;
309
+ },
310
+ get setSmartTag() {
311
+ return editorRef.current.setSmartTag;
312
+ },
313
+ get deleteSmartTag() {
314
+ return editorRef.current.deleteSmartTag;
315
+ },
316
+ get updateSmartTagAttrs() {
317
+ return editorRef.current.updateSmartTagAttrs;
318
+ },
319
+ get blur() {
320
+ return editorRef.current.blur;
321
+ },
322
+ // Forward TextEditorProviderRefHandler methods
323
+ updateColors: colors => providerRef.current?.updateColors?.(colors),
324
+ }), []);
325
+ return (_jsx(TextEditorProvider, { ref: providerRef, onChangeColors: onChangeColors, children: _jsx(TextEditorInternal, { ...editorProps, ref: editorRef }) }));
326
+ });
327
+ TextEditorWithProvider.displayName = 'TextEditorWithProvider';
328
+ export const TextEditor = Object.assign(TextEditorWithProvider, {
329
+ Provider: TextEditorProvider,
330
+ Internal: TextEditorInternal,
331
+ });
@@ -2,6 +2,7 @@ import { arrow, autoPlacement, computePosition, flip, hide, inline, offset, shif
2
2
  import { isTextSelection, posToDOMRect } from '@tiptap/core';
3
3
  import { Plugin, PluginKey } from '@tiptap/pm/state';
4
4
  import { CellSelection } from '@tiptap/pm/tables';
5
+ import { textBetween } from '../../utils';
5
6
  function combineDOMRects(rect1, rect2) {
6
7
  const top = Math.min(rect1.top, rect2.top);
7
8
  const bottom = Math.max(rect1.bottom, rect2.bottom);
@@ -76,12 +77,12 @@ export class BubbleMenuView {
76
77
  onDestroy: undefined,
77
78
  };
78
79
  this.shouldShow = ({ view, state, from, to, }) => {
79
- const { doc, selection } = state;
80
+ const { selection } = state;
80
81
  const { empty } = selection;
81
82
  // Sometime check for `empty` is not enough.
82
83
  // Doubleclick an empty paragraph returns a node size of 2.
83
84
  // So we check also for an empty text size.
84
- const isEmptyTextBlock = !doc.textBetween(from, to).length && isTextSelection(state.selection);
85
+ const isEmptyTextBlock = !textBetween(state, from, to).length && isTextSelection(state.selection);
85
86
  // When clicking on a element inside the bubble menu the editor "blur" event
86
87
  // is called and the bubble menu item is focussed. In this case we should
87
88
  // consider the menu as part of the editor and keep showing the menu
@@ -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 = state.doc.textBetween(from, to);
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 = state.doc.textBetween(range.from, range.to);
149
+ const currentText = textBetween(state, range.from, range.to);
150
150
  const updatedText = updated(currentText);
151
151
  if (currentText === updatedText)
152
152
  return;
@@ -1,11 +1,5 @@
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
  */
@@ -58,8 +58,9 @@ export const SmartTag = Node.create({
58
58
  return {
59
59
  setSmartTag: attrs => ({ chain, state }) => {
60
60
  const { selection, storedMarks } = state;
61
- const { $from } = selection;
62
- const marks = storedMarks || $from.marks();
61
+ const { $from, from, to } = selection;
62
+ // Get marks from inside the selection to capture boundary marks
63
+ const marks = storedMarks || (from !== to ? state.doc.resolve(from + 1).marks() : $from.marks());
63
64
  const smartTagNode = this.type.create(attrs);
64
65
  const nodeWithMarks = marks.length > 0 ? smartTagNode.mark(marks) : smartTagNode;
65
66
  return chain()
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useState } from 'react';
2
+ import { textBetween } from '../utils';
2
3
  export function useMarkTracking(editor, options = {}) {
3
4
  const { maxSnapshots = 10, excludeMarks = ['link', 'smartTag'], debounceDelay = 500, autoCapture = true, } = options;
4
5
  const [snapshots, setSnapshots] = useState([]);
@@ -14,7 +15,7 @@ export function useMarkTracking(editor, options = {}) {
14
15
  marks,
15
16
  position: $from.pos,
16
17
  timestamp: Date.now(),
17
- textContent: editor.state.doc.textBetween(Math.max(0, $from.pos - 50), Math.min(editor.state.doc.content.size, $from.pos + 50)),
18
+ textContent: textBetween(editor.state, Math.max(0, $from.pos - 50), Math.min(editor.state.doc.content.size, $from.pos + 50)),
18
19
  };
19
20
  setSnapshots(prev => {
20
21
  const updated = [snapshot, ...prev];
@@ -1,14 +1,15 @@
1
1
  /// <reference types="react" />
2
2
  import { JSONContent } from '@tiptap/core';
3
- export type { TextEditorProps, TextEditorRef, TextEditorComponentsRender, LinkAttrs, FontConfig, } from './types';
3
+ export type { TextEditorProps, TextEditorAllProps, TextEditorRef, TextEditorWithProviderRef, TextEditorComponentsRender, LinkAttrs, FontConfig, } from './types';
4
4
  export { isLinkMark, isLinkMarkRange } from './types';
5
5
  export { CUSTOM_LINK_EXTENSION_NAME } from './constants';
6
6
  export type { JSONContent as TextEditorJSONContent };
7
7
  export { TextEditorProvider, type TextEditorProviderProps, type TextEditorProviderRefHandler, } from './provider';
8
- export declare const TextEditor: import("react").NamedExoticComponent<Omit<import("./types").TextEditorProps & import("react").RefAttributes<import("./types").TextEditorRef>, "ref"> & {
9
- ref?: ((instance: import("./types").TextEditorRef | null) => void) | import("react").RefObject<import("./types").TextEditorRef> | null | undefined;
10
- }> & {
11
- readonly type: import("react").ForwardRefExoticComponent<import("./types").TextEditorProps & import("react").RefAttributes<import("./types").TextEditorRef>>;
8
+ export declare const TextEditor: import("react").ForwardRefExoticComponent<import("./types").TextEditorProps & {
9
+ onChangeColors?: ((colors: string[]) => void) | undefined;
10
+ } & import("react").RefAttributes<import("./types").TextEditorWithProviderRef>> & {
11
+ Provider: import("react").ForwardRefExoticComponent<import("./provider").TextEditorProviderProps & import("react").RefAttributes<import("./provider").TextEditorProviderRefHandler>>;
12
+ Internal: import("react").MemoExoticComponent<import("react").ForwardRefExoticComponent<import("./types").TextEditorProps & import("react").RefAttributes<import("./types").TextEditorRef>>>;
12
13
  } & {
13
14
  Utils: {
14
15
  htmlMinifyForEmail: (htmlEditorContent: string) => string;
@@ -0,0 +1,95 @@
1
+ {
2
+ "rawHTML": "<div class=\"fr-element fr-view\" dir=\"auto\" contenteditable=\"true\" aria-disabled=\"false\" spellcheck=\"true\"><p><span style=\"font-family: Montserrat; font-size: 22px; letter-spacing: 0px; color: #000000;\"><a data-link-id=\"j24jwuk2\" target=\"_blank\" href=\"https://ant.design/components/modal\"><span data-dynamic=\"true\" data-dynamic-id=\"i6fapgk6\" style=\"direction: unset; unicode-bidi: bidi-override; background-color: rgba(0, 199, 97, 0.2);\">Last modified by</span></a> Special <a data-link-id=\"rrd48wh5\" target=\"_blank\" href=\"https://ant.design\">Bonus <span data-dynamic=\"true\" data-dynamic-id=\"4ip5g3w4\" style=\"direction: unset; unicode-bidi: bidi-override; background-color: rgba(0, 199, 97, 0.2);\">Created date</span> Has</a> Been <span data-dynamic=\"true\" data-dynamic-id=\"vd3tmfc3\" style=\"direction: unset; unicode-bidi: bidi-override; background-color: rgba(0, 199, 97, 0.2);\"><a data-link-id=\"ir1h7e67\" target=\"_blank\" href=\"https://fireup.pro/news/goodbye-useeffect-exploring-use-in-react-19?ref=dailydev\">Name</a></span>&nbsp;Special <a data-link-id=\"rrd48wh5\" href=\"https://ant.design\" target=\"_blank\" id=\"isPasted\">Bonus</a>&nbsp;fasdfas&nbsp; sdfadf</span></p></div>",
3
+ "dynamic": {
4
+ "data": {
5
+ "4ip5g3w4": {
6
+ "type": "visitor-attribute",
7
+ "attribute": {
8
+ "label": "Created date",
9
+ "value": "date_created",
10
+ "status": 1,
11
+ "dataType": "datetime",
12
+ "disabled": false,
13
+ "datetimeFormatSettings": {
14
+ "type": "datetime",
15
+ "language": "en",
16
+ "hasDateFormat": true,
17
+ "hasTimeFormat": true,
18
+ "dateParseFormat": "MM/DD/YYYY",
19
+ "dateParseOption": "medium",
20
+ "timeParseFormat": "12hour",
21
+ "timeParseOption": "medium",
22
+ "dateFormatString": "MMM DD, YYYY h:mm:ss A"
23
+ }
24
+ },
25
+ "mappingKey": "lg5pk39o2qmxyc7oano5-4ip5g3w4",
26
+ "mappingFields": "visitor.date_created"
27
+ },
28
+ "i6fapgk6": {
29
+ "type": "event-attribute",
30
+ "event": "226539:17",
31
+ "source": [556657814],
32
+ "attribute": {
33
+ "type": 1,
34
+ "label": "Last modified by",
35
+ "value": "ad_zone.u_user_id",
36
+ "status": 1,
37
+ "children": [],
38
+ "dataType": "number",
39
+ "itemTypeId": -1013,
40
+ "itemTypeName": "ad_zone",
41
+ "propertyName": "u_user_id",
42
+ "eventPropertySyntax": "dims.ad_zone_id",
43
+ "numberFormatSettings": {
44
+ "type": "number",
45
+ "decimal": ".",
46
+ "grouping": ",",
47
+ "isCompact": false,
48
+ "prefixType": "code",
49
+ "currencyCode": "USD",
50
+ "decimalPlaces": 2
51
+ }
52
+ },
53
+ "mappingKey": "lg5pk39o2qmxyc7oano5-i6fapgk6",
54
+ "mappingFields": "event.ad_zone.u_user_id"
55
+ },
56
+ "vd3tmfc3": {
57
+ "type": "customer-attribute",
58
+ "attribute": {
59
+ "label": "Name",
60
+ "value": "name",
61
+ "status": 1,
62
+ "dataType": "string",
63
+ "disabled": false
64
+ },
65
+ "mappingKey": "lg5pk39o2qmxyc7oano5-vd3tmfc3",
66
+ "mappingFields": "customer.name"
67
+ }
68
+ },
69
+ "highlight": true,
70
+ "selectedId": ""
71
+ },
72
+ "link": {
73
+ "data": {
74
+ "ir1h7e67": {
75
+ "url": "https://fireup.pro/news/goodbye-useeffect-exploring-use-in-react-19?ref=dailydev",
76
+ "text": "Name",
77
+ "linkType": "static",
78
+ "openNewTab": true
79
+ },
80
+ "j24jwuk2": {
81
+ "url": "https://ant.design/components/modal",
82
+ "text": "Your",
83
+ "linkType": "static",
84
+ "openNewTab": true
85
+ },
86
+ "rrd48wh5": {
87
+ "url": "https://ant.design",
88
+ "text": "Bonus Offer Has",
89
+ "linkType": "static",
90
+ "openNewTab": true
91
+ }
92
+ },
93
+ "selectedId": ""
94
+ }
95
+ }
@@ -1,7 +1,6 @@
1
1
  import type React from 'react';
2
2
  import type { JSONContent, Editor, MarkRange } from '@tiptap/core';
3
- import type { Mark } from '@tiptap/pm/model';
4
- import type { SmartTagAttrs } from './extensions/SmartTag';
3
+ import type { Mark, Node } from '@tiptap/pm/model';
5
4
  import { ORDERED_LIST_STYLE_TYPE, UNORDERED_LIST_STYLE_TYPE } from './constants';
6
5
  import type { OverrideProperties } from 'type-fest';
7
6
  import { BubbleMenuPluginProps } from './extensions/BubbleMenu';
@@ -22,6 +21,11 @@ export type HandleLinkRef = {
22
21
  updated: (currentText: string) => string;
23
22
  mergeAdjacentEqualFn?: (currentAttrs: LinkAttrs, candidateAttrs: LinkAttrs) => boolean;
24
23
  }) => void;
24
+ eachLinkGroup: (callback: (params: {
25
+ attrs: LinkAttrs;
26
+ content: string;
27
+ }) => void) => void;
28
+ eachSmartag: (callback: (attrs: SmartTagAttrs) => void) => void;
25
29
  };
26
30
  export type TextEditorRef = HandleSmartTagRef & HandleLinkRef & {
27
31
  blur: (perserveSelection?: boolean) => void;
@@ -142,6 +146,12 @@ export type TextEditorProps = {
142
146
  };
143
147
  }) => void;
144
148
  };
149
+ export type TextEditorAllProps = TextEditorProps & {
150
+ onChangeColors?: (colors: string[]) => void;
151
+ };
152
+ export type TextEditorWithProviderRef = TextEditorRef & {
153
+ updateColors?: (colors: string[]) => void;
154
+ };
145
155
  export type OrderedListStyleType = (typeof ORDERED_LIST_STYLE_TYPE)[keyof typeof ORDERED_LIST_STYLE_TYPE];
146
156
  export type UnorderedListStyleType = (typeof UNORDERED_LIST_STYLE_TYPE)[keyof typeof UNORDERED_LIST_STYLE_TYPE];
147
157
  export declare const isOrderedListStyleType: (value: unknown) => value is OrderedListStyleType;
@@ -155,9 +165,18 @@ export type LinkAttrs = {
155
165
  title: string | null;
156
166
  data: Record<string, string>;
157
167
  }>;
168
+ export type SmartTagAttrs = {
169
+ id: string;
170
+ } & Partial<{
171
+ content: string;
172
+ tag: string;
173
+ }>;
158
174
  export type LinkMark = OverrideProperties<Mark, {
159
175
  attrs: LinkAttrs;
160
176
  }>;
177
+ export type SmartTagNode = OverrideProperties<Node, {
178
+ attrs: SmartTagAttrs;
179
+ }>;
161
180
  export type LinkMarkRange = OverrideProperties<MarkRange, {
162
181
  mark: LinkMark;
163
182
  }>;
@@ -170,6 +189,60 @@ export declare const isLinkMark: (mark: Mark) => mark is {
170
189
  toJSON: () => any;
171
190
  attrs: LinkAttrs;
172
191
  };
192
+ export declare const isSmartTagNode: (node: Node) => node is {
193
+ toString: () => string;
194
+ readonly type: import("prosemirror-model").NodeType;
195
+ readonly marks: readonly Mark[];
196
+ readonly content: import("prosemirror-model").Fragment;
197
+ readonly children: readonly Node[];
198
+ readonly text: string | undefined;
199
+ readonly nodeSize: number;
200
+ readonly childCount: number;
201
+ child: (index: number) => Node;
202
+ maybeChild: (index: number) => Node | null;
203
+ forEach: (f: (node: Node, offset: number, index: number) => void) => void;
204
+ nodesBetween: (from: number, to: number, f: (node: Node, pos: number, parent: Node | null, index: number) => boolean | void, startPos?: number | undefined) => void;
205
+ descendants: (f: (node: Node, pos: number, parent: Node | null, index: number) => boolean | void) => void;
206
+ readonly textContent: string;
207
+ textBetween: (from: number, to: number, blockSeparator?: string | null | undefined, leafText?: string | ((leafNode: Node) => string) | null | undefined) => string;
208
+ readonly firstChild: Node | null;
209
+ readonly lastChild: Node | null;
210
+ eq: (other: Node) => boolean;
211
+ sameMarkup: (other: Node) => boolean;
212
+ hasMarkup: (type: import("prosemirror-model").NodeType, attrs?: import("prosemirror-model").Attrs | null | undefined, marks?: readonly Mark[] | undefined) => boolean;
213
+ copy: (content?: import("prosemirror-model").Fragment | null | undefined) => Node;
214
+ mark: (marks: readonly Mark[]) => Node;
215
+ cut: (from: number, to?: number | undefined) => Node;
216
+ slice: (from: number, to?: number | undefined, includeParents?: boolean | undefined) => import("prosemirror-model").Slice;
217
+ replace: (from: number, to: number, slice: import("prosemirror-model").Slice) => Node;
218
+ nodeAt: (pos: number) => Node | null;
219
+ childAfter: (pos: number) => {
220
+ node: Node | null;
221
+ index: number;
222
+ offset: number;
223
+ };
224
+ childBefore: (pos: number) => {
225
+ node: Node | null;
226
+ index: number;
227
+ offset: number;
228
+ };
229
+ resolve: (pos: number) => import("prosemirror-model").ResolvedPos;
230
+ rangeHasMark: (from: number, to: number, type: import("prosemirror-model").MarkType | Mark) => boolean;
231
+ readonly isBlock: boolean;
232
+ readonly isTextblock: boolean;
233
+ readonly inlineContent: boolean;
234
+ readonly isInline: boolean;
235
+ readonly isText: boolean;
236
+ readonly isLeaf: boolean;
237
+ readonly isAtom: boolean;
238
+ contentMatchAt: (index: number) => import("prosemirror-model").ContentMatch;
239
+ canReplace: (from: number, to: number, replacement?: import("prosemirror-model").Fragment | undefined, start?: number | undefined, end?: number | undefined) => boolean;
240
+ canReplaceWith: (from: number, to: number, type: import("prosemirror-model").NodeType, marks?: readonly Mark[] | undefined) => boolean;
241
+ canAppend: (other: Node) => boolean;
242
+ check: () => void;
243
+ toJSON: () => any;
244
+ attrs: SmartTagAttrs;
245
+ };
173
246
  export declare const isLinkMarkRange: (markRange: MarkRange) => markRange is {
174
247
  from: number;
175
248
  to: number;
@@ -2,4 +2,5 @@ import { CUSTOM_LINK_EXTENSION_NAME, ORDERED_LIST_STYLE_TYPE, UNORDERED_LIST_STY
2
2
  export const isOrderedListStyleType = (value) => typeof value === 'string' && Object.values(ORDERED_LIST_STYLE_TYPE).some(v => v === value);
3
3
  export const isUnorderedListStyleType = (value) => typeof value === 'string' && Object.values(UNORDERED_LIST_STYLE_TYPE).some(v => v === value);
4
4
  export const isLinkMark = (mark) => mark.type.name === CUSTOM_LINK_EXTENSION_NAME;
5
+ export const isSmartTagNode = (node) => node.type.name === 'smartTag';
5
6
  export const isLinkMarkRange = (markRange) => isLinkMark(markRange.mark);
@@ -57,3 +57,17 @@ export declare class DocumentStateTracker {
57
57
  reset(): void;
58
58
  }
59
59
  export declare function isEntireParagraphSelected(editor: Editor): boolean;
60
+ /**
61
+ * Helper function to extract text from atom nodes (emoji, smartTag)
62
+ * Used as leafText callback for doc.textBetween()
63
+ */
64
+ export declare const getTextFromAtomNode: (node: ProseMirrorNode) => string;
65
+ /**
66
+ * Wrapper for doc.textBetween that automatically handles atom nodes (emoji, smartTag)
67
+ * @param state EditorState
68
+ * @param from Start position
69
+ * @param to End position
70
+ * @param blockSeparator Optional separator between blocks (default: '')
71
+ * @returns Text content including atom nodes
72
+ */
73
+ export declare const textBetween: (state: EditorState, from: number, to: number, blockSeparator?: string) => string;
@@ -1,3 +1,4 @@
1
+ import { LIST_EMOJI } from '../extensions/Emoji';
1
2
  /**
2
3
  * Default configuration for document state detection
3
4
  */
@@ -122,3 +123,27 @@ export function isEntireParagraphSelected(editor) {
122
123
  // Kiểm tra xem selection có bao phủ toàn bộ nội dung của paragraph không
123
124
  return from === paragraphStart && to === paragraphEnd;
124
125
  }
126
+ /**
127
+ * Helper function to extract text from atom nodes (emoji, smartTag)
128
+ * Used as leafText callback for doc.textBetween()
129
+ */
130
+ export const getTextFromAtomNode = (node) => {
131
+ if (node.type.name === 'emoji') {
132
+ // Find emoji character from name
133
+ const emojiChar = LIST_EMOJI.find(i => i.name === node.attrs.name)?.emoji;
134
+ return emojiChar || node.attrs.name || '*';
135
+ }
136
+ if (node.type.name === 'smartTag') {
137
+ return node.attrs.content || '*';
138
+ }
139
+ return '*';
140
+ };
141
+ /**
142
+ * Wrapper for doc.textBetween that automatically handles atom nodes (emoji, smartTag)
143
+ * @param state EditorState
144
+ * @param from Start position
145
+ * @param to End position
146
+ * @param blockSeparator Optional separator between blocks (default: '')
147
+ * @returns Text content including atom nodes
148
+ */
149
+ export const textBetween = (state, from, to, blockSeparator = '') => state.doc.textBetween(from, to, blockSeparator, getTextFromAtomNode);
@@ -237,6 +237,64 @@ export function cleanLineBreaks(html) {
237
237
  return html;
238
238
  }
239
239
  }
240
+ /**
241
+ * Swaps the structure when a dynamic tag contains only a single link.
242
+ * Changes from: <span data-dynamic><a>text</a></span>
243
+ * To: <a><span data-dynamic>text</span></a>
244
+ *
245
+ * @param html - HTML string to process
246
+ * @returns Processed HTML string with swapped structure
247
+ */
248
+ function swapDynamicTagWithLink(html) {
249
+ try {
250
+ const parser = new DOMParser();
251
+ const doc = parser.parseFromString(html, 'text/html');
252
+ // Find all dynamic tag spans
253
+ const dynamicSpans = doc.querySelectorAll('span[data-dynamic="true"]');
254
+ dynamicSpans.forEach(dynamicSpan => {
255
+ // Get all child nodes (elements and text nodes)
256
+ const childNodes = Array.from(dynamicSpan.childNodes);
257
+ // Filter out empty text nodes (whitespace only)
258
+ const nonEmptyNodes = childNodes.filter(node => {
259
+ if (node.nodeType === Node.TEXT_NODE) {
260
+ return node.textContent?.trim() !== '';
261
+ }
262
+ return true;
263
+ });
264
+ // Check if there's exactly one child and it's an <a> element
265
+ if (nonEmptyNodes.length === 1 && nonEmptyNodes[0].nodeType === Node.ELEMENT_NODE) {
266
+ const childElement = nonEmptyNodes[0];
267
+ if (childElement instanceof HTMLElement && childElement.tagName.toLowerCase() === 'a') {
268
+ const linkElement = childElement;
269
+ // Create new span with dynamic attributes
270
+ const newSpan = doc.createElement('span');
271
+ // Copy all attributes from original dynamic span
272
+ Array.from(dynamicSpan.attributes).forEach(attr => {
273
+ newSpan.setAttribute(attr.name, attr.value);
274
+ });
275
+ // Set the text content from the link
276
+ newSpan.textContent = linkElement.textContent;
277
+ // Create new link element
278
+ const newLink = doc.createElement('a');
279
+ // Copy all attributes from original link
280
+ Array.from(linkElement.attributes).forEach(attr => {
281
+ newLink.setAttribute(attr.name, attr.value);
282
+ });
283
+ // Put span inside link
284
+ newLink.appendChild(newSpan);
285
+ // Replace original dynamic span with the new link
286
+ dynamicSpan.replaceWith(newLink);
287
+ }
288
+ }
289
+ });
290
+ return doc.body.innerHTML;
291
+ }
292
+ catch (error) {
293
+ // eslint-disable-next-line no-console
294
+ console.error('Error swapping dynamic tag with link:', error);
295
+ return html; // Return original HTML in case of error
296
+ }
297
+ }
240
298
  /**
241
299
  * Safely parses and processes HTML content for editor use
242
300
  * @param html - HTML string to process
@@ -260,6 +318,8 @@ export function safeParseHTMLContent(params) {
260
318
  defaultStyle,
261
319
  });
262
320
  }
321
+ // Swap dynamic tag structure when it contains only a link
322
+ resultHTML = swapDynamicTagWithLink(resultHTML);
263
323
  // console.log('after safeParseHTMLContent', resultHTML);
264
324
  return resultHTML;
265
325
  }
@@ -78,7 +78,7 @@ export declare const getActiveLinkAttrsFromRange: (params: {
78
78
  readonly inactiveLinks: import("../types").LinkAttrs[];
79
79
  } | undefined;
80
80
  /**
81
- * Extends selection to include full link marks
81
+ * Extends selection to include full link marks and adjacent links with same href
82
82
  * @param state EditorState
83
83
  * @param from Start position
84
84
  * @param to End position
@@ -93,6 +93,15 @@ export declare const extendSelectionToFullLinks: (state: EditorState, from: numb
93
93
  to: number;
94
94
  linkAttrs: import("../types").LinkAttrs;
95
95
  };
96
+ /**
97
+ * Gets all full link groups in the document (adjacent links with same href are grouped)
98
+ * @param state EditorState
99
+ * @returns Array of link groups with attrs and content
100
+ */
101
+ export declare const getAllFullLinkGroups: (state: EditorState) => {
102
+ attrs: LinkMarkRange['mark']['attrs'];
103
+ content: string;
104
+ }[];
96
105
  /**
97
106
  * Handles link actions (create or edit)
98
107
  * @param view EditorView instance