@antscorp/antsomi-ui 1.3.7-beta.24 → 1.3.7-beta.26

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 (26) hide show
  1. package/es/components/molecules/SearchPopover/SearchPopover.js +2 -2
  2. package/es/components/molecules/SearchPopover/styled.d.ts +12 -1
  3. package/es/components/molecules/SearchPopover/styled.js +1 -2
  4. package/es/components/molecules/SearchPopover/types.d.ts +4 -3
  5. package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.d.ts +9 -10
  6. package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.js +45 -332
  7. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/index.d.ts +9 -0
  8. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/index.js +5 -0
  9. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useFocusManagement.d.ts +23 -0
  10. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useFocusManagement.js +81 -0
  11. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.d.ts +24 -0
  12. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.js +32 -0
  13. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.d.ts +26 -0
  14. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.js +93 -0
  15. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useTreeState.d.ts +24 -0
  16. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useTreeState.js +94 -0
  17. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.d.ts +7 -0
  18. package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.js +132 -0
  19. package/es/components/organism/TextEditor/TextEditor.js +5 -7
  20. package/es/components/organism/TextEditor/extensions/ListItem.d.ts +10 -0
  21. package/es/components/organism/TextEditor/extensions/ListItem.js +93 -0
  22. package/es/components/organism/TextEditor/styled.js +1 -0
  23. package/es/components/organism/TextEditor/ui/FontPopover/FontPopover.js +12 -2
  24. package/es/components/organism/TextEditor/ui/TextAlignSelect/TextAlignSelect.js +1 -3
  25. package/es/components/organism/TextEditor/ui/Toolbar/actions/FontSizeAction.js +6 -1
  26. package/package.json +1 -1
@@ -0,0 +1,32 @@
1
+ import { useCallback } from 'react';
2
+ /**
3
+ * Custom hook to handle user interactions with menu items
4
+ * Manages click events and mouse down state
5
+ */
6
+ export const useItemInteraction = (props) => {
7
+ const { normalizeTreeItems, handleSelectItem, handleToggleExpandItem, setFocusId, onClick, isMouseDownRef, } = props;
8
+ const handleClickItem = useCallback((args) => {
9
+ const { item, elKey, domEvent } = args;
10
+ const node = normalizeTreeItems.get(item.id);
11
+ if (!node || node.data.displayOnly)
12
+ return;
13
+ const hasChilren = node.childIds.length > 0;
14
+ handleSelectItem(node);
15
+ if (hasChilren) {
16
+ handleToggleExpandItem(node);
17
+ }
18
+ setFocusId(node.id);
19
+ onClick?.({ item: node.data, elKey, domEvent });
20
+ }, [normalizeTreeItems, handleSelectItem, handleToggleExpandItem, setFocusId, onClick]);
21
+ const handleMouseDown = useCallback(() => {
22
+ isMouseDownRef.current = true;
23
+ // Reset flag after a short delay to handle the focus event
24
+ setTimeout(() => {
25
+ isMouseDownRef.current = false;
26
+ }, 0);
27
+ }, [isMouseDownRef]);
28
+ return {
29
+ handleClickItem,
30
+ handleMouseDown,
31
+ };
32
+ };
@@ -0,0 +1,26 @@
1
+ /// <reference types="react" />
2
+ import { NormalizedNode } from '@antscorp/antsomi-ui/es/utils/tree';
3
+ import { ItemType, MenuInlineProps } from '../../../types';
4
+ export interface UseKeyboardNavigationReturn {
5
+ handleKeyDown: (e: React.KeyboardEvent) => void;
6
+ }
7
+ export interface UseKeyboardNavigationProps {
8
+ visibleItems: NormalizedNode<ItemType>[];
9
+ focusId: string | null;
10
+ expandedKeys: Set<string>;
11
+ normalizeTreeItems: Map<string, NormalizedNode<ItemType>>;
12
+ setFocusId: (id: string | null) => void;
13
+ handleExpandItems: (items: NormalizedNode<ItemType>[]) => void;
14
+ handleCollapseItems: (items: NormalizedNode<ItemType>[]) => void;
15
+ handleSelectItem: (item: NormalizedNode<ItemType>) => void;
16
+ handleToggleExpandItem: (item: NormalizedNode<ItemType>) => void;
17
+ findNextFocusableItem: (startIndex: number, direction: 'next' | 'prev') => number;
18
+ findFirstFocusableItem: () => number;
19
+ findLastFocusableItem: () => number;
20
+ onClick?: MenuInlineProps['onClick'];
21
+ }
22
+ /**
23
+ * Custom hook to handle keyboard navigation for tree menu
24
+ * Implements ARIA-compliant keyboard interaction patterns
25
+ */
26
+ export declare const useKeyboardNavigation: (props: UseKeyboardNavigationProps) => UseKeyboardNavigationReturn;
@@ -0,0 +1,93 @@
1
+ import { useCallback } from 'react';
2
+ /**
3
+ * Custom hook to handle keyboard navigation for tree menu
4
+ * Implements ARIA-compliant keyboard interaction patterns
5
+ */
6
+ export const useKeyboardNavigation = (props) => {
7
+ const { visibleItems, focusId, expandedKeys, normalizeTreeItems, setFocusId, handleExpandItems, handleCollapseItems, handleSelectItem, handleToggleExpandItem, findNextFocusableItem, findFirstFocusableItem, findLastFocusableItem, onClick, } = props;
8
+ const handleKeyDown = useCallback((e) => {
9
+ if (visibleItems.length === 0)
10
+ return;
11
+ e.preventDefault();
12
+ const currentFocusIndex = visibleItems.findIndex(item => item.id === focusId);
13
+ if (currentFocusIndex === -1) {
14
+ const firstFocusableIndex = findFirstFocusableItem();
15
+ setFocusId(visibleItems[firstFocusableIndex]?.id ?? null);
16
+ return;
17
+ }
18
+ const currentFocusItem = visibleItems[currentFocusIndex];
19
+ switch (e.key) {
20
+ case 'ArrowDown': {
21
+ const nextFocusIndex = findNextFocusableItem(currentFocusIndex, 'next');
22
+ setFocusId(visibleItems[nextFocusIndex].id);
23
+ break;
24
+ }
25
+ case 'ArrowUp': {
26
+ const nextFocusIndex = findNextFocusableItem(currentFocusIndex, 'prev');
27
+ setFocusId(visibleItems[nextFocusIndex].id);
28
+ break;
29
+ }
30
+ case 'ArrowRight': {
31
+ if (currentFocusItem.childIds.length > 0 && !expandedKeys.has(currentFocusItem.id)) {
32
+ handleExpandItems([currentFocusItem]);
33
+ }
34
+ break;
35
+ }
36
+ case 'ArrowLeft': {
37
+ if (currentFocusItem.childIds.length > 0 && expandedKeys.has(currentFocusItem.id)) {
38
+ handleCollapseItems([currentFocusItem]);
39
+ }
40
+ else if (currentFocusItem?.parentId) {
41
+ // Find parent and ensure it's focusable
42
+ const parentItem = normalizeTreeItems.get(currentFocusItem.parentId);
43
+ if (parentItem && !parentItem.data.displayOnly) {
44
+ setFocusId(currentFocusItem.parentId);
45
+ }
46
+ }
47
+ break;
48
+ }
49
+ case 'Home': {
50
+ const firstFocusableIndex = findFirstFocusableItem();
51
+ setFocusId(visibleItems[firstFocusableIndex].id);
52
+ break;
53
+ }
54
+ case 'End': {
55
+ const lastFocusableIndex = findLastFocusableItem();
56
+ setFocusId(visibleItems[lastFocusableIndex].id);
57
+ break;
58
+ }
59
+ case 'Enter': {
60
+ if (currentFocusItem && !currentFocusItem.data.displayOnly) {
61
+ handleSelectItem(currentFocusItem);
62
+ if (currentFocusItem.childIds.length > 0) {
63
+ handleToggleExpandItem(currentFocusItem);
64
+ }
65
+ onClick?.({
66
+ item: currentFocusItem.data,
67
+ elKey: 'item',
68
+ });
69
+ }
70
+ break;
71
+ }
72
+ default:
73
+ break;
74
+ }
75
+ }, [
76
+ visibleItems,
77
+ focusId,
78
+ expandedKeys,
79
+ normalizeTreeItems,
80
+ setFocusId,
81
+ handleExpandItems,
82
+ handleCollapseItems,
83
+ handleSelectItem,
84
+ handleToggleExpandItem,
85
+ findNextFocusableItem,
86
+ findFirstFocusableItem,
87
+ findLastFocusableItem,
88
+ onClick,
89
+ ]);
90
+ return {
91
+ handleKeyDown,
92
+ };
93
+ };
@@ -0,0 +1,24 @@
1
+ import { NormalizedNode } from '@antscorp/antsomi-ui/es/utils/tree';
2
+ import { ItemType } from '../../../types';
3
+ export interface UseTreeStateReturn {
4
+ normalizeTreeItems: Map<string, NormalizedNode<ItemType>>;
5
+ rootItemIds: Set<string>;
6
+ rootNodes: NormalizedNode<ItemType>[];
7
+ expandedKeys: Set<string>;
8
+ selectedKeys: Set<string>;
9
+ handleExpandItems: (items: NormalizedNode<ItemType>[], deep?: number) => void;
10
+ handleCollapseItems: (items: NormalizedNode<ItemType>[]) => void;
11
+ handleToggleExpandItem: (item: NormalizedNode<ItemType>) => void;
12
+ handleSelectItem: (item: NormalizedNode<ItemType>) => void;
13
+ expandAll: () => void;
14
+ }
15
+ export interface UseTreeStateProps {
16
+ items: ItemType[];
17
+ expanded: string[];
18
+ selected: string | string[];
19
+ selectable: boolean;
20
+ }
21
+ /**
22
+ * Custom hook to manage tree state including expansion, selection, and normalization
23
+ */
24
+ export declare const useTreeState: (props: UseTreeStateProps) => UseTreeStateReturn;
@@ -0,0 +1,94 @@
1
+ import { useState, useCallback, useMemo, useEffect } from 'react';
2
+ import { useDeepCompareEffect } from '@antscorp/antsomi-ui/es/hooks';
3
+ import { findNodes, getChildrenRecursively, } from '@antscorp/antsomi-ui/es/utils/tree';
4
+ import { serializeItems } from '../../../utils';
5
+ /**
6
+ * Custom hook to manage tree state including expansion, selection, and normalization
7
+ */
8
+ export const useTreeState = (props) => {
9
+ const { items: itemsProp, expanded: expandedProp, selected: selectedProp, selectable } = props;
10
+ const [expandedKeys, setExpandedKeys] = useState(new Set());
11
+ const [selectedKeys, setSelectedKeys] = useState(new Set());
12
+ const [{ items: normalizeTreeItems, rootItemIds }, setTreeState] = useState({
13
+ items: new Map(),
14
+ rootItemIds: new Set(),
15
+ });
16
+ // Sync external selectedProp with internal state
17
+ useDeepCompareEffect(() => {
18
+ setSelectedKeys(new Set(selectedProp));
19
+ }, [selectedProp]);
20
+ // Sync external expandedProp with internal state
21
+ useDeepCompareEffect(() => {
22
+ setExpandedKeys(new Set(expandedProp));
23
+ }, [expandedProp]);
24
+ // Serialize items into normalized tree structure
25
+ useEffect(() => {
26
+ setTreeState(serializeItems({
27
+ items: itemsProp,
28
+ }));
29
+ }, [itemsProp]);
30
+ // Compute root nodes
31
+ const rootNodes = useMemo(() => findNodes(node => rootItemIds.has(node.id), normalizeTreeItems), [normalizeTreeItems, rootItemIds]);
32
+ // Handler to collapse items
33
+ const handleCollapseItems = useCallback((items) => {
34
+ setExpandedKeys(prev => {
35
+ const newOpenKeys = new Set(prev);
36
+ items.forEach(({ data }) => {
37
+ if (prev.has(data.key)) {
38
+ newOpenKeys.delete(data.key);
39
+ }
40
+ });
41
+ return newOpenKeys;
42
+ });
43
+ }, []);
44
+ // Handler to expand items with optional deep expansion
45
+ const handleExpandItems = useCallback((items, deep = 0) => {
46
+ setExpandedKeys(prev => {
47
+ const updatedExpandedKeys = new Set(prev);
48
+ items.forEach(item => {
49
+ const normalizeItem = normalizeTreeItems.get(item.data.key);
50
+ if (normalizeItem) {
51
+ updatedExpandedKeys.add(normalizeItem.id);
52
+ if (deep > 0) {
53
+ getChildrenRecursively(normalizeItem.id, normalizeTreeItems, normalizeItem.level + deep).forEach(v => updatedExpandedKeys.add(v.id));
54
+ }
55
+ }
56
+ });
57
+ return updatedExpandedKeys;
58
+ });
59
+ }, [normalizeTreeItems]);
60
+ // Handler to toggle expansion state of an item
61
+ const handleToggleExpandItem = useCallback((item) => {
62
+ const { key } = item.data;
63
+ const isExpanded = expandedKeys.has(key);
64
+ if (isExpanded) {
65
+ handleCollapseItems([item]);
66
+ }
67
+ else {
68
+ handleExpandItems([item]);
69
+ }
70
+ }, [expandedKeys, handleCollapseItems, handleExpandItems]);
71
+ // Handler to select an item
72
+ const handleSelectItem = useCallback((item) => {
73
+ const { key } = item.data;
74
+ if (!selectable || selectedKeys.has(key) || item.data.displayOnly)
75
+ return;
76
+ setSelectedKeys(new Set([key]));
77
+ }, [selectable, selectedKeys]);
78
+ // Handler to expand all items
79
+ const expandAll = useCallback(() => {
80
+ handleExpandItems(rootNodes, Infinity);
81
+ }, [handleExpandItems, rootNodes]);
82
+ return {
83
+ normalizeTreeItems,
84
+ rootItemIds,
85
+ rootNodes,
86
+ expandedKeys,
87
+ selectedKeys,
88
+ handleExpandItems,
89
+ handleCollapseItems,
90
+ handleToggleExpandItem,
91
+ handleSelectItem,
92
+ expandAll,
93
+ };
94
+ };
@@ -0,0 +1,7 @@
1
+ import { NormalizedNode } from '@antscorp/antsomi-ui/es/utils/tree';
2
+ import { ItemType } from '../../../types';
3
+ /**
4
+ * Custom hook to compute and optimize visible items in a tree structure
5
+ * Automatically selects the best strategy based on tree characteristics
6
+ */
7
+ export declare const useVisibleItems: (normalizeTreeItems: Map<string, NormalizedNode<ItemType>>, rootItemIds: Set<string>, expandedKeys: Set<string>) => NormalizedNode<ItemType>[];
@@ -0,0 +1,132 @@
1
+ import { useMemo, useEffect } from 'react';
2
+ import { getPathToNode } from '@antscorp/antsomi-ui/es/utils/tree';
3
+ /**
4
+ * Optimized function to compute visible items using breadth-first traversal
5
+ * instead of filtering all items
6
+ */
7
+ const computeVisibleItemsOptimized = (normalizeTreeItems, rootItemIds, expandedKeys) => {
8
+ const visibleItems = [];
9
+ const queue = Array.from(rootItemIds);
10
+ // Process queue in breadth-first manner
11
+ let index = 0;
12
+ while (index < queue.length) {
13
+ const itemId = queue[index++];
14
+ const item = normalizeTreeItems.get(itemId);
15
+ if (!item)
16
+ continue;
17
+ visibleItems.push(item);
18
+ // Add children to queue only if this item is expanded
19
+ if (expandedKeys.has(item.id) && item.childIds.length > 0) {
20
+ queue.push(...item.childIds);
21
+ }
22
+ }
23
+ return visibleItems;
24
+ };
25
+ /**
26
+ * Alternative: Path-based optimization with memoization
27
+ * Caches path calculations to avoid redundant work
28
+ */
29
+ const computeVisibleItemsWithPathCache = (normalizeTreeItems, expandedKeys, pathCache) => {
30
+ const visibleItems = [];
31
+ for (const item of normalizeTreeItems.values()) {
32
+ const isRootItem = item.level === 0;
33
+ if (isRootItem) {
34
+ visibleItems.push(item);
35
+ continue;
36
+ }
37
+ if (!item.parentId)
38
+ continue;
39
+ // Use cached path or compute and cache
40
+ let pathToParent = pathCache.get(item.parentId);
41
+ if (!pathToParent) {
42
+ pathToParent = getPathToNode(item.parentId, normalizeTreeItems);
43
+ pathCache.set(item.parentId, pathToParent);
44
+ }
45
+ // Check if all parents are expanded
46
+ const allParentExpanded = pathToParent.every(parent => expandedKeys.has(parent.id));
47
+ if (allParentExpanded) {
48
+ visibleItems.push(item);
49
+ }
50
+ }
51
+ return visibleItems;
52
+ };
53
+ /**
54
+ * Level-based optimization: Process items level by level
55
+ * More efficient for deep trees with many collapsed branches
56
+ */
57
+ const computeVisibleItemsByLevel = (normalizeTreeItems, rootItemIds, expandedKeys) => {
58
+ const visibleItems = [];
59
+ const expandedParents = new Set();
60
+ // Add root items and track expanded ones
61
+ for (const rootId of rootItemIds) {
62
+ const rootItem = normalizeTreeItems.get(rootId);
63
+ if (rootItem) {
64
+ visibleItems.push(rootItem);
65
+ if (expandedKeys.has(rootId)) {
66
+ expandedParents.add(rootId);
67
+ }
68
+ }
69
+ }
70
+ // Process each level
71
+ let currentLevelParents = expandedParents;
72
+ let currentLevel = 1;
73
+ while (currentLevelParents.size > 0) {
74
+ const nextLevelParents = new Set();
75
+ // Process all items at current level
76
+ for (const parentId of currentLevelParents) {
77
+ const parent = normalizeTreeItems.get(parentId);
78
+ if (!parent)
79
+ continue;
80
+ // Add all children of this expanded parent
81
+ for (const childId of parent.childIds) {
82
+ const child = normalizeTreeItems.get(childId);
83
+ if (child && child.level === currentLevel) {
84
+ visibleItems.push(child);
85
+ // Track if this child is expanded for next level
86
+ if (expandedKeys.has(childId)) {
87
+ nextLevelParents.add(childId);
88
+ }
89
+ }
90
+ }
91
+ }
92
+ currentLevelParents = nextLevelParents;
93
+ currentLevel++;
94
+ }
95
+ return visibleItems;
96
+ };
97
+ /**
98
+ * Custom hook to compute and optimize visible items in a tree structure
99
+ * Automatically selects the best strategy based on tree characteristics
100
+ */
101
+ export const useVisibleItems = (normalizeTreeItems, rootItemIds, expandedKeys) => {
102
+ // Cache for path calculations to avoid redundant work
103
+ const pathCache = useMemo(() => new Map(), []);
104
+ // Clear path cache when tree structure changes
105
+ useEffect(() => {
106
+ pathCache.clear();
107
+ }, [normalizeTreeItems, pathCache]);
108
+ // Optimized visible items calculation with strategy selection
109
+ const visibleItems = useMemo(() => {
110
+ if (normalizeTreeItems.size === 0)
111
+ return [];
112
+ // Choose optimization strategy based on tree characteristics
113
+ const treeDepth = Math.max(...Array.from(normalizeTreeItems.values()).map(item => item.level));
114
+ const totalItems = normalizeTreeItems.size;
115
+ const expandedCount = expandedKeys.size;
116
+ // Strategy selection heuristics:
117
+ // - For shallow trees with many expanded items: use breadth-first
118
+ // - For deep trees with few expanded items: use level-based
119
+ // - For medium trees: use path-cache optimization
120
+ if (treeDepth <= 3 && expandedCount > totalItems * 0.3) {
121
+ // Shallow tree with many expanded items
122
+ return computeVisibleItemsOptimized(normalizeTreeItems, rootItemIds, expandedKeys);
123
+ }
124
+ if (treeDepth > 5 && expandedCount < totalItems * 0.2) {
125
+ // Deep tree with few expanded items
126
+ return computeVisibleItemsByLevel(normalizeTreeItems, rootItemIds, expandedKeys);
127
+ }
128
+ // Medium complexity - use cached path approach
129
+ return computeVisibleItemsWithPathCache(normalizeTreeItems, expandedKeys, pathCache);
130
+ }, [expandedKeys, normalizeTreeItems, rootItemIds, pathCache]);
131
+ return visibleItems;
132
+ };
@@ -9,7 +9,7 @@ import { Selection, Focus } from '@tiptap/extensions';
9
9
  import { useEditor } from '@tiptap/react';
10
10
  import StarterKit from '@tiptap/starter-kit';
11
11
  import clsx from 'clsx';
12
- import { isBoolean, omit } from 'lodash';
12
+ import { isBoolean } from 'lodash';
13
13
  import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, } from 'react';
14
14
  import { useDebouncedCallback } from 'use-debounce';
15
15
  import { DEFAULT_TEXT_STYLE } from './constants';
@@ -23,6 +23,7 @@ import { Indent } from './extensions/Indent';
23
23
  import { LetterSpacing } from './extensions/LetterSpacing';
24
24
  import { LineHeight } from './extensions/LineHeight';
25
25
  import { CustomLink } from './extensions/Link';
26
+ import { CustomListItem } from './extensions/ListItem';
26
27
  import { CustomOrderedList } from './extensions/OrderedList';
27
28
  import { SmartTag } from './extensions/SmartTag';
28
29
  import { TextTransform } from './extensions/TextTransform';
@@ -104,15 +105,12 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
104
105
  link: false,
105
106
  bulletList: false,
106
107
  orderedList: false,
107
- listItem: {
108
- HTMLAttributes: {
109
- style: 'line-height:normal;',
110
- },
111
- },
108
+ listItem: false,
112
109
  undoRedo: {
113
110
  depth: 100,
114
111
  },
115
112
  }),
113
+ CustomListItem,
116
114
  CustomUnorderedList,
117
115
  CustomOrderedList,
118
116
  FontWeight.configure({
@@ -307,7 +305,7 @@ const TextEditorInternal = memo(forwardRef((props, ref) => {
307
305
  }
308
306
  return (_jsxs(_Fragment, { children: [_jsx(StyledEditorContent, { className: clsx(`${ANTSOMI_COMPONENT_PREFIX_CLS}-text-editor`, className), "$textStyle": defaultTextStyle, style: {
309
307
  // Inline styles apply to inner html elements (support email client)
310
- ...omit(defaultTextStyle, ['fontSize']),
308
+ ...defaultTextStyle,
311
309
  ...style,
312
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: {
313
311
  onUpsert: () => {
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Custom ListItem extension that extends the default TipTap ListItem
3
+ * with custom Backspace behavior to prevent merging paragraphs into lists
4
+ *
5
+ * This extension only overrides keyboard shortcuts while maintaining
6
+ * all other default ListItem functionality (HTML parsing, rendering, etc.)
7
+ *
8
+ * @see https://www.tiptap.dev/api/nodes/list-item
9
+ */
10
+ export declare const CustomListItem: import("@tiptap/core").Node<import("@tiptap/extension-list").ListItemOptions, any>;
@@ -0,0 +1,93 @@
1
+ import { ListItem } from '@tiptap/extension-list-item';
2
+ /**
3
+ * Custom ListItem extension that extends the default TipTap ListItem
4
+ * with custom Backspace behavior to prevent merging paragraphs into lists
5
+ *
6
+ * This extension only overrides keyboard shortcuts while maintaining
7
+ * all other default ListItem functionality (HTML parsing, rendering, etc.)
8
+ *
9
+ * @see https://www.tiptap.dev/api/nodes/list-item
10
+ */
11
+ export const CustomListItem = ListItem.extend({
12
+ addOptions() {
13
+ return {
14
+ HTMLAttributes: {
15
+ style: 'line-height:normal;',
16
+ },
17
+ bulletListTypeName: 'customUnorderedList',
18
+ orderedListTypeName: 'customOrderedList',
19
+ };
20
+ },
21
+ addKeyboardShortcuts() {
22
+ return {
23
+ // Keep default keyboard shortcuts from parent ListItem
24
+ Enter: () => this.editor.commands.splitListItem(this.name),
25
+ Tab: () => this.editor.commands.sinkListItem(this.name),
26
+ 'Shift-Tab': () => this.editor.commands.liftListItem(this.name),
27
+ /**
28
+ * Custom Backspace handler to prevent merging empty paragraphs into lists
29
+ *
30
+ * Behavior:
31
+ * 1. Check if cursor is at the start of a paragraph
32
+ * 2. Check if the node before is a list (ordered or unordered)
33
+ * 3. If both conditions are true and paragraph is empty, delete the paragraph
34
+ * 4. Otherwise, use default backspace behavior
35
+ */
36
+ Backspace: () => {
37
+ const { state } = this.editor;
38
+ const { selection } = state;
39
+ const { $from, empty } = selection;
40
+ // Only handle if selection is empty (cursor position, not text selection)
41
+ if (!empty) {
42
+ return false;
43
+ }
44
+ // Check if cursor is at the start of current node
45
+ const isAtStart = $from.parentOffset === 0;
46
+ if (!isAtStart) {
47
+ return false;
48
+ }
49
+ // Get current node (should be a paragraph)
50
+ const currentNode = $from.parent;
51
+ // Only handle paragraphs
52
+ if (currentNode.type.name !== 'paragraph') {
53
+ return false;
54
+ }
55
+ // Get the index of current paragraph in its parent (usually doc)
56
+ const paragraphDepth = $from.depth;
57
+ const indexInParent = $from.index(paragraphDepth - 1);
58
+ // Check if there's a node before (index > 0)
59
+ if (indexInParent === 0) {
60
+ return false;
61
+ }
62
+ // Get parent node (usually doc) and the node before current paragraph
63
+ const parent = $from.node(paragraphDepth - 1);
64
+ const nodeBefore = parent.child(indexInParent - 1);
65
+ if (!nodeBefore) {
66
+ return false;
67
+ }
68
+ // Check if the node before is a list (ordered or unordered)
69
+ const isListBefore = nodeBefore.type.name === this.options.orderedListTypeName ||
70
+ nodeBefore.type.name === this.options.bulletListTypeName;
71
+ if (!isListBefore) {
72
+ return false;
73
+ }
74
+ // Check if current paragraph is empty
75
+ const isParagraphEmpty = currentNode.content.size === 0;
76
+ if (!isParagraphEmpty) {
77
+ // If paragraph has content, allow default backspace to handle
78
+ return false;
79
+ }
80
+ // Calculate the exact positions of the paragraph node
81
+ // We need to delete from the position before the paragraph open tag
82
+ // to the position after the paragraph close tag
83
+ const paragraphPos = $from.before();
84
+ const paragraphEndPos = $from.after();
85
+ // Delete the entire paragraph node using deleteRange
86
+ return this.editor.commands.deleteRange({
87
+ from: paragraphPos,
88
+ to: paragraphEndPos,
89
+ });
90
+ },
91
+ };
92
+ },
93
+ });
@@ -12,6 +12,7 @@ export const StyledEditorContent = styled(EditorContent) `
12
12
 
13
13
  // Some apps have global p styles, so we need to override it
14
14
 
15
+ font-size: ${p => p.$textStyle.fontSize};
15
16
  line-height: ${p => p.$textStyle.lineHeight};
16
17
  font-family: ${p => p.$textStyle.fontFamily};
17
18
  color: ${p => p.$textStyle.color};
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { memo, useCallback, useEffect, useMemo, useState } from 'react';
2
+ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
3
  import { Typography } from 'antd';
4
4
  import { SearchPopover, VirtualizedMenu } from '@antscorp/antsomi-ui/es/components/molecules';
5
5
  import { FontGroupTitle, PopoverTrigger } from './styled';
@@ -52,6 +52,7 @@ export const FontPopover = memo((props) => {
52
52
  const itemSize = 30;
53
53
  const itemSpacing = 4;
54
54
  const activeFontName = fontValue?.font.fontFamily.name;
55
+ const inputSearchRef = useRef(null);
55
56
  const [search, setSearch] = useState('');
56
57
  const [open, setOpen] = useState(false);
57
58
  const [fontItemsSize, setFontItemSize] = useDebounce(0, 200);
@@ -83,10 +84,19 @@ export const FontPopover = memo((props) => {
83
84
  const listSize = calInlineListSize(Math.min(items.length, 6), itemSize, itemSpacing);
84
85
  setFontItemSize(listSize);
85
86
  }, [items.length, setFontItemSize]);
87
+ useEffect(() => {
88
+ if (open) {
89
+ // Use setTimeout to ensure the popover content is fully mounted in the DOM
90
+ const timeoutId = setTimeout(() => {
91
+ inputSearchRef.current?.focus();
92
+ }, 0);
93
+ return () => clearTimeout(timeoutId);
94
+ }
95
+ }, [open]);
86
96
  return (_jsx(SearchPopover, { open: open, placement: "bottomLeft", onOpenChange: handleOpenChange, getPopupContainer: () => bubbleMenuContainer || document.body, inputSearchProps: {
87
97
  value: search,
88
98
  onAfterChange: setSearch,
89
- }, overlayClassName: "font-popover-overlay", content: _jsx("div", { className: "list-font-root", style: {
99
+ }, inputSearchRef: inputSearchRef, overlayClassName: "font-popover-overlay", content: _jsx("div", { className: "list-font-root", style: {
90
100
  height: `${fontItemsSize}px`,
91
101
  }, children: _jsx(VirtualizedMenu, { itemSize: itemSize, inlinePadding: 0, itemSpacing: itemSpacing, selectable: false, items: items }) }), children: _jsx(PopoverTrigger, { className: "font-popover-trigger", children: _jsx(Typography.Text, { ellipsis: { tooltip: true }, children: activeFontName }) }) }));
92
102
  });
@@ -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]);
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antscorp/antsomi-ui",
3
- "version": "1.3.7-beta.24",
3
+ "version": "1.3.7-beta.26",
4
4
  "description": "An enterprise-class UI design language and React UI library.",
5
5
  "sideEffects": [
6
6
  "dist/*",