@nocobase/flow-engine 2.1.0-beta.9 → 2.2.0-beta.1
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/lib/FlowContextProvider.d.ts +5 -1
- package/lib/FlowContextProvider.js +9 -2
- package/lib/components/FieldModelRenderer.js +2 -2
- package/lib/components/FlowModelRenderer.d.ts +3 -1
- package/lib/components/FlowModelRenderer.js +12 -6
- package/lib/components/FormItem.d.ts +6 -0
- package/lib/components/FormItem.js +11 -3
- package/lib/components/MobilePopup.js +6 -5
- package/lib/components/dnd/gridDragPlanner.d.ts +59 -2
- package/lib/components/dnd/gridDragPlanner.js +607 -19
- package/lib/components/dnd/index.d.ts +31 -2
- package/lib/components/dnd/index.js +244 -23
- package/lib/components/settings/wrappers/component/SelectWithTitle.d.ts +2 -1
- package/lib/components/settings/wrappers/component/SelectWithTitle.js +14 -12
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.d.ts +3 -0
- package/lib/components/settings/wrappers/contextual/DefaultSettingsIcon.js +152 -42
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.d.ts +23 -43
- package/lib/components/settings/wrappers/contextual/FlowsFloatContextMenu.js +352 -295
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.d.ts +36 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarPortal.js +274 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.d.ts +30 -0
- package/lib/components/settings/wrappers/contextual/useFloatToolbarVisibility.js +315 -0
- package/lib/components/subModel/AddSubModelButton.js +12 -1
- package/lib/components/subModel/LazyDropdown.js +301 -52
- package/lib/components/subModel/index.d.ts +1 -0
- package/lib/components/subModel/index.js +19 -0
- package/lib/components/subModel/utils.d.ts +2 -1
- package/lib/components/subModel/utils.js +15 -5
- package/lib/components/variables/VariableHybridInput.d.ts +27 -0
- package/lib/components/variables/VariableHybridInput.js +499 -0
- package/lib/components/variables/index.d.ts +2 -0
- package/lib/components/variables/index.js +3 -0
- package/lib/data-source/index.d.ts +84 -0
- package/lib/data-source/index.js +269 -7
- package/lib/executor/FlowExecutor.js +6 -3
- package/lib/flow-registry/DetachedFlowRegistry.d.ts +21 -0
- package/lib/flow-registry/DetachedFlowRegistry.js +80 -0
- package/lib/flow-registry/index.d.ts +1 -0
- package/lib/flow-registry/index.js +3 -1
- package/lib/flowContext.d.ts +9 -1
- package/lib/flowContext.js +77 -6
- package/lib/flowEngine.d.ts +136 -4
- package/lib/flowEngine.js +429 -51
- package/lib/flowI18n.js +2 -1
- package/lib/flowSettings.d.ts +14 -6
- package/lib/flowSettings.js +34 -6
- package/lib/index.d.ts +2 -0
- package/lib/index.js +7 -0
- package/lib/lazy-helper.d.ts +14 -0
- package/lib/lazy-helper.js +71 -0
- package/lib/locale/en-US.json +1 -0
- package/lib/locale/index.d.ts +2 -0
- package/lib/locale/zh-CN.json +1 -0
- package/lib/models/DisplayItemModel.d.ts +1 -1
- package/lib/models/EditableItemModel.d.ts +1 -1
- package/lib/models/FilterableItemModel.d.ts +1 -1
- package/lib/models/flowModel.d.ts +13 -10
- package/lib/models/flowModel.js +126 -34
- package/lib/provider.js +38 -23
- package/lib/reactive/observer.js +46 -16
- package/lib/runjs-context/contexts/FormJSFieldItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSBlockRunJSContext.js +4 -15
- package/lib/runjs-context/contexts/JSColumnRunJSContext.js +5 -2
- package/lib/runjs-context/contexts/JSEditableFieldRunJSContext.js +5 -8
- package/lib/runjs-context/contexts/JSFieldRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/JSItemRunJSContext.js +4 -3
- package/lib/runjs-context/contexts/base.js +464 -29
- package/lib/runjs-context/contexts/elementDoc.d.ts +11 -0
- package/lib/runjs-context/contexts/elementDoc.js +152 -0
- package/lib/runjs-context/setup.js +1 -0
- package/lib/runjs-context/snippets/index.js +13 -2
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/detail/set-field-style.snippet.js +50 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.d.ts +11 -0
- package/lib/runjs-context/snippets/scene/table/set-cell-style.snippet.js +54 -0
- package/lib/types.d.ts +50 -2
- package/lib/types.js +1 -0
- package/lib/utils/createCollectionContextMeta.js +6 -2
- package/lib/utils/index.d.ts +3 -2
- package/lib/utils/index.js +7 -0
- package/lib/utils/loadedPageCache.d.ts +24 -0
- package/lib/utils/loadedPageCache.js +139 -0
- package/lib/utils/parsePathnameToViewParams.d.ts +5 -1
- package/lib/utils/parsePathnameToViewParams.js +28 -4
- package/lib/utils/randomId.d.ts +39 -0
- package/lib/utils/randomId.js +45 -0
- package/lib/utils/runjsTemplateCompat.js +1 -1
- package/lib/utils/runjsValue.js +41 -11
- package/lib/utils/schema-utils.d.ts +7 -1
- package/lib/utils/schema-utils.js +19 -0
- package/lib/views/FlowView.d.ts +7 -1
- package/lib/views/FlowView.js +11 -1
- package/lib/views/PageComponent.js +8 -6
- package/lib/views/ViewNavigation.d.ts +12 -2
- package/lib/views/ViewNavigation.js +28 -9
- package/lib/views/createViewMeta.js +114 -50
- package/lib/views/inheritLayoutContext.d.ts +10 -0
- package/lib/views/inheritLayoutContext.js +50 -0
- package/lib/views/runViewBeforeClose.d.ts +10 -0
- package/lib/views/runViewBeforeClose.js +45 -0
- package/lib/views/useDialog.d.ts +2 -1
- package/lib/views/useDialog.js +12 -3
- package/lib/views/useDrawer.d.ts +2 -1
- package/lib/views/useDrawer.js +12 -3
- package/lib/views/usePage.d.ts +5 -11
- package/lib/views/usePage.js +304 -144
- package/package.json +5 -4
- package/src/FlowContextProvider.tsx +9 -1
- package/src/__tests__/createViewMeta.popup.test.ts +115 -1
- package/src/__tests__/flow-engine.test.ts +166 -0
- package/src/__tests__/flowContext.test.ts +105 -1
- package/src/__tests__/flowEngine.modelLoaders.test.ts +245 -0
- package/src/__tests__/flowEngine.moveModel.test.ts +81 -1
- package/src/__tests__/flowEngine.removeModel.test.ts +47 -3
- package/src/__tests__/flowSettings.test.ts +94 -15
- package/src/__tests__/objectVariable.test.ts +24 -0
- package/src/__tests__/provider.test.tsx +24 -2
- package/src/__tests__/renderHiddenInConfig.test.tsx +6 -6
- package/src/__tests__/runjsContext.test.ts +21 -0
- package/src/__tests__/runjsContextImplementations.test.ts +9 -2
- package/src/__tests__/runjsContextRuntime.test.ts +2 -0
- package/src/__tests__/runjsLocales.test.ts +6 -5
- package/src/__tests__/runjsSnippets.test.ts +21 -0
- package/src/__tests__/viewScopedFlowEngine.test.ts +136 -3
- package/src/components/FieldModelRenderer.tsx +2 -1
- package/src/components/FlowModelRenderer.tsx +18 -6
- package/src/components/FormItem.tsx +7 -1
- package/src/components/MobilePopup.tsx +4 -2
- package/src/components/__tests__/FlowModelRenderer.test.tsx +65 -2
- package/src/components/__tests__/FormItem.test.tsx +25 -0
- package/src/components/__tests__/dnd.test.ts +44 -0
- package/src/components/__tests__/flow-model-render-error-fallback.test.tsx +20 -10
- package/src/components/__tests__/gridDragPlanner.test.ts +472 -5
- package/src/components/dnd/__tests__/DndProvider.test.tsx +98 -0
- package/src/components/dnd/gridDragPlanner.ts +750 -17
- package/src/components/dnd/index.tsx +305 -28
- package/src/components/settings/wrappers/component/SelectWithTitle.tsx +21 -9
- package/src/components/settings/wrappers/contextual/DefaultSettingsIcon.tsx +178 -48
- package/src/components/settings/wrappers/contextual/FlowsFloatContextMenu.tsx +487 -440
- package/src/components/settings/wrappers/contextual/__tests__/DefaultSettingsIcon.test.tsx +344 -8
- package/src/components/settings/wrappers/contextual/__tests__/FlowsFloatContextMenu.test.tsx +778 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarPortal.ts +360 -0
- package/src/components/settings/wrappers/contextual/useFloatToolbarVisibility.ts +361 -0
- package/src/components/subModel/AddSubModelButton.tsx +16 -2
- package/src/components/subModel/LazyDropdown.tsx +341 -56
- package/src/components/subModel/__tests__/AddSubModelButton.test.tsx +524 -38
- package/src/components/subModel/__tests__/utils.test.ts +24 -0
- package/src/components/subModel/index.ts +1 -0
- package/src/components/subModel/utils.ts +13 -2
- package/src/components/variables/VariableHybridInput.tsx +531 -0
- package/src/components/variables/index.ts +2 -0
- package/src/data-source/__tests__/collection.test.ts +41 -2
- package/src/data-source/__tests__/index.test.ts +69 -2
- package/src/data-source/index.ts +332 -8
- package/src/executor/FlowExecutor.ts +6 -3
- package/src/executor/__tests__/flowExecutor.test.ts +57 -0
- package/src/flow-registry/DetachedFlowRegistry.ts +46 -0
- package/src/flow-registry/__tests__/detachedFlowRegistry.test.ts +47 -0
- package/src/flow-registry/index.ts +1 -0
- package/src/flowContext.ts +85 -6
- package/src/flowEngine.ts +484 -45
- package/src/flowI18n.ts +2 -1
- package/src/flowSettings.ts +40 -6
- package/src/index.ts +2 -0
- package/src/lazy-helper.tsx +57 -0
- package/src/locale/en-US.json +1 -0
- package/src/locale/zh-CN.json +1 -0
- package/src/models/DisplayItemModel.tsx +1 -1
- package/src/models/EditableItemModel.tsx +1 -1
- package/src/models/FilterableItemModel.tsx +1 -1
- package/src/models/__tests__/flowEngine.resolveUse.test.ts +0 -15
- package/src/models/__tests__/flowModel.test.ts +65 -37
- package/src/models/flowModel.tsx +184 -65
- package/src/provider.tsx +41 -25
- package/src/reactive/__tests__/observer.test.tsx +82 -0
- package/src/reactive/observer.tsx +87 -25
- package/src/runjs-context/contexts/FormJSFieldItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSBlockRunJSContext.ts +4 -15
- package/src/runjs-context/contexts/JSColumnRunJSContext.ts +4 -2
- package/src/runjs-context/contexts/JSEditableFieldRunJSContext.ts +5 -9
- package/src/runjs-context/contexts/JSFieldRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/JSItemRunJSContext.ts +4 -3
- package/src/runjs-context/contexts/base.ts +467 -31
- package/src/runjs-context/contexts/elementDoc.ts +130 -0
- package/src/runjs-context/setup.ts +1 -0
- package/src/runjs-context/snippets/index.ts +12 -1
- package/src/runjs-context/snippets/scene/detail/set-field-style.snippet.ts +30 -0
- package/src/runjs-context/snippets/scene/table/set-cell-style.snippet.ts +34 -0
- package/src/types.ts +62 -0
- package/src/utils/__tests__/createCollectionContextMeta.test.ts +48 -0
- package/src/utils/__tests__/parsePathnameToViewParams.test.ts +21 -0
- package/src/utils/__tests__/runjsValue.test.ts +11 -0
- package/src/utils/__tests__/utils.test.ts +62 -0
- package/src/utils/createCollectionContextMeta.ts +6 -2
- package/src/utils/index.ts +5 -1
- package/src/utils/loadedPageCache.ts +147 -0
- package/src/utils/parsePathnameToViewParams.ts +45 -5
- package/src/utils/randomId.ts +48 -0
- package/src/utils/runjsTemplateCompat.ts +1 -1
- package/src/utils/runjsValue.ts +50 -11
- package/src/utils/schema-utils.ts +30 -1
- package/src/views/FlowView.tsx +22 -2
- package/src/views/PageComponent.tsx +7 -4
- package/src/views/ViewNavigation.ts +46 -9
- package/src/views/__tests__/FlowView.usePage.test.tsx +243 -3
- package/src/views/__tests__/ViewNavigation.test.ts +52 -0
- package/src/views/__tests__/inheritLayoutContext.test.ts +53 -0
- package/src/views/__tests__/runViewBeforeClose.test.ts +30 -0
- package/src/views/__tests__/useDialog.closeDestroy.test.tsx +12 -12
- package/src/views/createViewMeta.ts +106 -34
- package/src/views/inheritLayoutContext.ts +26 -0
- package/src/views/runViewBeforeClose.ts +19 -0
- package/src/views/useDialog.tsx +13 -3
- package/src/views/useDrawer.tsx +13 -3
- package/src/views/usePage.tsx +367 -180
|
@@ -247,18 +247,75 @@ const useKeepDropdownOpen = () => {
|
|
|
247
247
|
*/
|
|
248
248
|
const useMenuSearch = () => {
|
|
249
249
|
const [searchValues, setSearchValues] = useState<Record<string, string>>({});
|
|
250
|
+
const [inputValues, setInputValues] = useState<Record<string, string>>({});
|
|
250
251
|
const [isSearching, setIsSearching] = useState(false);
|
|
252
|
+
const [composingCount, setComposingCount] = useState(0);
|
|
253
|
+
const composingKeysRef = useRef<Set<string>>(new Set());
|
|
251
254
|
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
252
255
|
|
|
253
|
-
const updateSearchValue = (key: string, value: string) => {
|
|
256
|
+
const updateSearchValue = useCallback((key: string, value: string) => {
|
|
254
257
|
setIsSearching(true);
|
|
258
|
+
setInputValues((prev) => ({ ...prev, [key]: value }));
|
|
255
259
|
setSearchValues((prev) => ({ ...prev, [key]: value }));
|
|
256
260
|
|
|
257
261
|
if (searchTimeoutRef.current) {
|
|
258
262
|
clearTimeout(searchTimeoutRef.current);
|
|
259
263
|
}
|
|
260
264
|
searchTimeoutRef.current = setTimeout(() => setIsSearching(false), 300);
|
|
261
|
-
};
|
|
265
|
+
}, []);
|
|
266
|
+
|
|
267
|
+
const startComposition = useCallback((key: string) => {
|
|
268
|
+
composingKeysRef.current.add(key);
|
|
269
|
+
setIsSearching(true);
|
|
270
|
+
setComposingCount(composingKeysRef.current.size);
|
|
271
|
+
if (searchTimeoutRef.current) {
|
|
272
|
+
clearTimeout(searchTimeoutRef.current);
|
|
273
|
+
searchTimeoutRef.current = null;
|
|
274
|
+
}
|
|
275
|
+
}, []);
|
|
276
|
+
|
|
277
|
+
const endComposition = useCallback(
|
|
278
|
+
(key: string, value: string) => {
|
|
279
|
+
composingKeysRef.current.delete(key);
|
|
280
|
+
setComposingCount(composingKeysRef.current.size);
|
|
281
|
+
updateSearchValue(key, value);
|
|
282
|
+
},
|
|
283
|
+
[updateSearchValue],
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
const updateInputValue = useCallback((key: string, value: string) => {
|
|
287
|
+
setInputValues((prev) => ({ ...prev, [key]: value }));
|
|
288
|
+
}, []);
|
|
289
|
+
|
|
290
|
+
const clearSearchValue = useCallback((key: string) => {
|
|
291
|
+
composingKeysRef.current.delete(key);
|
|
292
|
+
setComposingCount(composingKeysRef.current.size);
|
|
293
|
+
|
|
294
|
+
setInputValues((prev) => {
|
|
295
|
+
if (!(key in prev)) return prev;
|
|
296
|
+
const next = { ...prev };
|
|
297
|
+
delete next[key];
|
|
298
|
+
return next;
|
|
299
|
+
});
|
|
300
|
+
setSearchValues((prev) => {
|
|
301
|
+
if (!(key in prev)) return prev;
|
|
302
|
+
const next = { ...prev };
|
|
303
|
+
delete next[key];
|
|
304
|
+
return next;
|
|
305
|
+
});
|
|
306
|
+
}, []);
|
|
307
|
+
|
|
308
|
+
const clearAllSearchValues = useCallback(() => {
|
|
309
|
+
composingKeysRef.current.clear();
|
|
310
|
+
setComposingCount(0);
|
|
311
|
+
setInputValues({});
|
|
312
|
+
setSearchValues({});
|
|
313
|
+
setIsSearching(false);
|
|
314
|
+
}, []);
|
|
315
|
+
|
|
316
|
+
const isComposing = useCallback((key?: string) => {
|
|
317
|
+
return key ? composingKeysRef.current.has(key) : composingKeysRef.current.size > 0;
|
|
318
|
+
}, []);
|
|
262
319
|
|
|
263
320
|
useEffect(() => {
|
|
264
321
|
return () => {
|
|
@@ -270,8 +327,15 @@ const useMenuSearch = () => {
|
|
|
270
327
|
|
|
271
328
|
return {
|
|
272
329
|
searchValues,
|
|
273
|
-
|
|
330
|
+
inputValues,
|
|
331
|
+
isSearching: isSearching || composingCount > 0,
|
|
274
332
|
updateSearchValue,
|
|
333
|
+
updateInputValue,
|
|
334
|
+
startComposition,
|
|
335
|
+
endComposition,
|
|
336
|
+
clearSearchValue,
|
|
337
|
+
clearAllSearchValues,
|
|
338
|
+
isComposing,
|
|
275
339
|
};
|
|
276
340
|
};
|
|
277
341
|
|
|
@@ -358,29 +422,102 @@ const SearchInputWithAutoFocus: FC<InputProps & { visible: boolean }> = (props)
|
|
|
358
422
|
|
|
359
423
|
const getKeyPath = (path: string[], key: string) => [...path, key].join('/');
|
|
360
424
|
|
|
425
|
+
const normalizeOpenKeys = (nextOpenKeys: string[]) => {
|
|
426
|
+
const latestKey = nextOpenKeys[nextOpenKeys.length - 1];
|
|
427
|
+
|
|
428
|
+
if (!latestKey) {
|
|
429
|
+
return [];
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return nextOpenKeys.filter((key) => latestKey === key || latestKey.startsWith(`${key}/`));
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const getLabelSearchText = (label: React.ReactNode): string => {
|
|
436
|
+
if (label === null || label === undefined || typeof label === 'boolean') {
|
|
437
|
+
return '';
|
|
438
|
+
}
|
|
439
|
+
if (typeof label === 'string' || typeof label === 'number') {
|
|
440
|
+
return String(label);
|
|
441
|
+
}
|
|
442
|
+
if (Array.isArray(label)) {
|
|
443
|
+
return label.map(getLabelSearchText).join(' ');
|
|
444
|
+
}
|
|
445
|
+
if (React.isValidElement(label)) {
|
|
446
|
+
return getLabelSearchText(label.props.children);
|
|
447
|
+
}
|
|
448
|
+
return '';
|
|
449
|
+
};
|
|
450
|
+
|
|
361
451
|
const createSearchItem = (
|
|
362
452
|
item: Item,
|
|
363
453
|
searchKey: string,
|
|
364
454
|
currentSearchValue: string,
|
|
365
455
|
menuVisible: boolean,
|
|
366
456
|
t: (key: string) => string,
|
|
367
|
-
|
|
457
|
+
searchHandlers: {
|
|
458
|
+
updateSearchValue: (key: string, value: string) => void;
|
|
459
|
+
updateInputValue: (key: string, value: string) => void;
|
|
460
|
+
startComposition: (key: string) => void;
|
|
461
|
+
endComposition: (key: string, value: string) => void;
|
|
462
|
+
isComposing: (key: string) => boolean;
|
|
463
|
+
},
|
|
464
|
+
activateSearchSubmenu: (key: string) => void,
|
|
465
|
+
deactivateSearchSubmenu: (key: string) => void,
|
|
466
|
+
shouldActivateSearchSubmenu: boolean,
|
|
368
467
|
) => ({
|
|
369
468
|
key: `${searchKey}-search`,
|
|
370
469
|
type: 'group' as const,
|
|
371
470
|
label: (
|
|
372
|
-
<div>
|
|
471
|
+
<div onMouseDown={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}>
|
|
373
472
|
<SearchInputWithAutoFocus
|
|
374
473
|
visible={menuVisible}
|
|
375
474
|
variant="borderless"
|
|
376
475
|
allowClear
|
|
377
476
|
placeholder={t(item.searchPlaceholder || 'Search')}
|
|
378
477
|
value={currentSearchValue}
|
|
478
|
+
onFocus={(e) => {
|
|
479
|
+
e.stopPropagation();
|
|
480
|
+
}}
|
|
379
481
|
onChange={(e) => {
|
|
380
482
|
e.stopPropagation();
|
|
381
|
-
|
|
483
|
+
const value = e.target.value;
|
|
484
|
+
if (shouldActivateSearchSubmenu) {
|
|
485
|
+
activateSearchSubmenu(searchKey);
|
|
486
|
+
}
|
|
487
|
+
if ((e.nativeEvent as any)?.isComposing || searchHandlers.isComposing(searchKey)) {
|
|
488
|
+
searchHandlers.updateInputValue(searchKey, value);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (!value && shouldActivateSearchSubmenu) {
|
|
492
|
+
deactivateSearchSubmenu(searchKey);
|
|
493
|
+
}
|
|
494
|
+
searchHandlers.updateSearchValue(searchKey, value);
|
|
495
|
+
}}
|
|
496
|
+
onCompositionStart={(e) => {
|
|
497
|
+
e.stopPropagation();
|
|
498
|
+
if (shouldActivateSearchSubmenu) {
|
|
499
|
+
activateSearchSubmenu(searchKey);
|
|
500
|
+
}
|
|
501
|
+
searchHandlers.startComposition(searchKey);
|
|
502
|
+
}}
|
|
503
|
+
onCompositionEnd={(e) => {
|
|
504
|
+
e.stopPropagation();
|
|
505
|
+
const value = e.currentTarget.value;
|
|
506
|
+
if (shouldActivateSearchSubmenu) {
|
|
507
|
+
if (value) {
|
|
508
|
+
activateSearchSubmenu(searchKey);
|
|
509
|
+
} else {
|
|
510
|
+
deactivateSearchSubmenu(searchKey);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
searchHandlers.endComposition(searchKey, value);
|
|
514
|
+
}}
|
|
515
|
+
onClick={(e) => {
|
|
516
|
+
e.stopPropagation();
|
|
517
|
+
}}
|
|
518
|
+
onKeyDown={(e) => {
|
|
519
|
+
e.stopPropagation();
|
|
382
520
|
}}
|
|
383
|
-
onClick={(e) => e.stopPropagation()}
|
|
384
521
|
onMouseDown={(e) => {
|
|
385
522
|
// 防止菜单聚焦丢失或页面滚动
|
|
386
523
|
e.stopPropagation();
|
|
@@ -406,18 +543,31 @@ const createEmptyItem = (itemKey: string, t: (key: string) => string) => ({
|
|
|
406
543
|
disabled: true,
|
|
407
544
|
});
|
|
408
545
|
|
|
546
|
+
const KEEP_OPEN_LABEL_STYLE: React.CSSProperties = {
|
|
547
|
+
display: 'block',
|
|
548
|
+
width: '100%',
|
|
549
|
+
};
|
|
550
|
+
|
|
409
551
|
// ==================== Main Component ====================
|
|
410
552
|
|
|
411
553
|
// 短暂保持打开状态的注册表(用于跨父节点快速重建时的恢复)
|
|
412
554
|
const DROPDOWN_PERSIST_TTL_MS = 350;
|
|
555
|
+
const SUBMENU_CLOSE_DELAY = 0.05;
|
|
556
|
+
const SUBMENU_MOTION_DISABLED = {
|
|
557
|
+
motionEnter: false,
|
|
558
|
+
motionLeave: false,
|
|
559
|
+
};
|
|
413
560
|
const dropdownPersistRegistry: Map<string, number> = new Map();
|
|
414
561
|
|
|
415
562
|
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
|
|
416
563
|
const engine = useFlowEngine();
|
|
417
564
|
const [menuVisible, setMenuVisible] = useState(false);
|
|
418
565
|
const [openKeys, setOpenKeys] = useState<Set<string>>(new Set());
|
|
566
|
+
const [activeSearchKey, setActiveSearchKey] = useState<string | null>(null);
|
|
419
567
|
const [rootItems, setRootItems] = useState<Item[]>([]);
|
|
420
568
|
const [rootLoading, setRootLoading] = useState(false);
|
|
569
|
+
const closeByOutsideClickRef = useRef(false);
|
|
570
|
+
const skipPreserveActiveSearchRef = useRef(false);
|
|
421
571
|
const dropdownMaxHeight = useNiceDropdownMaxHeight();
|
|
422
572
|
const t = engine.translate.bind(engine);
|
|
423
573
|
|
|
@@ -432,10 +582,105 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
432
582
|
openKeys,
|
|
433
583
|
refreshKeys,
|
|
434
584
|
);
|
|
435
|
-
const
|
|
585
|
+
const searchHandlers = useMenuSearch();
|
|
586
|
+
const { searchValues, inputValues, clearSearchValue, clearAllSearchValues } = searchHandlers;
|
|
436
587
|
const { requestKeepOpen, shouldPreventClose } = useKeepDropdownOpen();
|
|
437
588
|
useSubmenuStyles(menuVisible, dropdownMaxHeight);
|
|
438
589
|
|
|
590
|
+
const closeMenu = useCallback(() => {
|
|
591
|
+
setMenuVisible(false);
|
|
592
|
+
setActiveSearchKey(null);
|
|
593
|
+
setOpenKeys(new Set());
|
|
594
|
+
clearAllSearchValues();
|
|
595
|
+
}, [clearAllSearchValues]);
|
|
596
|
+
|
|
597
|
+
const activateSearchSubmenu = useCallback((key: string) => {
|
|
598
|
+
setActiveSearchKey(key);
|
|
599
|
+
setOpenKeys((prev) => {
|
|
600
|
+
if (prev.has(key)) return prev;
|
|
601
|
+
const next = new Set(prev);
|
|
602
|
+
next.add(key);
|
|
603
|
+
return next;
|
|
604
|
+
});
|
|
605
|
+
}, []);
|
|
606
|
+
|
|
607
|
+
const deactivateSearchSubmenu = useCallback((key: string) => {
|
|
608
|
+
setActiveSearchKey((prev) => (prev === key ? null : prev));
|
|
609
|
+
}, []);
|
|
610
|
+
|
|
611
|
+
const closeActiveSearchForPath = useCallback(
|
|
612
|
+
(keyPath: string) => {
|
|
613
|
+
if (
|
|
614
|
+
!activeSearchKey ||
|
|
615
|
+
keyPath === activeSearchKey ||
|
|
616
|
+
keyPath.startsWith(`${activeSearchKey}/`) ||
|
|
617
|
+
activeSearchKey.startsWith(`${keyPath}/`)
|
|
618
|
+
) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
skipPreserveActiveSearchRef.current = true;
|
|
623
|
+
clearSearchValue(activeSearchKey);
|
|
624
|
+
setActiveSearchKey(null);
|
|
625
|
+
setOpenKeys((prev) => {
|
|
626
|
+
const next = new Set(prev);
|
|
627
|
+
next.delete(activeSearchKey);
|
|
628
|
+
return next;
|
|
629
|
+
});
|
|
630
|
+
},
|
|
631
|
+
[activeSearchKey, clearSearchValue],
|
|
632
|
+
);
|
|
633
|
+
|
|
634
|
+
const handleMenuOpenChange = useCallback(
|
|
635
|
+
(nextOpenKeys: string[]) => {
|
|
636
|
+
let normalized = normalizeOpenKeys(nextOpenKeys);
|
|
637
|
+
if (activeSearchKey && openKeys.has(activeSearchKey) && !normalized.includes(activeSearchKey)) {
|
|
638
|
+
if (normalized.length || skipPreserveActiveSearchRef.current) {
|
|
639
|
+
clearSearchValue(activeSearchKey);
|
|
640
|
+
setActiveSearchKey(null);
|
|
641
|
+
} else {
|
|
642
|
+
normalized = [activeSearchKey];
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!normalized.length && shouldPreventClose()) {
|
|
647
|
+
dropdownMenuProps.onOpenChange?.(Array.from(openKeys));
|
|
648
|
+
skipPreserveActiveSearchRef.current = false;
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
Array.from(openKeys).forEach((key) => {
|
|
653
|
+
if (!normalized.includes(key)) {
|
|
654
|
+
clearSearchValue(key);
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
setOpenKeys(new Set(normalized));
|
|
658
|
+
dropdownMenuProps.onOpenChange?.(normalized);
|
|
659
|
+
skipPreserveActiveSearchRef.current = false;
|
|
660
|
+
},
|
|
661
|
+
[activeSearchKey, clearSearchValue, dropdownMenuProps, openKeys, shouldPreventClose],
|
|
662
|
+
);
|
|
663
|
+
|
|
664
|
+
useEffect(() => {
|
|
665
|
+
if (!menuVisible) return;
|
|
666
|
+
|
|
667
|
+
const markOutsideClick = (event: MouseEvent | PointerEvent) => {
|
|
668
|
+
const target = event.target as HTMLElement | null;
|
|
669
|
+
const isOutside = !target?.closest('.ant-dropdown, .ant-dropdown-menu, .ant-dropdown-menu-submenu-popup');
|
|
670
|
+
closeByOutsideClickRef.current = isOutside;
|
|
671
|
+
if (isOutside) {
|
|
672
|
+
closeMenu();
|
|
673
|
+
}
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
document.addEventListener('pointerdown', markOutsideClick, true);
|
|
677
|
+
document.addEventListener('mousedown', markOutsideClick, true);
|
|
678
|
+
return () => {
|
|
679
|
+
document.removeEventListener('pointerdown', markOutsideClick, true);
|
|
680
|
+
document.removeEventListener('mousedown', markOutsideClick, true);
|
|
681
|
+
};
|
|
682
|
+
}, [closeMenu, menuVisible]);
|
|
683
|
+
|
|
439
684
|
// 在挂载时,若存在 persistKey 且仍在持久期内,则尝试恢复打开状态
|
|
440
685
|
useEffect(() => {
|
|
441
686
|
if (!persistKey) return;
|
|
@@ -460,6 +705,12 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
460
705
|
};
|
|
461
706
|
}, [persistKey, menuVisible]);
|
|
462
707
|
|
|
708
|
+
useEffect(() => {
|
|
709
|
+
if (!menuVisible) {
|
|
710
|
+
setOpenKeys(new Set());
|
|
711
|
+
}
|
|
712
|
+
}, [menuVisible]);
|
|
713
|
+
|
|
463
714
|
// 加载根 items,支持同步/异步函数
|
|
464
715
|
useEffect(() => {
|
|
465
716
|
const loadRootItems = async () => {
|
|
@@ -492,21 +743,18 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
492
743
|
): any[] {
|
|
493
744
|
const searchKey = keyPath;
|
|
494
745
|
const currentSearchValue = searchValues[searchKey] || '';
|
|
746
|
+
const currentInputValue = inputValues[searchKey] ?? currentSearchValue;
|
|
747
|
+
const shouldActivateSearchSubmenu = !(item.type === 'group' && path.length === 0);
|
|
495
748
|
|
|
496
749
|
// 递归过滤:当 child 为分组时,会继续向下过滤其 children;
|
|
497
750
|
// 仅保留自身匹配或存在匹配子项的分组。
|
|
498
751
|
const filteredChildren = currentSearchValue
|
|
499
752
|
? (function deepFilter(items: Item[]): Item[] {
|
|
500
753
|
const searchText = currentSearchValue.toLowerCase();
|
|
501
|
-
const tryString = (v: any) => {
|
|
502
|
-
if (!v) return '';
|
|
503
|
-
return typeof v === 'string' ? v : String(v);
|
|
504
|
-
};
|
|
505
754
|
return items
|
|
506
755
|
.map((child) => {
|
|
507
|
-
const labelStr =
|
|
508
|
-
const selfMatch =
|
|
509
|
-
labelStr.includes(searchText) || (child.key && String(child.key).toLowerCase().includes(searchText));
|
|
756
|
+
const labelStr = getLabelSearchText(child.label).toLowerCase();
|
|
757
|
+
const selfMatch = labelStr.includes(searchText);
|
|
510
758
|
if (child.type === 'group' && Array.isArray(child.children)) {
|
|
511
759
|
const nested = deepFilter(child.children);
|
|
512
760
|
if (selfMatch || nested.length > 0) {
|
|
@@ -521,7 +769,17 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
521
769
|
: children;
|
|
522
770
|
|
|
523
771
|
const resolvedFiltered = resolve(filteredChildren, [...path, item.key]);
|
|
524
|
-
const searchItem = createSearchItem(
|
|
772
|
+
const searchItem = createSearchItem(
|
|
773
|
+
item,
|
|
774
|
+
searchKey,
|
|
775
|
+
currentInputValue,
|
|
776
|
+
menuVisible,
|
|
777
|
+
t,
|
|
778
|
+
searchHandlers,
|
|
779
|
+
activateSearchSubmenu,
|
|
780
|
+
deactivateSearchSubmenu,
|
|
781
|
+
shouldActivateSearchSubmenu,
|
|
782
|
+
);
|
|
525
783
|
const dividerItem = { key: `${keyPath}-search-divider`, type: 'divider' as const };
|
|
526
784
|
|
|
527
785
|
if (currentSearchValue && resolvedFiltered.length === 0) {
|
|
@@ -588,56 +846,75 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
588
846
|
return { type: 'divider', key: keyPath };
|
|
589
847
|
}
|
|
590
848
|
|
|
849
|
+
const label = typeof item.label === 'string' ? t(item.label) : item.label;
|
|
850
|
+
|
|
591
851
|
// 非 group 的“子菜单”也支持本层级搜索:当 item.searchable = true 且存在 children 时
|
|
592
852
|
if (item.searchable && children) {
|
|
593
853
|
return {
|
|
594
|
-
key:
|
|
595
|
-
label
|
|
854
|
+
key: keyPath,
|
|
855
|
+
label,
|
|
596
856
|
onClick: (info: any) => {},
|
|
597
|
-
onMouseEnter: () =>
|
|
598
|
-
setOpenKeys((prev) => {
|
|
599
|
-
if (prev.has(keyPath)) return prev;
|
|
600
|
-
const next = new Set(prev);
|
|
601
|
-
next.add(keyPath);
|
|
602
|
-
return next;
|
|
603
|
-
});
|
|
604
|
-
},
|
|
857
|
+
onMouseEnter: () => closeActiveSearchForPath(keyPath),
|
|
605
858
|
children: buildSearchChildren(children, item, keyPath, path, menuVisible, resolveItems),
|
|
606
859
|
};
|
|
607
860
|
}
|
|
608
861
|
|
|
862
|
+
const itemShouldKeepOpen = !children && (item.keepDropdownOpen ?? keepDropdownOpen ?? false);
|
|
863
|
+
const handleLeafClick = (info: any) => {
|
|
864
|
+
if (children) {
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
if (itemShouldKeepOpen) {
|
|
869
|
+
requestKeepOpen();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
const extendedInfo: ExtendedMenuInfo = {
|
|
873
|
+
...info,
|
|
874
|
+
key: info?.key ?? keyPath,
|
|
875
|
+
keyPath: info?.keyPath ?? [keyPath],
|
|
876
|
+
item: info?.item || item,
|
|
877
|
+
originalItem: item,
|
|
878
|
+
keepDropdownOpen: itemShouldKeepOpen,
|
|
879
|
+
};
|
|
880
|
+
|
|
881
|
+
menu.onClick?.(extendedInfo);
|
|
882
|
+
};
|
|
883
|
+
|
|
609
884
|
return {
|
|
610
885
|
key: keyPath,
|
|
611
|
-
label:
|
|
886
|
+
label: itemShouldKeepOpen ? (
|
|
887
|
+
<div
|
|
888
|
+
style={KEEP_OPEN_LABEL_STYLE}
|
|
889
|
+
onMouseDown={(event) => {
|
|
890
|
+
event.stopPropagation();
|
|
891
|
+
requestKeepOpen();
|
|
892
|
+
}}
|
|
893
|
+
onClick={(event) => {
|
|
894
|
+
event.stopPropagation();
|
|
895
|
+
handleLeafClick({
|
|
896
|
+
key: keyPath,
|
|
897
|
+
keyPath: [keyPath],
|
|
898
|
+
item,
|
|
899
|
+
domEvent: event,
|
|
900
|
+
});
|
|
901
|
+
}}
|
|
902
|
+
>
|
|
903
|
+
{label}
|
|
904
|
+
</div>
|
|
905
|
+
) : (
|
|
906
|
+
label
|
|
907
|
+
),
|
|
612
908
|
onClick: (info: any) => {
|
|
613
|
-
if (
|
|
909
|
+
if (!itemShouldKeepOpen) handleLeafClick(info);
|
|
910
|
+
},
|
|
911
|
+
onMouseEnter: () => closeActiveSearchForPath(keyPath),
|
|
912
|
+
onMouseDown: () => {
|
|
913
|
+
if (!itemShouldKeepOpen) {
|
|
614
914
|
return;
|
|
615
915
|
}
|
|
616
916
|
|
|
617
|
-
|
|
618
|
-
const itemShouldKeepOpen = item.keepDropdownOpen ?? keepDropdownOpen ?? false;
|
|
619
|
-
|
|
620
|
-
// 如果需要保持菜单打开,请求保持打开状态
|
|
621
|
-
if (itemShouldKeepOpen) {
|
|
622
|
-
requestKeepOpen();
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
const extendedInfo: ExtendedMenuInfo = {
|
|
626
|
-
...info,
|
|
627
|
-
item: info.item || item,
|
|
628
|
-
originalItem: item,
|
|
629
|
-
keepDropdownOpen: itemShouldKeepOpen,
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
menu.onClick?.(extendedInfo);
|
|
633
|
-
},
|
|
634
|
-
onMouseEnter: () => {
|
|
635
|
-
setOpenKeys((prev) => {
|
|
636
|
-
if (prev.has(keyPath)) return prev;
|
|
637
|
-
const next = new Set(prev);
|
|
638
|
-
next.add(keyPath);
|
|
639
|
-
return next;
|
|
640
|
-
});
|
|
917
|
+
requestKeepOpen();
|
|
641
918
|
},
|
|
642
919
|
children:
|
|
643
920
|
children && children.length > 0
|
|
@@ -684,17 +961,20 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
684
961
|
placement="bottomLeft"
|
|
685
962
|
menu={{
|
|
686
963
|
...dropdownMenuProps,
|
|
964
|
+
openKeys: Array.from(openKeys),
|
|
687
965
|
items: items,
|
|
966
|
+
subMenuCloseDelay: dropdownMenuProps.subMenuCloseDelay ?? SUBMENU_CLOSE_DELAY,
|
|
967
|
+
motion: dropdownMenuProps.motion ?? SUBMENU_MOTION_DISABLED,
|
|
688
968
|
onClick: () => {},
|
|
969
|
+
onOpenChange: handleMenuOpenChange,
|
|
689
970
|
style: {
|
|
690
971
|
maxHeight: dropdownMaxHeight,
|
|
691
972
|
overflowY: 'auto',
|
|
692
973
|
...dropdownMenuProps?.style,
|
|
693
974
|
},
|
|
694
975
|
}}
|
|
695
|
-
onOpenChange={(visible) => {
|
|
696
|
-
|
|
697
|
-
if (!visible && isSearching) {
|
|
976
|
+
onOpenChange={(visible, info) => {
|
|
977
|
+
if (!visible && activeSearchKey && info?.source === 'trigger' && !closeByOutsideClickRef.current) {
|
|
698
978
|
return;
|
|
699
979
|
}
|
|
700
980
|
|
|
@@ -703,7 +983,12 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
|
|
|
703
983
|
return;
|
|
704
984
|
}
|
|
705
985
|
|
|
706
|
-
|
|
986
|
+
if (!visible) {
|
|
987
|
+
closeMenu();
|
|
988
|
+
} else {
|
|
989
|
+
setMenuVisible(visible);
|
|
990
|
+
}
|
|
991
|
+
closeByOutsideClickRef.current = false;
|
|
707
992
|
}}
|
|
708
993
|
>
|
|
709
994
|
{props.children}
|