|
|
@@ -107,15 +107,41 @@ function addHeadingNumbers(node: OutlineNode, numberMap: Map<string, string>): O
|
|
107
|
107
|
*/
|
|
108
|
108
|
export const DocumentOutline: React.FC<DocumentOutlineProps> = ({ visible = true }) => {
|
|
109
|
109
|
const headings = useEditorStore(
|
|
110
|
|
- useShallow((state) =>
|
|
111
|
|
- state.blocks
|
|
112
|
|
- .filter((block): block is HeadingBlock => block.type === 'heading')
|
|
|
110
|
+ useShallow((state) => {
|
|
|
111
|
+ const seenIds = new Set<string>();
|
|
|
112
|
+ return state.blocks
|
|
|
113
|
+ .filter(
|
|
|
114
|
+ (block): block is HeadingBlock =>
|
|
|
115
|
+ block.type === 'heading' &&
|
|
|
116
|
+ typeof block.id === 'string' &&
|
|
|
117
|
+ block.id.length > 0 &&
|
|
|
118
|
+ Number.isInteger(block.level) &&
|
|
|
119
|
+ block.level >= 1 &&
|
|
|
120
|
+ block.level <= 6
|
|
|
121
|
+ )
|
|
113
|
122
|
.sort((left, right) => left.block_order - right.block_order)
|
|
114
|
|
- )
|
|
|
123
|
+ .filter((heading) => {
|
|
|
124
|
+ if (seenIds.has(heading.id)) return false;
|
|
|
125
|
+ seenIds.add(heading.id);
|
|
|
126
|
+ return true;
|
|
|
127
|
+ });
|
|
|
128
|
+ })
|
|
115
|
129
|
);
|
|
116
|
130
|
const selectedBlockId = useEditorStore((state) => state.selectedBlockId);
|
|
|
131
|
+ const selectBlock = useEditorStore((state) => state.selectBlock);
|
|
117
|
132
|
const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);
|
|
118
|
133
|
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
|
|
134
|
+ const highlightTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
135
|
+ const highlightedElementRef = React.useRef<HTMLElement | null>(null);
|
|
|
136
|
+
|
|
|
137
|
+ React.useEffect(() => {
|
|
|
138
|
+ return () => {
|
|
|
139
|
+ if (highlightTimerRef.current) {
|
|
|
140
|
+ clearTimeout(highlightTimerRef.current);
|
|
|
141
|
+ }
|
|
|
142
|
+ highlightedElementRef.current?.classList.remove('block-highlight');
|
|
|
143
|
+ };
|
|
|
144
|
+ }, []);
|
|
119
|
145
|
|
|
120
|
146
|
// 构建树结构
|
|
121
|
147
|
const treeData = useMemo(() => {
|
|
|
@@ -139,21 +165,42 @@ export const DocumentOutline: React.FC<DocumentOutlineProps> = ({ visible = true
|
|
139
|
165
|
}, [treeData]);
|
|
140
|
166
|
|
|
141
|
167
|
// 处理节点点击
|
|
142
|
|
- const handleSelect = useCallback((selectedKeys: React.Key[]) => {
|
|
143
|
|
- if (selectedKeys.length === 0) return;
|
|
|
168
|
+ const handleSelect = useCallback(
|
|
|
169
|
+ (selectedKeys: React.Key[]) => {
|
|
|
170
|
+ if (selectedKeys.length === 0) {
|
|
|
171
|
+ selectBlock(null);
|
|
|
172
|
+ return;
|
|
|
173
|
+ }
|
|
|
174
|
+
|
|
|
175
|
+ const blockId = selectedKeys[0];
|
|
|
176
|
+ if (typeof blockId !== 'string') return;
|
|
144
|
177
|
|
|
145
|
|
- const blockId = selectedKeys[0] as string;
|
|
146
|
|
- const element = document.querySelector(`[data-block-id="${blockId}"]`);
|
|
|
178
|
+ const element = Array.from(document.querySelectorAll<HTMLElement>('[data-block-id]')).find(
|
|
|
179
|
+ (candidate) => candidate.dataset.blockId === blockId
|
|
|
180
|
+ );
|
|
147
|
181
|
|
|
148
|
|
- if (element) {
|
|
|
182
|
+ if (!element) {
|
|
|
183
|
+ selectBlock(null);
|
|
|
184
|
+ return;
|
|
|
185
|
+ }
|
|
|
186
|
+
|
|
|
187
|
+ selectBlock(blockId);
|
|
149
|
188
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
150
|
|
- // 可以添加高亮效果
|
|
|
189
|
+ highlightedElementRef.current?.classList.remove('block-highlight');
|
|
|
190
|
+ if (highlightTimerRef.current) {
|
|
|
191
|
+ clearTimeout(highlightTimerRef.current);
|
|
|
192
|
+ }
|
|
151
|
193
|
element.classList.add('block-highlight');
|
|
152
|
|
- setTimeout(() => {
|
|
|
194
|
+ highlightedElementRef.current = element;
|
|
|
195
|
+ highlightTimerRef.current = setTimeout(() => {
|
|
153
|
196
|
element.classList.remove('block-highlight');
|
|
|
197
|
+ if (highlightedElementRef.current === element) {
|
|
|
198
|
+ highlightedElementRef.current = null;
|
|
|
199
|
+ }
|
|
154
|
200
|
}, 2000);
|
|
155
|
|
- }
|
|
156
|
|
- }, []);
|
|
|
201
|
+ },
|
|
|
202
|
+ [selectBlock]
|
|
|
203
|
+ );
|
|
157
|
204
|
|
|
158
|
205
|
// 处理展开/折叠
|
|
159
|
206
|
const handleExpand = useCallback((expandedKeys: React.Key[]) => {
|