@antscorp/antsomi-ui 2.0.111 → 2.0.112
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/organism/TextEditor/TextEditor.js +27 -12
- package/es/components/organism/TextEditor/extensions/Link.js +31 -12
- package/es/components/organism/TextEditor/extensions/SmartTag.d.ts +5 -0
- package/es/components/organism/TextEditor/extensions/SmartTag.js +95 -4
- package/es/components/organism/TextEditor/types.d.ts +17 -4
- package/es/components/organism/TextEditor/ui/Toolbar/FormattingToolbar.js +1 -1
- 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/utils/link.js +12 -12
- package/package.json +1 -1
|
@@ -38,7 +38,7 @@ import { analyzeFont, defaultShouldShowBubbleMenu, getAllFullLinkGroups, handleL
|
|
|
38
38
|
import { BubbleMenu } from './ui/BubbleMenu';
|
|
39
39
|
import { LinkPopover } from './ui/LinkPopover';
|
|
40
40
|
const TextEditorInternal = memo(forwardRef((props, ref) => {
|
|
41
|
-
const { id, className, config, editable = true, initialContent = '', dataAttributes, onUpdateDebounced = 300, defaultTextStyle: defaultTextStyleProp, linkHandler: outerLinkHandler, smartTagHandler: outerSmartTagHandler, bubbleMenuProps, style, render, onUpdate, onFocus, onChangeFont, } = props;
|
|
41
|
+
const { id, className, config, editable = true, initialContent = '', dataAttributes, onUpdateDebounced = 300, defaultTextStyle: defaultTextStyleProp, linkHandler: outerLinkHandler, smartTagHandler: outerSmartTagHandler, bubbleMenuProps, style, render, onUpdate, onFocus, onCreate, onChangeFont, } = props;
|
|
42
42
|
// Initialize bubbleMenuContainer with default value (document.body)
|
|
43
43
|
// This ensures BubbleMenu receives a valid appendTo prop on first render
|
|
44
44
|
const bubbleMenuContainer = useRef((() => {
|
|
@@ -106,7 +106,7 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
|
|
|
106
106
|
bulletList: false,
|
|
107
107
|
orderedList: false,
|
|
108
108
|
listItem: false,
|
|
109
|
-
undoRedo: {
|
|
109
|
+
undoRedo: config?.UndoRedo ?? {
|
|
110
110
|
depth: 100,
|
|
111
111
|
},
|
|
112
112
|
}),
|
|
@@ -153,7 +153,9 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
|
|
|
153
153
|
Focus.configure({
|
|
154
154
|
className: 'has-focused',
|
|
155
155
|
}),
|
|
156
|
-
SmartTag
|
|
156
|
+
SmartTag.configure({
|
|
157
|
+
generateSmartTagId: config?.SmartTag?.generateId,
|
|
158
|
+
}),
|
|
157
159
|
TextAlign.configure({
|
|
158
160
|
types: ['heading', 'paragraph'],
|
|
159
161
|
}),
|
|
@@ -187,6 +189,14 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
|
|
|
187
189
|
if (safeParseContent !== initialContent) {
|
|
188
190
|
editor.commands.setContent(safeParseContent);
|
|
189
191
|
}
|
|
192
|
+
const text = editor.getText();
|
|
193
|
+
const json = editor.getJSON();
|
|
194
|
+
const html = editor.getHTML();
|
|
195
|
+
onCreate?.({
|
|
196
|
+
html: htmlSerializerForOutput(html),
|
|
197
|
+
text,
|
|
198
|
+
json,
|
|
199
|
+
});
|
|
190
200
|
},
|
|
191
201
|
onUpdate: ({ editor }) => {
|
|
192
202
|
handleOnUpdateDebounce(editor);
|
|
@@ -291,23 +301,20 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
|
|
|
291
301
|
})
|
|
292
302
|
.run();
|
|
293
303
|
},
|
|
304
|
+
getHTML: () => htmlSerializerForOutput(editor.getHTML()),
|
|
294
305
|
eachLinkGroup: handleEachLinkGroup,
|
|
295
306
|
eachSmartag: handleEachSmartag,
|
|
296
307
|
setSmartTag: handleSetSmartTag,
|
|
297
308
|
deleteSmartTag: handleDeleteSmartTag,
|
|
298
309
|
updateSmartTagAttrs: handleUpdateSmartTagAttrs,
|
|
299
310
|
blur: handleBlur,
|
|
311
|
+
setContent: editor.commands.setContent,
|
|
300
312
|
style: contentRef.current?.style,
|
|
301
|
-
editor,
|
|
302
313
|
}));
|
|
303
314
|
if (!editor) {
|
|
304
315
|
return null;
|
|
305
316
|
}
|
|
306
|
-
|
|
307
|
-
// Inline styles apply to inner html elements (support email client)
|
|
308
|
-
...defaultTextStyle,
|
|
309
|
-
...style,
|
|
310
|
-
}, id: id, ref: contentRef, editor: editor, ...dataAttributes }), _jsx(LinkPopover, { editor: editor }), !linkFormVisible && (_jsxs(_Fragment, { children: [_jsx(BubbleMenu, { pluginKey: "linkPreviewBubbleMenu", ...bubbleMenuProps, appendTo: bubbleMenuContainer.current, editor: editor, shouldShow: shouldShowLinkPreview, children: _jsx(LinkPreviewToolbar, { editor: editor, linkHanlder: {
|
|
317
|
+
const bubbleMenus = (bubbleMenuProps?.enabled ?? true) ? (_jsxs(_Fragment, { children: [_jsx(LinkPopover, { editor: editor }), !linkFormVisible && (_jsxs(_Fragment, { children: [_jsx(BubbleMenu, { pluginKey: "linkPreviewBubbleMenu", ...bubbleMenuProps, appendTo: bubbleMenuContainer.current, editor: editor, shouldShow: shouldShowLinkPreview, children: _jsx(LinkPreviewToolbar, { editor: editor, linkHanlder: {
|
|
311
318
|
onUpsert: () => {
|
|
312
319
|
handleLinkAction(editor.view, linkHandler);
|
|
313
320
|
},
|
|
@@ -325,7 +332,12 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
|
|
|
325
332
|
weight,
|
|
326
333
|
analysis: analyzeFont(font),
|
|
327
334
|
});
|
|
328
|
-
} }) })] }))] }));
|
|
335
|
+
} }) })] }))] })) : null;
|
|
336
|
+
return (_jsxs(_Fragment, { children: [_jsx(StyledEditorContent, { className: clsx(`${ANTSOMI_COMPONENT_PREFIX_CLS}-text-editor`, className), "$textStyle": defaultTextStyle, style: {
|
|
337
|
+
// Inline styles apply to inner html elements (support email client)
|
|
338
|
+
...defaultTextStyle,
|
|
339
|
+
...style,
|
|
340
|
+
}, id: id, ref: contentRef, editor: editor, ...dataAttributes }), bubbleMenus] }));
|
|
329
341
|
}));
|
|
330
342
|
TextEditorInternal.displayName = 'TextEditorInternal';
|
|
331
343
|
const TextEditorWithProvider = forwardRef((props, ref) => {
|
|
@@ -343,8 +355,8 @@ const TextEditorWithProvider = forwardRef((props, ref) => {
|
|
|
343
355
|
get style() {
|
|
344
356
|
return editorRef.current?.style;
|
|
345
357
|
},
|
|
346
|
-
get
|
|
347
|
-
return editorRef.current
|
|
358
|
+
get getHTML() {
|
|
359
|
+
return editorRef.current.getHTML;
|
|
348
360
|
},
|
|
349
361
|
get setLink() {
|
|
350
362
|
return editorRef.current.setLink;
|
|
@@ -376,6 +388,9 @@ const TextEditorWithProvider = forwardRef((props, ref) => {
|
|
|
376
388
|
get blur() {
|
|
377
389
|
return editorRef.current.blur;
|
|
378
390
|
},
|
|
391
|
+
get setContent() {
|
|
392
|
+
return editorRef.current.setContent;
|
|
393
|
+
},
|
|
379
394
|
// Forward TextEditorProviderRefHandler methods
|
|
380
395
|
updateColors: colors => providerRef.current?.updateColors?.(colors),
|
|
381
396
|
}), []);
|
|
@@ -40,22 +40,41 @@ export const CustomLink = TiptapLinkExtension.extend({
|
|
|
40
40
|
setCustomLink: ({ attrs, content }) => ({ state, chain }) => {
|
|
41
41
|
const { selection } = state;
|
|
42
42
|
const { from, to } = selection;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// Check if custom content is provided and different from current text
|
|
44
|
+
const currentText = textBetween(state, from, to);
|
|
45
|
+
const shouldUseCustomContent = content && currentText !== content;
|
|
46
|
+
if (shouldUseCustomContent) {
|
|
47
|
+
// Use custom content as string (replace content)
|
|
48
|
+
return chain()
|
|
49
|
+
.focus()
|
|
50
|
+
.deleteSelection()
|
|
51
|
+
.insertContent(content)
|
|
52
|
+
.command(({ tr }) => {
|
|
53
|
+
const endPos = tr.selection.from;
|
|
54
|
+
const startPos = endPos - content.length;
|
|
55
|
+
tr.setSelection(TextSelection.create(tr.doc, startPos, endPos));
|
|
56
|
+
return true;
|
|
57
|
+
})
|
|
58
|
+
.setLink({
|
|
59
|
+
...attrs,
|
|
60
|
+
href: attrs?.href || './',
|
|
61
|
+
})
|
|
62
|
+
.command(({ tr }) => {
|
|
63
|
+
const endPos = tr.selection.to;
|
|
64
|
+
tr.setSelection(TextSelection.create(tr.doc, endPos, endPos));
|
|
65
|
+
return true;
|
|
66
|
+
})
|
|
67
|
+
.run();
|
|
46
68
|
}
|
|
47
|
-
|
|
69
|
+
// Preserve existing content structure (atom nodes like smartTag)
|
|
70
|
+
// Just apply link mark on current selection without modifying content
|
|
71
|
+
return chain()
|
|
48
72
|
.focus()
|
|
49
|
-
.deleteSelection()
|
|
50
|
-
.insertContent(linkContent)
|
|
51
73
|
.command(({ tr }) => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
tr.setSelection(TextSelection.create(tr.doc, startPos, endPos));
|
|
74
|
+
// Ensure selection is set correctly
|
|
75
|
+
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
|
55
76
|
return true;
|
|
56
77
|
})
|
|
57
|
-
// .setUnderline()
|
|
58
|
-
// .setColor(LINK_TEXT_COLOR)
|
|
59
78
|
.setLink({
|
|
60
79
|
...attrs,
|
|
61
80
|
href: attrs?.href || './',
|
|
@@ -65,7 +84,7 @@ export const CustomLink = TiptapLinkExtension.extend({
|
|
|
65
84
|
tr.setSelection(TextSelection.create(tr.doc, endPos, endPos));
|
|
66
85
|
return true;
|
|
67
86
|
})
|
|
68
|
-
.run()
|
|
87
|
+
.run();
|
|
69
88
|
},
|
|
70
89
|
deleteCustomLink: predicate => ({ state, chain }) => {
|
|
71
90
|
const { doc } = state;
|
|
@@ -5,6 +5,11 @@ import { Attrs } from '@tiptap/pm/model';
|
|
|
5
5
|
*/
|
|
6
6
|
export interface SmartTagOptions {
|
|
7
7
|
HTMLAttributes: Record<string, unknown>;
|
|
8
|
+
/**
|
|
9
|
+
* Custom function to generate unique IDs for smart tags
|
|
10
|
+
* If not provided, falls back to uniqid library
|
|
11
|
+
*/
|
|
12
|
+
generateSmartTagId?: () => string;
|
|
8
13
|
}
|
|
9
14
|
/**
|
|
10
15
|
* Represents the options for the SmartTag node.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { Node, mergeAttributes } from '@tiptap/core';
|
|
1
|
+
import { Node, getMarksBetween, mergeAttributes } from '@tiptap/core';
|
|
2
|
+
import { Plugin, PluginKey } from '@tiptap/pm/state';
|
|
3
|
+
import uniqid from 'uniqid';
|
|
4
|
+
import { isSmartTagNode } from '../types';
|
|
2
5
|
export const EXTENSION_NAME = 'smartTag';
|
|
3
6
|
/**
|
|
4
7
|
* Represents a dynamic tag node in the editor.
|
|
@@ -12,6 +15,7 @@ export const SmartTag = Node.create({
|
|
|
12
15
|
addOptions() {
|
|
13
16
|
return {
|
|
14
17
|
HTMLAttributes: {},
|
|
18
|
+
generateSmartTagId: () => uniqid(),
|
|
15
19
|
};
|
|
16
20
|
},
|
|
17
21
|
addAttributes() {
|
|
@@ -29,6 +33,11 @@ export const SmartTag = Node.create({
|
|
|
29
33
|
parseHTML: el => el.getAttribute('tag'),
|
|
30
34
|
renderHTML: attrs => (attrs.tag ? { tag: attrs.tag } : null),
|
|
31
35
|
},
|
|
36
|
+
// Transient attribute - not persisted to HTML, used for tracking paste operations
|
|
37
|
+
sourceId: {
|
|
38
|
+
default: null,
|
|
39
|
+
rendered: false, // Don't include in HTML output
|
|
40
|
+
},
|
|
32
41
|
};
|
|
33
42
|
},
|
|
34
43
|
parseHTML() {
|
|
@@ -57,10 +66,10 @@ export const SmartTag = Node.create({
|
|
|
57
66
|
addCommands() {
|
|
58
67
|
return {
|
|
59
68
|
setSmartTag: attrs => ({ chain, state }) => {
|
|
60
|
-
const { selection
|
|
61
|
-
const {
|
|
69
|
+
const { selection } = state;
|
|
70
|
+
const { from, to } = selection;
|
|
62
71
|
// Get marks from inside the selection to capture boundary marks
|
|
63
|
-
const marks =
|
|
72
|
+
const marks = getMarksBetween(from, to, state.doc).map(markRange => markRange.mark);
|
|
64
73
|
const smartTagNode = this.type.create(attrs);
|
|
65
74
|
const nodeWithMarks = marks.length > 0 ? smartTagNode.mark(marks) : smartTagNode;
|
|
66
75
|
return chain()
|
|
@@ -100,4 +109,86 @@ export const SmartTag = Node.create({
|
|
|
100
109
|
},
|
|
101
110
|
};
|
|
102
111
|
},
|
|
112
|
+
addProseMirrorPlugins() {
|
|
113
|
+
// Smart Tag Deduplication Plugin
|
|
114
|
+
// Responsibility: Set sourceId for pasted smart tags (for external component to handle cloning)
|
|
115
|
+
const deduplicationPlugin = new Plugin({
|
|
116
|
+
key: new PluginKey('smartTagDeduplication'),
|
|
117
|
+
appendTransaction: (transactions, oldState, newState) => {
|
|
118
|
+
// Only process if there was a paste or document change
|
|
119
|
+
const hasPaste = transactions.some(tr => tr.getMeta('paste'));
|
|
120
|
+
const hasDocChange = transactions.some(tr => tr.docChanged);
|
|
121
|
+
if (!hasPaste && !hasDocChange) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
// Build position mapping from oldState to newState
|
|
125
|
+
// This accounts for position shifts when pasting
|
|
126
|
+
const { mapping } = transactions[0];
|
|
127
|
+
// Build a map of existing smart tag IDs in oldState (before paste)
|
|
128
|
+
const oldStateIds = new Map();
|
|
129
|
+
oldState.doc.descendants((node, pos) => {
|
|
130
|
+
if (isSmartTagNode(node)) {
|
|
131
|
+
oldStateIds.set(node.attrs.id, { pos });
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
// Collect all smart tag IDs in newState (after paste)
|
|
135
|
+
const newStateIds = new Map();
|
|
136
|
+
newState.doc.descendants((node, pos) => {
|
|
137
|
+
if (isSmartTagNode(node)) {
|
|
138
|
+
const { id } = node.attrs;
|
|
139
|
+
if (!newStateIds.has(id)) {
|
|
140
|
+
newStateIds.set(id, []);
|
|
141
|
+
}
|
|
142
|
+
newStateIds.get(id).push({ pos, node });
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
// Find duplicates: IDs that appear multiple times in newState
|
|
146
|
+
const nodesToSetSourceId = new Map();
|
|
147
|
+
newStateIds.forEach((positions, id) => {
|
|
148
|
+
if (positions.length > 1) {
|
|
149
|
+
// This ID has duplicates
|
|
150
|
+
const oldInfo = oldStateIds.get(id);
|
|
151
|
+
if (oldInfo) {
|
|
152
|
+
// Map old position to new position accounting for document changes
|
|
153
|
+
const mappedPos = mapping.map(oldInfo.pos);
|
|
154
|
+
// Filter out the original node (by mapped position), keep only pasted nodes
|
|
155
|
+
const pastedNodes = positions.filter(({ pos }) => pos !== mappedPos);
|
|
156
|
+
if (pastedNodes.length > 0) {
|
|
157
|
+
nodesToSetSourceId.set(id, pastedNodes);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// No original in oldState - all are new (e.g., paste multiple new smart tags)
|
|
162
|
+
// Pick first as "original", rest get sourceId
|
|
163
|
+
const [_first, ...rest] = positions;
|
|
164
|
+
if (rest.length > 0) {
|
|
165
|
+
nodesToSetSourceId.set(id, rest);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
// If no duplicates found, no action needed
|
|
171
|
+
if (nodesToSetSourceId.size === 0) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
// Set sourceId for pasted nodes (do NOT generate new IDs)
|
|
175
|
+
// External component will handle ID generation and attrs cloning
|
|
176
|
+
const { tr } = newState;
|
|
177
|
+
let hasChanges = false;
|
|
178
|
+
nodesToSetSourceId.forEach((positions, sourceId) => {
|
|
179
|
+
positions.forEach(({ pos, node }) => {
|
|
180
|
+
// Only set sourceId, keep original ID
|
|
181
|
+
tr.setNodeMarkup(pos, undefined, {
|
|
182
|
+
...node.attrs,
|
|
183
|
+
sourceId,
|
|
184
|
+
id: this.options.generateSmartTagId?.() || uniqid(),
|
|
185
|
+
});
|
|
186
|
+
hasChanges = true;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
return hasChanges ? tr : null;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
return [deduplicationPlugin];
|
|
193
|
+
},
|
|
103
194
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from 'react';
|
|
2
|
-
import type { JSONContent,
|
|
2
|
+
import type { JSONContent, MarkRange } from '@tiptap/core';
|
|
3
3
|
import { BubbleMenuPluginProps } from '@tiptap/extension-bubble-menu';
|
|
4
4
|
import type { Mark, Node } from '@tiptap/pm/model';
|
|
5
5
|
import { ORDERED_LIST_STYLE_TYPE, UNORDERED_LIST_STYLE_TYPE } from './constants';
|
|
@@ -7,7 +7,7 @@ import type { OverrideProperties } from 'type-fest';
|
|
|
7
7
|
export type HandleSmartTagRef = {
|
|
8
8
|
setSmartTag: (attrs: SmartTagAttrs) => void;
|
|
9
9
|
deleteSmartTag: (id: string) => void;
|
|
10
|
-
updateSmartTagAttrs: (id: string, updatedAttrs:
|
|
10
|
+
updateSmartTagAttrs: (id: string, updatedAttrs: Partial<SmartTagAttrs>) => void;
|
|
11
11
|
};
|
|
12
12
|
export type HandleLinkRef = {
|
|
13
13
|
setLink: (params: {
|
|
@@ -30,7 +30,8 @@ export type HandleLinkRef = {
|
|
|
30
30
|
export type TextEditorRef = HandleSmartTagRef & HandleLinkRef & {
|
|
31
31
|
blur: (perserveSelection?: boolean) => void;
|
|
32
32
|
style?: CSSStyleDeclaration;
|
|
33
|
-
|
|
33
|
+
getHTML?: () => string;
|
|
34
|
+
setContent?: (text: string) => void;
|
|
34
35
|
};
|
|
35
36
|
export type SmartTagHandler = Partial<{
|
|
36
37
|
edit: (id: string, event: React.MouseEvent) => void;
|
|
@@ -126,6 +127,7 @@ export type ToolbarConfig = {
|
|
|
126
127
|
* Configuration options for TextEditor components
|
|
127
128
|
*/
|
|
128
129
|
export type Config = Partial<{
|
|
130
|
+
UndoRedo: false;
|
|
129
131
|
/** Font family configuration */
|
|
130
132
|
FontFamily: {
|
|
131
133
|
/** Array of available font configurations */
|
|
@@ -140,6 +142,9 @@ export type Config = Partial<{
|
|
|
140
142
|
/** Whether to use custom bullet styles */
|
|
141
143
|
useCustomBullet?: boolean;
|
|
142
144
|
};
|
|
145
|
+
SmartTag: {
|
|
146
|
+
generateId?: () => string;
|
|
147
|
+
};
|
|
143
148
|
/** Toolbar configuration */
|
|
144
149
|
Toolbar: ToolbarConfig;
|
|
145
150
|
}>;
|
|
@@ -169,7 +174,9 @@ export type TextEditorProps = {
|
|
|
169
174
|
smartTagHandler?: SmartTagHandler;
|
|
170
175
|
linkHandler?: LinkHandler;
|
|
171
176
|
editable?: boolean;
|
|
172
|
-
bubbleMenuProps?: Pick<BubbleMenuPluginProps, 'options' | 'appendTo'
|
|
177
|
+
bubbleMenuProps?: Pick<BubbleMenuPluginProps, 'options' | 'appendTo'> & {
|
|
178
|
+
enabled?: boolean;
|
|
179
|
+
};
|
|
173
180
|
defaultTextStyle?: Partial<TextStyle>;
|
|
174
181
|
dataAttributes?: Record<`data-${string}`, unknown>;
|
|
175
182
|
onUpdateDebounced?: number;
|
|
@@ -180,6 +187,11 @@ export type TextEditorProps = {
|
|
|
180
187
|
text: string;
|
|
181
188
|
json: JSONContent;
|
|
182
189
|
}) => void;
|
|
190
|
+
onCreate?: (args: {
|
|
191
|
+
html: string;
|
|
192
|
+
text: string;
|
|
193
|
+
json: JSONContent;
|
|
194
|
+
}) => void;
|
|
183
195
|
onChangeFont?: (changeInfo: {
|
|
184
196
|
font: FontConfig;
|
|
185
197
|
weight: number;
|
|
@@ -211,6 +223,7 @@ export type LinkAttrs = {
|
|
|
211
223
|
}>;
|
|
212
224
|
export type SmartTagAttrs = {
|
|
213
225
|
id: string;
|
|
226
|
+
sourceId?: string;
|
|
214
227
|
} & Partial<{
|
|
215
228
|
content: string;
|
|
216
229
|
tag: string;
|
|
@@ -25,7 +25,7 @@ export const FormattingToolbar = (props) => {
|
|
|
25
25
|
}, [config?.Toolbar]);
|
|
26
26
|
// Map action names to React components
|
|
27
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 }, "fontFamily")),
|
|
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
29
|
fontSize: (_jsx(FontSizeAction, { editor: editor, defaultFontSize: defaultTextStyle.fontSize }, "fontSize")),
|
|
30
30
|
bold: _jsx(BoldAction, { editor: editor }, "bold"),
|
|
31
31
|
italic: _jsx(ItalicAction, { editor: editor }, "italic"),
|
|
@@ -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
|
},
|
|
@@ -177,8 +177,9 @@ export const extendSelectionToFullLinks = (state, from, to) => {
|
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
180
|
+
// Track min/max positions directly instead of collecting all ranges
|
|
181
|
+
let newFrom = from;
|
|
182
|
+
let newTo = to;
|
|
182
183
|
const processedRanges = new Set();
|
|
183
184
|
for (const linkRange of linkMarkRanges) {
|
|
184
185
|
const rangeKey = `${linkRange.from}-${linkRange.to}`;
|
|
@@ -186,23 +187,22 @@ export const extendSelectionToFullLinks = (state, from, to) => {
|
|
|
186
187
|
continue;
|
|
187
188
|
const { href } = linkRange.mark.attrs;
|
|
188
189
|
if (!href) {
|
|
189
|
-
|
|
190
|
+
newFrom = Math.min(newFrom, linkRange.from);
|
|
191
|
+
newTo = Math.max(newTo, linkRange.to);
|
|
190
192
|
processedRanges.add(rangeKey);
|
|
191
193
|
continue;
|
|
192
194
|
}
|
|
193
195
|
// Find all adjacent links with same href
|
|
194
196
|
const adjacentLinks = findAdjacentLinksWithSameHref(linkRange, href, linkEndingAt, linkStartingAt, state.doc);
|
|
195
|
-
adjacentLinks
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
// Since adjacentLinks is sorted (before links unshifted, after links pushed),
|
|
198
|
+
// first link has minimum from, last link has maximum to
|
|
199
|
+
newFrom = Math.min(newFrom, adjacentLinks[0].from);
|
|
200
|
+
newTo = Math.max(newTo, adjacentLinks[adjacentLinks.length - 1].to);
|
|
201
|
+
// Mark all adjacent links as processed
|
|
202
|
+
adjacentLinks.forEach(adjLink => {
|
|
203
|
+
processedRanges.add(`${adjLink.from}-${adjLink.to}`);
|
|
201
204
|
});
|
|
202
205
|
}
|
|
203
|
-
// Calculate final range from all collected links
|
|
204
|
-
const newFrom = Math.min(...allLinkRanges.map(range => range.from), from);
|
|
205
|
-
const newTo = Math.max(...allLinkRanges.map(range => range.to), to);
|
|
206
206
|
return {
|
|
207
207
|
from: newFrom,
|
|
208
208
|
to: newTo,
|