@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
|
@@ -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 @@
|
|
|
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]);
|
package/es/components/organism/TextEditor/ui/Toolbar/{Toolbar.d.ts → FormattingToolbar.d.ts}
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Editor } from '@tiptap/core';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { Config, FontConfig,
|
|
4
|
-
export type
|
|
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
|
|
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
|
|
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 {
|
|
1
|
+
export { LinkPreviewToolbar, type LinkPreviewToolbarProps } from './LinkPreviewToolbar';
|
|
2
|
+
export { FormattingToolbar, type FormattingToolbarProps } from './FormattingToolbar';
|
|
@@ -1 +1,2 @@
|
|
|
1
|
-
export {
|
|
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
|