@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.
Files changed (122) hide show
  1. package/es/components/atoms/InputDynamic/InputDynamic.js +3 -3
  2. package/es/components/atoms/index.d.ts +0 -2
  3. package/es/components/atoms/index.js +0 -2
  4. package/es/components/molecules/AddDynamicContent/AddDynamicContent.js +28 -2
  5. package/es/components/molecules/AddDynamicContent/constants.d.ts +2 -0
  6. package/es/components/molecules/AddDynamicContent/constants.js +2 -0
  7. package/es/components/molecules/EmojiPopover/EmojiPopover.d.ts +1 -0
  8. package/es/components/molecules/EmojiPopover/EmojiPopover.js +2 -2
  9. package/es/components/molecules/EmojiPopover/styled.d.ts +2 -2
  10. package/es/components/molecules/EmojiPopover/styled.js +1 -1
  11. package/es/components/molecules/SearchPopover/SearchPopover.js +2 -2
  12. package/es/components/molecules/SearchPopover/styled.d.ts +12 -1
  13. package/es/components/molecules/SearchPopover/styled.js +1 -2
  14. package/es/components/molecules/SearchPopover/types.d.ts +4 -3
  15. package/es/components/molecules/TagifyInput/TagifyInput.js +55 -19
  16. package/es/components/molecules/TagifyInput/types.d.ts +20 -1
  17. package/es/components/molecules/TagifyInput/utils.d.ts +6 -1
  18. package/es/components/molecules/TagifyInput/utils.js +19 -0
  19. package/es/components/molecules/VirtualizedMenu/VirtualizedMenu.d.ts +1 -0
  20. package/es/components/molecules/VirtualizedMenu/components/Item/Item.js +3 -7
  21. package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.d.ts +10 -10
  22. package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.js +46 -327
  23. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/index.d.ts +9 -0
  24. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/index.js +5 -0
  25. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useFocusManagement.d.ts +23 -0
  26. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useFocusManagement.js +81 -0
  27. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.d.ts +24 -0
  28. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.js +32 -0
  29. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.d.ts +26 -0
  30. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.js +93 -0
  31. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useTreeState.d.ts +24 -0
  32. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useTreeState.js +94 -0
  33. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.d.ts +7 -0
  34. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.js +132 -0
  35. package/es/components/molecules/VirtualizedMenu/styled.js +14 -3
  36. package/es/components/molecules/VirtualizedMenu/types.d.ts +1 -0
  37. package/es/components/molecules/VirtualizedMenu/utils.d.ts +1 -0
  38. package/es/components/molecules/VirtualizedMenu/utils.js +1 -0
  39. package/es/components/organism/ActivityTimeline/ActivityTimeline.js +3 -3
  40. package/es/components/organism/ActivityTimeline/constants.d.ts +9 -9
  41. package/es/components/organism/ActivityTimeline/constants.js +3 -3
  42. package/es/components/organism/ActivityTimeline/index.d.ts +530 -1
  43. package/es/components/organism/ActivityTimeline/index.js +9 -1
  44. package/es/components/organism/ActivityTimeline/utils.js +7 -6
  45. package/es/components/organism/TextEditor/TextEditor.d.ts +9 -2
  46. package/es/components/organism/TextEditor/TextEditor.js +181 -40
  47. package/es/components/organism/TextEditor/constants.d.ts +9 -0
  48. package/es/components/organism/TextEditor/constants.js +66 -0
  49. package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu-plugin.d.ts +6 -128
  50. package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu-plugin.js +7 -292
  51. package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu.d.ts +1 -1
  52. package/es/components/organism/TextEditor/extensions/BubbleMenu/bubble-menu.js +4 -0
  53. package/es/components/organism/TextEditor/extensions/Link.js +3 -3
  54. package/es/components/organism/TextEditor/extensions/ListItem.d.ts +10 -0
  55. package/es/components/organism/TextEditor/extensions/ListItem.js +93 -0
  56. package/es/components/organism/TextEditor/extensions/SmartTag.d.ts +5 -6
  57. package/es/components/organism/TextEditor/extensions/SmartTag.js +94 -2
  58. package/es/components/organism/TextEditor/hooks/useColorSet.js +7 -0
  59. package/es/components/organism/TextEditor/hooks/useMarkTracking.js +2 -1
  60. package/es/components/organism/TextEditor/index.d.ts +7 -5
  61. package/es/components/organism/TextEditor/index.scss +4 -7
  62. package/es/components/organism/TextEditor/provider.d.ts +1 -0
  63. package/es/components/organism/TextEditor/provider.js +6 -3
  64. package/es/components/organism/TextEditor/store.d.ts +11 -4
  65. package/es/components/organism/TextEditor/store.js +22 -2
  66. package/es/components/organism/TextEditor/stories/WithOldDynAndLink/froala-legacy-format.settings.json +95 -0
  67. package/es/components/organism/TextEditor/stories/WithOldDynAndLink/shared.d.ts +111 -0
  68. package/es/components/organism/TextEditor/stories/WithOldDynAndLink/shared.js +82 -0
  69. package/es/components/organism/TextEditor/stories/shared.d.ts +64 -0
  70. package/es/components/organism/TextEditor/stories/shared.js +57 -0
  71. package/es/components/organism/TextEditor/styled.d.ts +1 -1
  72. package/es/components/organism/TextEditor/styled.js +1 -0
  73. package/es/components/organism/TextEditor/types.d.ts +153 -9
  74. package/es/components/organism/TextEditor/types.js +1 -0
  75. package/es/components/organism/TextEditor/ui/BubbleMenu/BubbleMenu.d.ts +2 -1
  76. package/es/components/organism/TextEditor/ui/BubbleMenu/BubbleMenu.js +80 -51
  77. package/es/components/organism/TextEditor/ui/Emoji/EmojiList.js +1 -1
  78. package/es/components/organism/TextEditor/ui/Emoji/suggestion.d.ts +1 -1
  79. package/es/components/organism/TextEditor/ui/Emoji/suggestion.js +2 -2
  80. package/es/components/organism/TextEditor/ui/FontPopover/FontPopover.js +12 -2
  81. package/es/components/organism/TextEditor/ui/LinkInsertForm/LinkInsertForm.d.ts +16 -0
  82. package/es/components/organism/TextEditor/ui/LinkInsertForm/LinkInsertForm.js +61 -0
  83. package/es/components/organism/TextEditor/ui/LinkInsertForm/index.d.ts +2 -0
  84. package/es/components/organism/TextEditor/ui/LinkInsertForm/index.js +1 -0
  85. package/es/components/organism/TextEditor/ui/LinkPopover/LinkPopover.d.ts +9 -0
  86. package/es/components/organism/TextEditor/ui/LinkPopover/LinkPopover.js +126 -0
  87. package/es/components/organism/TextEditor/ui/LinkPopover/index.d.ts +2 -0
  88. package/es/components/organism/TextEditor/ui/LinkPopover/index.js +1 -0
  89. package/es/components/organism/TextEditor/ui/Popover/Popover.js +1 -1
  90. package/es/components/organism/TextEditor/ui/TextAlignSelect/TextAlignSelect.js +1 -3
  91. package/es/components/organism/TextEditor/ui/Toolbar/{Toolbar.d.ts → FormattingToolbar.d.ts} +3 -4
  92. package/es/components/organism/TextEditor/ui/Toolbar/FormattingToolbar.js +85 -0
  93. package/es/components/organism/TextEditor/ui/Toolbar/LinkPreviewToolbar.d.ts +10 -0
  94. package/es/components/organism/TextEditor/ui/Toolbar/LinkPreviewToolbar.js +39 -0
  95. package/es/components/organism/TextEditor/ui/Toolbar/actions/EmojiAction.js +4 -3
  96. package/es/components/organism/TextEditor/ui/Toolbar/actions/FontFamilyAction.d.ts +1 -0
  97. package/es/components/organism/TextEditor/ui/Toolbar/actions/FontFamilyAction.js +2 -2
  98. package/es/components/organism/TextEditor/ui/Toolbar/actions/FontSizeAction.js +6 -1
  99. package/es/components/organism/TextEditor/ui/Toolbar/index.d.ts +2 -1
  100. package/es/components/organism/TextEditor/ui/Toolbar/index.js +2 -1
  101. package/es/components/organism/TextEditor/utils/documentState.d.ts +14 -0
  102. package/es/components/organism/TextEditor/utils/documentState.js +25 -0
  103. package/es/components/organism/TextEditor/utils/htmlProcessing.js +60 -0
  104. package/es/components/organism/TextEditor/utils/link.d.ts +10 -1
  105. package/es/components/organism/TextEditor/utils/link.js +161 -7
  106. package/es/components/organism/TextEditor/utils/menu.js +2 -1
  107. package/es/components/organism/TextEditor/utils/selection.js +3 -2
  108. package/es/components/organism/TextEditor/utils/smartTag.js +2 -1
  109. package/es/components/organism/index.d.ts +1 -1
  110. package/es/hooks/index.d.ts +1 -1
  111. package/es/hooks/index.js +1 -1
  112. package/es/services/MediaTemplateDesign/UploadFile/index.js +4 -4
  113. package/es/types/index.d.ts +1 -1
  114. package/es/types/index.js +1 -1
  115. package/es/utils/common.d.ts +1 -1
  116. package/es/utils/common.js +3 -3
  117. package/package.json +23 -35
  118. package/es/components/organism/TextEditor/ui/BubbleToolbar/BubbleToolbar.d.ts +0 -1
  119. package/es/components/organism/TextEditor/ui/BubbleToolbar/BubbleToolbar.js +0 -1
  120. package/es/components/organism/TextEditor/ui/BubbleToolbar/index.d.ts +0 -0
  121. package/es/components/organism/TextEditor/ui/BubbleToolbar/index.js +0 -1
  122. package/es/components/organism/TextEditor/ui/Toolbar/Toolbar.js +0 -39
@@ -0,0 +1,126 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { posToDOMRect } from '@tiptap/react';
3
+ import { useEffect, useRef, useCallback, useLayoutEffect } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { computePosition, flip, shift, offset } from '@floating-ui/dom';
6
+ import { useTextEditorStore } from '../../provider';
7
+ import { LinkInsertForm } from '../LinkInsertForm';
8
+ import { COMPONENT_CLS } from '../../constants';
9
+ const updatePosition = (editor, element) => {
10
+ const virtualElement = {
11
+ getBoundingClientRect: () => posToDOMRect(editor.view, editor.state.selection.from, editor.state.selection.to),
12
+ };
13
+ computePosition(virtualElement, element, {
14
+ placement: 'bottom-start',
15
+ strategy: 'fixed',
16
+ middleware: [offset(10), shift(), flip()],
17
+ }).then(({ x, y, strategy }) => {
18
+ element.style.position = strategy;
19
+ element.style.left = `${x}px`;
20
+ element.style.top = `${y}px`;
21
+ });
22
+ };
23
+ export const LinkPopover = props => {
24
+ const { editor, container } = props;
25
+ const popoverRef = useRef(null);
26
+ const savedSelectionRef = useRef(null);
27
+ // Get link form state and actions from store
28
+ const linkFormState = useTextEditorStore(state => state.linkFormState);
29
+ const hideLinkForm = useTextEditorStore(state => state.hideLinkForm);
30
+ const isVisible = linkFormState?.isVisible ?? false;
31
+ // Save initial selection when form becomes visible
32
+ useEffect(() => {
33
+ if (isVisible) {
34
+ savedSelectionRef.current = {
35
+ from: editor.state.selection.from,
36
+ to: editor.state.selection.to,
37
+ };
38
+ }
39
+ else {
40
+ savedSelectionRef.current = null;
41
+ }
42
+ }, [editor, isVisible]);
43
+ // Update position whenever editor state changes or form becomes visible
44
+ useLayoutEffect(() => {
45
+ if (!isVisible || !popoverRef.current)
46
+ return;
47
+ updatePosition(editor, popoverRef.current);
48
+ }, [editor, isVisible, linkFormState]);
49
+ // Effect 1: Close on selection change (only if selection actually changed from saved)
50
+ useEffect(() => {
51
+ if (!isVisible || !savedSelectionRef.current)
52
+ return;
53
+ const handleSelectionUpdate = () => {
54
+ const currentSelection = editor.state.selection;
55
+ const savedSelection = savedSelectionRef.current;
56
+ // Only close if selection moved to a different position
57
+ if (savedSelection &&
58
+ (currentSelection.from !== savedSelection.from || currentSelection.to !== savedSelection.to)) {
59
+ hideLinkForm();
60
+ // Editor already has focus when selection changes, no need to refocus
61
+ }
62
+ };
63
+ editor.on('selectionUpdate', handleSelectionUpdate);
64
+ return () => {
65
+ editor.off('selectionUpdate', handleSelectionUpdate);
66
+ };
67
+ }, [editor, isVisible, hideLinkForm]);
68
+ // Effect 2: Close on click outside
69
+ useEffect(() => {
70
+ if (!isVisible)
71
+ return;
72
+ const handleClickOutside = (event) => {
73
+ if (popoverRef.current && !popoverRef.current.contains(event.target)) {
74
+ hideLinkForm();
75
+ // Check if click was on the editor
76
+ const editorElement = editor.view.dom;
77
+ const clickedOnEditor = editorElement.contains(event.target);
78
+ // If not clicked on editor, focus it after closing
79
+ if (!clickedOnEditor) {
80
+ setTimeout(() => {
81
+ editor.commands.focus();
82
+ }, 0);
83
+ }
84
+ // If clicked on editor, it will auto-focus naturally
85
+ }
86
+ };
87
+ // Use mousedown to catch the event before it propagates
88
+ document.addEventListener('mousedown', handleClickOutside);
89
+ return () => {
90
+ document.removeEventListener('mousedown', handleClickOutside);
91
+ };
92
+ }, [editor, isVisible, hideLinkForm]);
93
+ // Handle form submission
94
+ const handleSubmit = useCallback(() => {
95
+ hideLinkForm();
96
+ // Focus back to editor after closing form
97
+ setTimeout(() => {
98
+ editor.commands.focus();
99
+ }, 0);
100
+ }, [editor, hideLinkForm]);
101
+ // Handle form cancellation (also handles Escape key via LinkInsertForm)
102
+ const handleCancel = useCallback(() => {
103
+ hideLinkForm();
104
+ // Focus back to editor after closing form
105
+ setTimeout(() => {
106
+ editor.commands.focus();
107
+ }, 0);
108
+ }, [editor, hideLinkForm]);
109
+ // Don't render if not visible or no form state
110
+ if (!isVisible || !linkFormState) {
111
+ return null;
112
+ }
113
+ const popoverElement = (_jsx("div", { ref: popoverRef, className: `${COMPONENT_CLS}-link-popover`, style: {
114
+ position: 'absolute',
115
+ zIndex: 1000,
116
+ background: '#fff',
117
+ border: '1px solid #d9d9d9',
118
+ borderRadius: '8px',
119
+ padding: '16px',
120
+ boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
121
+ minWidth: '300px',
122
+ }, children: _jsx(LinkInsertForm, { editor: editor, mode: linkFormState.mode, initialData: linkFormState.data, onSubmit: handleSubmit, onCancel: handleCancel }) }));
123
+ // Use portal to render into container or document.body
124
+ const containerElement = (typeof container === 'function' ? container() : container) || document.body;
125
+ return createPortal(popoverElement, containerElement);
126
+ };
@@ -0,0 +1,2 @@
1
+ export { LinkPopover } from './LinkPopover';
2
+ export type { LinkPopoverProps } from './LinkPopover';
@@ -0,0 +1 @@
1
+ export { LinkPopover } from './LinkPopover';
@@ -5,5 +5,5 @@ import { useCallback } from 'react';
5
5
  export const TextEditorPopover = ({ children, ...restProps }) => {
6
6
  const bubbleMenuContainer = useTextEditorStore(state => state.bubbleMenuContainer);
7
7
  const getPopupContainer = useCallback(() => bubbleMenuContainer || document.body, [bubbleMenuContainer]);
8
- return (_jsx(Popover, { trigger: ['click'], ...restProps, getPopupContainer: getPopupContainer, children: children }));
8
+ return (_jsx(Popover, { destroyTooltipOnHide: true, trigger: ['click'], ...restProps, getPopupContainer: getPopupContainer, children: children }));
9
9
  };
@@ -52,9 +52,7 @@ export const TextAlignSelect = (props) => {
52
52
  editor: editor,
53
53
  selector: ({ editor: editorInstance }) => {
54
54
  const alignment = ['left', 'center', 'right', 'justify'].find(alignment => editorInstance?.isActive({ textAlign: alignment }));
55
- return {
56
- currentAlignment: alignment || 'left',
57
- };
55
+ return { currentAlignment: alignment };
58
56
  },
59
57
  });
60
58
  const selectedOption = useMemo(() => textAlignOptions.find(option => option.value === currentAlignment), [currentAlignment]);
@@ -1,7 +1,7 @@
1
1
  import { Editor } from '@tiptap/core';
2
2
  import React from 'react';
3
- import { Config, FontConfig, TextEditorComponentsRender, TextStyle } from '../../types';
4
- export type ToolbarProps = {
3
+ import { Config, FontConfig, TextStyle } from '../../types';
4
+ export type FormattingToolbarProps = {
5
5
  editor: Editor;
6
6
  config?: Config;
7
7
  defaultTextStyle?: TextStyle;
@@ -11,10 +11,9 @@ export type ToolbarProps = {
11
11
  linkHanlder?: {
12
12
  onUpsert?: () => void;
13
13
  };
14
- render?: Partial<Pick<TextEditorComponentsRender, 'renderLinkToolbar'>>;
15
14
  onChangeFont?: (args: {
16
15
  font: FontConfig;
17
16
  weight: number;
18
17
  }) => void;
19
18
  };
20
- export declare const Toolbar: (props: ToolbarProps) => import("react/jsx-runtime").JSX.Element;
19
+ export declare const FormattingToolbar: (props: FormattingToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,85 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Flex } from 'antd';
3
+ import { useMemo } from 'react';
4
+ import { ToolbarWrapper } from '../../styled';
5
+ import { BoldAction, BulletListAction, ClearFormattingAction, EmoijiAction, FontFamilyAction, FontSizeAction, HistoryAction, IndentAction, ItalicAction, LetterSpacingAction, LinkAction, OrderedListAction, OutdentAction, SmartTagAction, LineSpacingAction, StrikeAction, SubscriptAction, SuperscriptAction, TextAlignAction, TextBackColorAction, TextColorAction, TextTransformAction, UnderlineAction, } from './actions';
6
+ import { ALL_TOOLBAR_ACTIONS, DEFAULT_TEXT_STYLE, DEFAULT_TOOLBAR_GROUPING } from '../../constants';
7
+ import { useElementSize } from '@antscorp/antsomi-ui/es/hooks';
8
+ export const FormattingToolbar = (props) => {
9
+ const { editor, smartTagHandler, linkHanlder, config, defaultTextStyle = DEFAULT_TEXT_STYLE, onChangeFont, } = props;
10
+ const [ref] = useElementSize();
11
+ // Determine visible actions based on config
12
+ const visibleActions = useMemo(() => {
13
+ const toolbarConfig = config?.Toolbar;
14
+ // If items is false, hide toolbar entirely
15
+ if (toolbarConfig?.items === false) {
16
+ return [];
17
+ }
18
+ // If items is an array, use it (user-specified order)
19
+ if (Array.isArray(toolbarConfig?.items)) {
20
+ return toolbarConfig.items;
21
+ }
22
+ // Default: show all actions except excluded ones
23
+ const excludeSet = new Set(toolbarConfig?.exclude || []);
24
+ return ALL_TOOLBAR_ACTIONS.filter(action => !excludeSet.has(action));
25
+ }, [config?.Toolbar]);
26
+ // Map action names to React components
27
+ const actionComponentMap = useMemo(() => ({
28
+ fontFamily: (_jsx(FontFamilyAction, { editor: editor, fonts: config?.FontFamily?.fonts, onChange: (font, weight) => onChangeFont?.({ font, weight }), fontGroupingFn: config?.FontFamily?.fontGroupingFn, groupOrder: config?.FontFamily?.groupOrder, defaultFontFamily: defaultTextStyle.fontFamily }, "fontFamily")),
29
+ fontSize: (_jsx(FontSizeAction, { editor: editor, defaultFontSize: defaultTextStyle.fontSize }, "fontSize")),
30
+ bold: _jsx(BoldAction, { editor: editor }, "bold"),
31
+ italic: _jsx(ItalicAction, { editor: editor }, "italic"),
32
+ underline: _jsx(UnderlineAction, { editor: editor }, "underline"),
33
+ link: _jsx(LinkAction, { editor: editor, onClick: linkHanlder?.onUpsert }, "link"),
34
+ strike: _jsx(StrikeAction, { editor: editor }, "strike"),
35
+ superscript: _jsx(SuperscriptAction, { editor: editor }, "superscript"),
36
+ subscript: _jsx(SubscriptAction, { editor: editor }, "subscript"),
37
+ textTransform: _jsx(TextTransformAction, { editor: editor }, "textTransform"),
38
+ textColor: _jsx(TextColorAction, { editor: editor }, "textColor"),
39
+ backgroundColor: _jsx(TextBackColorAction, { editor: editor }, "backgroundColor"),
40
+ emoji: _jsx(EmoijiAction, { editor: editor }, "emoji"),
41
+ smartTag: (_jsx(SmartTagAction, { editor: editor, onClick: smartTagHandler?.onUpsert }, "smartTag")),
42
+ bulletList: (_jsx(BulletListAction, { editor: editor, useCustomBullet: config?.UnorderedList?.useCustomBullet }, "bulletList")),
43
+ orderedList: _jsx(OrderedListAction, { editor: editor }, "orderedList"),
44
+ textAlign: _jsx(TextAlignAction, { editor: editor }, "textAlign"),
45
+ lineSpacing: (_jsx(LineSpacingAction, { editor: editor, defaultValue: defaultTextStyle?.lineHeight }, "lineSpacing")),
46
+ letterSpacing: _jsx(LetterSpacingAction, { editor: editor }, "letterSpacing"),
47
+ indent: _jsx(IndentAction, { editor: editor }, "indent"),
48
+ outdent: _jsx(OutdentAction, { editor: editor }, "outdent"),
49
+ history: _jsx(HistoryAction, { editor: editor }, "history"),
50
+ clearFormatting: _jsx(ClearFormattingAction, { editor: editor }, "clearFormatting"),
51
+ }), [
52
+ editor,
53
+ config?.FontFamily,
54
+ config?.UnorderedList,
55
+ defaultTextStyle,
56
+ linkHanlder,
57
+ smartTagHandler,
58
+ onChangeFont,
59
+ ]);
60
+ // Group actions into rows
61
+ const actionGroups = useMemo(() => {
62
+ if (visibleActions.length === 0) {
63
+ return [];
64
+ }
65
+ // Use default grouping to determine which row each action belongs to
66
+ const row1Actions = DEFAULT_TOOLBAR_GROUPING[0];
67
+ const row2Actions = DEFAULT_TOOLBAR_GROUPING[1];
68
+ const row1 = [];
69
+ const row2 = [];
70
+ visibleActions.forEach(action => {
71
+ if (row1Actions.includes(action)) {
72
+ row1.push(action);
73
+ }
74
+ else if (row2Actions.includes(action)) {
75
+ row2.push(action);
76
+ }
77
+ });
78
+ return [row1, row2].filter(row => row.length > 0);
79
+ }, [visibleActions]);
80
+ // If no visible actions, don't render toolbar
81
+ if (visibleActions.length === 0) {
82
+ return null;
83
+ }
84
+ return (_jsx(ToolbarWrapper, { ref: ref, vertical: true, gap: 20, children: actionGroups.map((group, groupIdx) => (_jsx(Flex, { gap: 15, justify: "flex-start", align: "center", children: group.map(actionName => actionComponentMap[actionName]) }, groupIdx))) }));
85
+ };
@@ -0,0 +1,10 @@
1
+ import { Editor } from '@tiptap/core';
2
+ import { TextEditorComponentsRender } from '../../types';
3
+ export type LinkPreviewToolbarProps = {
4
+ editor: Editor;
5
+ linkHanlder?: {
6
+ onUpsert?: () => void;
7
+ };
8
+ render?: Partial<Pick<TextEditorComponentsRender, 'renderLinkToolbar'>>;
9
+ };
10
+ export declare const LinkPreviewToolbar: (props: LinkPreviewToolbarProps) => import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,39 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CopyDuplicateIcon, EditIcon } from '@antscorp/antsomi-ui/es/components/icons';
3
+ import { useEditorState } from '@tiptap/react';
4
+ import { Typography } from 'antd';
5
+ import { ToolbarWrapper } from '../../styled';
6
+ import { isShowLinkToolbar } from '../../utils';
7
+ import { TextEditorButton } from '../Button';
8
+ import { UnsetLinkAction } from './actions';
9
+ import { CUSTOM_LINK_EXTENSION_NAME } from '../../constants';
10
+ export const LinkPreviewToolbar = (props) => {
11
+ const { editor, linkHanlder, render } = props;
12
+ const { showLinkMenu, linkAttrs } = useEditorState({
13
+ editor,
14
+ selector: ctx => ({
15
+ showLinkMenu: isShowLinkToolbar(ctx.editor.state),
16
+ linkAttrs: editor.getAttributes(CUSTOM_LINK_EXTENSION_NAME),
17
+ }),
18
+ });
19
+ if (!showLinkMenu) {
20
+ return null;
21
+ }
22
+ const href = String(linkAttrs?.href);
23
+ const goToLinkEl = (_jsx(Typography.Link, { style: { maxWidth: 150 }, ellipsis: true, href: href, children: href }));
24
+ const copyLinkEl = (_jsx(TextEditorButton, { icon: _jsx(CopyDuplicateIcon, { size: 18 }), tooltipProps: { title: 'Copy' }, onClick: () => {
25
+ navigator.clipboard.writeText(href);
26
+ } }));
27
+ const editLinkEl = (_jsx(TextEditorButton, { icon: _jsx(EditIcon, { size: 18 }), tooltipProps: { title: 'Edit' }, onClick: () => linkHanlder?.onUpsert?.() }));
28
+ const unsetLinkEl = _jsx(UnsetLinkAction, { editor: editor });
29
+ let linkTools = (_jsxs(_Fragment, { children: [goToLinkEl, copyLinkEl, editLinkEl, unsetLinkEl] }));
30
+ if (render?.renderLinkToolbar && linkAttrs) {
31
+ linkTools = render.renderLinkToolbar(linkAttrs, linkTools, {
32
+ copy: copyLinkEl,
33
+ edit: editLinkEl,
34
+ gotoLink: goToLinkEl,
35
+ unlink: unsetLinkEl,
36
+ });
37
+ }
38
+ return (_jsx(ToolbarWrapper, { wrap: "wrap", gap: 8, align: "center", children: linkTools }));
39
+ };
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useCallback } from 'react';
3
3
  import { EmojiSmileIcon } from '@antscorp/antsomi-ui/es/components/icons';
4
- import { EMOJI_COLLECTIONS, EmojiPopover } from '@antscorp/antsomi-ui/es/components/molecules';
4
+ import { EMOJI_COLLECTIONS } from '@antscorp/antsomi-ui/es/components/molecules/TagifyInput';
5
+ import { EmojiPopover } from '@antscorp/antsomi-ui/es/components/molecules/EmojiPopover';
5
6
  import { TextEditorButton } from '../../Button';
6
7
  import { TEXT_EDITOR_CONSTANTS } from '../../../constants';
7
8
  import { useTextEditorStore } from '../../../provider';
@@ -16,7 +17,7 @@ export const EmoijiAction = ({ editor }) => {
16
17
  }),
17
18
  });
18
19
  const getPopupContainer = useCallback(() => bubbleMenuContainer || document.body, [bubbleMenuContainer]);
19
- return (_jsx(EmojiPopover, { collections: [
20
+ return (_jsx(EmojiPopover, { isForceHide: true, collections: [
20
21
  {
21
22
  key: EMOJI_COLLECTIONS.COMMON,
22
23
  label: _jsx(EmojiSmileIcon, {}),
@@ -25,7 +26,7 @@ export const EmoijiAction = ({ editor }) => {
25
26
  if (typeof emoji !== 'string')
26
27
  return;
27
28
  editor?.chain().insertEmoji(emoji);
28
- }, getPopupContainer: getPopupContainer, children: _jsx(TextEditorButton, { disabled: disabled, tooltipProps: {
29
+ }, getPopupContainer: getPopupContainer, destroyTooltipOnHide: true, children: _jsx(TextEditorButton, { disabled: disabled, tooltipProps: {
29
30
  title: TOOLTIPS.EMOJI.TITLE,
30
31
  }, icon: _jsx(EmojiSmileIcon, { size: 18 }) }) }));
31
32
  };
@@ -14,5 +14,6 @@ export interface FontFamilyActionProps {
14
14
  groupOrder?: string[] | GroupOrderFunction;
15
15
  /** Callback when font selection changes */
16
16
  onChange?: (font: FontConfig, weight: number) => void;
17
+ defaultFontFamily?: string;
17
18
  }
18
19
  export declare const FontFamilyAction: (props: FontFamilyActionProps) => import("react/jsx-runtime").JSX.Element;
@@ -5,7 +5,7 @@ import { getPrimaryFontFamily } from '@antscorp/antsomi-ui/es/utils';
5
5
  import { useEditorState } from '@tiptap/react';
6
6
  import { useCallback, useEffect, useRef } from 'react';
7
7
  export const FontFamilyAction = (props) => {
8
- const { editor, fonts = DEFAULT_FONT_CONFIGS, onChange, fontGroupingFn, groupOrder } = props;
8
+ const { editor, fonts = DEFAULT_FONT_CONFIGS, onChange, fontGroupingFn, groupOrder, defaultFontFamily, } = props;
9
9
  const onChangeRef = useRef();
10
10
  useEffect(() => {
11
11
  onChangeRef.current = onChange;
@@ -15,7 +15,7 @@ export const FontFamilyAction = (props) => {
15
15
  selector: ({ editor: editorInstance }) => {
16
16
  const { fontWeight, fontFamily } = editorInstance.getAttributes('textStyle') || {};
17
17
  return {
18
- fontValue: getPrimaryFontFamily(fontFamily),
18
+ fontValue: getPrimaryFontFamily(fontFamily || defaultFontFamily),
19
19
  fontWeight: Number(fontWeight),
20
20
  };
21
21
  },
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { useTextEditorStore } from '../../../provider';
3
3
  import { FontSizeInput, } from '@antscorp/antsomi-ui/es/components/molecules';
4
4
  import styled from 'styled-components';
5
- import { memo, useCallback, useMemo, useRef, useState } from 'react';
5
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
6
  import { useThrottledCallback } from 'use-debounce';
7
7
  import { useEditorState } from '@tiptap/react';
8
8
  import { toString } from 'lodash';
@@ -30,6 +30,11 @@ export const FontSizeAction = memo(({ editor, throttled = 100, defaultFontSize =
30
30
  },
31
31
  });
32
32
  const [currentFontSize, setCurrentFontSize] = useState(numberFontSize);
33
+ useEffect(() => {
34
+ if (numberFontSize) {
35
+ setCurrentFontSize(numberFontSize);
36
+ }
37
+ }, [numberFontSize]);
33
38
  const updateEditorFontSize = useThrottledCallback((v) => {
34
39
  if (v === numberFontSize)
35
40
  return;
@@ -1 +1,2 @@
1
- export { Toolbar, type ToolbarProps } from './Toolbar';
1
+ export { LinkPreviewToolbar, type LinkPreviewToolbarProps } from './LinkPreviewToolbar';
2
+ export { FormattingToolbar, type FormattingToolbarProps } from './FormattingToolbar';
@@ -1 +1,2 @@
1
- export { Toolbar } from './Toolbar';
1
+ export { LinkPreviewToolbar } from './LinkPreviewToolbar';
2
+ export { FormattingToolbar } from './FormattingToolbar';
@@ -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