@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.
- 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/VirtualizedMenu/components/MenuInline/MenuInline.d.ts +9 -10
- package/es/components/molecules/VirtualizedMenu/components/MenuInline/MenuInline.js +45 -332
- 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/organism/TextEditor/TextEditor.js +5 -7
- 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/styled.js +1 -0
- package/es/components/organism/TextEditor/ui/FontPopover/FontPopover.js +12 -2
- package/es/components/organism/TextEditor/ui/TextAlignSelect/TextAlignSelect.js +1 -3
- package/es/components/organism/TextEditor/ui/Toolbar/actions/FontSizeAction.js +6 -1
- package/package.json +1 -1
package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useItemInteraction.js
ADDED
|
@@ -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;
|
package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useKeyboardNavigation.js
ADDED
|
@@ -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
|
+
};
|
package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.d.ts
ADDED
|
@@ -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>[];
|
package/es/components/molecules/VirtualizedMenu/components/MenuInline/hooks/useVisibleItems.js
ADDED
|
@@ -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
|
|
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
|
-
...
|
|
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;
|