@tmagic/editor 1.8.0-manmanyu.8 → 1.8.0
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/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +22 -13
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +10 -5
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +7 -6
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +21 -119
- package/dist/es/components/ContentMenu.vue_vue_type_script_setup_true_lang.js +21 -5
- package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +62 -16
- package/dist/es/components/Icon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/Resizer.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/components/ScrollBar.js +0 -1
- package/dist/es/components/ScrollBar.vue_vue_type_script_setup_true_lang.js +5 -4
- package/dist/es/components/ScrollViewer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/SearchInput.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/SplitView.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ToolButton.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/ViewForm.js +5 -0
- package/dist/es/components/ViewForm.vue_vue_type_script_setup_true_lang.js +56 -0
- package/dist/es/editorProps.js +7 -1
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +30 -113
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/CondOpSelect.vue_vue_type_script_setup_true_lang.js +7 -19
- package/dist/es/fields/DataSourceFieldSelect/FieldSelect.vue_vue_type_script_setup_true_lang.js +7 -6
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +4 -2
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +12 -4
- package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +9 -20
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +2 -2
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +5 -3
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +9 -4
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +10 -110
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +52 -363
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/PageFragmentSelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +3 -6
- package/dist/es/fields/StyleSetter/components/BackgroundPosition.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +4 -35
- package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/fields/StyleSetter/configs.js +663 -0
- package/dist/es/fields/StyleSetter/icons/align-items/Center.js +1 -1
- package/dist/es/fields/StyleSetter/icons/align-items/FlexEnd.js +1 -1
- package/dist/es/fields/StyleSetter/icons/align-items/FlexStart.js +1 -1
- package/dist/es/fields/StyleSetter/icons/align-items/SpaceAround.js +1 -1
- package/dist/es/fields/StyleSetter/icons/align-items/SpaceBetween.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/NoRepeat.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/Repeat.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatX.js +1 -1
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatY.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/Block.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/Flex.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/Inline.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/InlineBlock.js +1 -1
- package/dist/es/fields/StyleSetter/icons/display/None.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/Column.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/ColumnReverse.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/Row.js +1 -1
- package/dist/es/fields/StyleSetter/icons/flex-direction/RowReverse.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/Center.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexEnd.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexStart.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceAround.js +1 -1
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceBetween.js +1 -1
- package/dist/es/fields/StyleSetter/icons/text-align/Center.js +1 -1
- package/dist/es/fields/StyleSetter/icons/text-align/Left.js +1 -1
- package/dist/es/fields/StyleSetter/icons/text-align/Right.js +1 -1
- package/dist/es/fields/StyleSetter/pro/Background.vue_vue_type_script_setup_true_lang.js +29 -106
- package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +8 -11
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +30 -89
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +29 -297
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +29 -82
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +8 -24
- package/dist/es/fields/UISelect.js +0 -1
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/configs/codeSelect.js +98 -0
- package/dist/es/fields/configs/displayConds.js +158 -0
- package/dist/es/fields/configs/eventSelect.js +225 -0
- package/dist/es/fields/configs/stickyAddButton.js +21 -0
- package/dist/es/fields/headless-validation.js +139 -0
- package/dist/es/headless.js +2 -0
- package/dist/es/hooks/use-code-block-edit.js +4 -2
- package/dist/es/hooks/use-compare-form.js +101 -0
- package/dist/es/hooks/use-form-context.js +29 -0
- package/dist/es/hooks/use-stage.js +1 -0
- package/dist/es/icons/CenterIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/icons/CodeIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/icons/DatasourceIcon.js +1 -1
- package/dist/es/icons/FolderMinusIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/index.js +17 -9
- package/dist/es/initService.js +52 -16
- package/dist/es/layouts/AddPageBox.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/CodeEditor.vue_vue_type_script_setup_true_lang.js +26 -10
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +11 -8
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/layouts/NavMenuColumn.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/Bucket.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +8 -5
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +26 -10
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +36 -28
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/history-list/composables.js +1 -1
- package/dist/es/layouts/history-list/useHistoryList.js +4 -4
- package/dist/es/layouts/history-list/useHistoryRevert.js +38 -33
- package/dist/es/layouts/page-bar/AddButton.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/PageBar.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/PageBarScrollContainer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/PageList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/page-bar/Search.vue_vue_type_script_setup_true_lang.js +2 -2
- package/dist/es/layouts/props-panel/FormPanel.vue_vue_type_script_setup_true_lang.js +50 -20
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +53 -35
- package/dist/es/layouts/props-panel/use-style-panel.js +3 -2
- package/dist/es/layouts/sidebar/ComponentListPanel.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +21 -36
- package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/code-block/CodeBlockListPanel.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/es/layouts/sidebar/code-block/useContentMenu.js +19 -4
- package/dist/es/layouts/sidebar/data-source/DataSourceAddButton.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceConfigPanel.vue_vue_type_script_setup_true_lang.js +9 -4
- package/dist/es/layouts/sidebar/data-source/DataSourceList.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/data-source/DataSourceListPanel.vue_vue_type_script_setup_true_lang.js +4 -4
- package/dist/es/layouts/sidebar/data-source/useContentMenu.js +19 -4
- package/dist/es/layouts/sidebar/layer/LayerMenu.vue_vue_type_script_setup_true_lang.js +3 -3
- package/dist/es/layouts/sidebar/layer/LayerNodeContent.js +5 -0
- package/dist/es/layouts/sidebar/layer/LayerNodeContent.vue_vue_type_script_setup_true_lang.js +46 -0
- package/dist/es/layouts/sidebar/layer/LayerNodeTool.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +6 -4
- package/dist/es/layouts/sidebar/layer/use-click.js +3 -3
- package/dist/es/layouts/sidebar/layer/use-drag.js +11 -10
- package/dist/es/layouts/sidebar/layer/use-node-status.js +2 -2
- package/dist/es/layouts/workspace/Breadcrumb.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/Workspace.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/NodeListMenu.vue_vue_type_script_setup_true_lang.js +9 -3
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +55 -9
- package/dist/es/layouts/workspace/viewer/StageOverlay.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/workspace/viewer/ViewerMenu.vue_vue_type_script_setup_true_lang.js +8 -8
- package/dist/es/plugin.js +35 -23
- package/dist/es/services/codeBlock.js +40 -31
- package/dist/es/services/dataSource.js +46 -34
- package/dist/es/services/dep.js +162 -41
- package/dist/es/services/editor.js +373 -137
- package/dist/es/services/events.js +4 -4
- package/dist/es/services/history.js +275 -392
- package/dist/es/services/keybinding.js +3 -3
- package/dist/es/services/props.js +2 -1
- package/dist/es/services/storage.js +2 -2
- package/dist/es/services/ui.js +7 -2
- package/dist/es/style.css +277 -114
- package/dist/es/utils/code-block.js +14 -7
- package/dist/es/utils/config.js +4 -1
- package/dist/es/utils/const.js +2 -2
- package/dist/es/utils/content-menu.js +2 -2
- package/dist/es/utils/data-source/index.js +34 -12
- package/dist/es/utils/dep/collect-worker-client.js +154 -0
- package/dist/es/utils/dep/idle-task.js +70 -22
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor.js +4 -2
- package/dist/es/utils/event.js +135 -0
- package/dist/es/utils/history.js +35 -47
- package/dist/es/utils/monaco-editor.js +22 -6
- package/dist/es/utils/operator.js +2 -2
- package/dist/es/utils/props.js +50 -106
- package/dist/es/utils/type-match-rules.js +356 -0
- package/dist/style.css +277 -114
- package/dist/themes/magic-admin.css +978 -175
- package/dist/tmagic-editor-headless.umd.cjs +10880 -0
- package/dist/tmagic-editor.umd.cjs +13047 -11363
- package/package.json +18 -13
- package/src/Editor.vue +23 -6
- package/src/components/CodeBlockEditor.vue +3 -0
- package/src/components/CodeParams.vue +10 -4
- package/src/components/CompareForm.vue +20 -190
- package/src/components/ContentMenu.vue +34 -6
- package/src/components/FloatingBox.vue +71 -7
- package/src/components/ScrollBar.vue +0 -67
- package/src/components/ViewForm.vue +54 -0
- package/src/editorProps.ts +23 -10
- package/src/fields/Code.vue +1 -1
- package/src/fields/CodeSelect.vue +20 -113
- package/src/fields/CodeSelectCol.vue +1 -1
- package/src/fields/CondOpSelect.vue +5 -26
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +2 -2
- package/src/fields/DataSourceFieldSelect/Index.vue +4 -1
- package/src/fields/DataSourceFields.vue +5 -1
- package/src/fields/DataSourceInput.vue +10 -28
- package/src/fields/DataSourceMethodSelect.vue +1 -1
- package/src/fields/DataSourceMethods.vue +6 -2
- package/src/fields/DataSourceMocks.vue +3 -0
- package/src/fields/DataSourceSelect.vue +1 -1
- package/src/fields/DisplayConds.vue +17 -139
- package/src/fields/EventSelect.vue +29 -416
- package/src/fields/KeyValue.vue +1 -1
- package/src/fields/StyleSetter/Index.vue +2 -8
- package/src/fields/StyleSetter/components/Border.vue +4 -38
- package/src/fields/StyleSetter/components/Box.vue +1 -1
- package/src/fields/StyleSetter/components/Position.vue +1 -1
- package/src/fields/StyleSetter/configs.ts +690 -0
- package/src/fields/StyleSetter/pro/Background.vue +9 -64
- package/src/fields/StyleSetter/pro/Border.vue +5 -16
- package/src/fields/StyleSetter/pro/Font.vue +11 -71
- package/src/fields/StyleSetter/pro/Layout.vue +10 -230
- package/src/fields/StyleSetter/pro/Position.vue +11 -82
- package/src/fields/StyleSetter/pro/Transform.vue +6 -34
- package/src/fields/UISelect.vue +0 -12
- package/src/fields/configs/codeSelect.ts +127 -0
- package/src/fields/configs/displayConds.ts +184 -0
- package/src/fields/configs/eventSelect.ts +309 -0
- package/src/fields/configs/stickyAddButton.ts +32 -0
- package/src/fields/headless-validation.ts +177 -0
- package/src/headless.ts +33 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-compare-form.ts +166 -0
- package/src/hooks/use-form-context.ts +56 -0
- package/src/hooks/use-stage.ts +1 -0
- package/src/index.ts +9 -0
- package/src/initService.ts +73 -18
- package/src/layouts/CodeEditor.vue +32 -6
- package/src/layouts/Framework.vue +7 -6
- package/src/layouts/history-list/BucketTab.vue +2 -2
- package/src/layouts/history-list/HistoryDiffDialog.vue +33 -15
- package/src/layouts/history-list/HistoryListPanel.vue +16 -18
- package/src/layouts/history-list/useHistoryRevert.ts +57 -41
- package/src/layouts/props-panel/FormPanel.vue +75 -20
- package/src/layouts/props-panel/PropsPanel.vue +81 -30
- package/src/layouts/props-panel/use-style-panel.ts +4 -3
- package/src/layouts/sidebar/ComponentListPanel.vue +6 -2
- package/src/layouts/sidebar/Sidebar.vue +5 -3
- package/src/layouts/sidebar/code-block/CodeBlockListPanel.vue +5 -2
- package/src/layouts/sidebar/code-block/useContentMenu.ts +14 -2
- package/src/layouts/sidebar/data-source/DataSourceConfigPanel.vue +3 -0
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +3 -3
- package/src/layouts/sidebar/data-source/useContentMenu.ts +14 -2
- package/src/layouts/sidebar/layer/LayerMenu.vue +2 -2
- package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
- package/src/layouts/sidebar/layer/LayerPanel.vue +5 -2
- package/src/layouts/sidebar/layer/use-click.ts +3 -3
- package/src/layouts/sidebar/layer/use-node-status.ts +2 -2
- package/src/layouts/workspace/viewer/NodeListMenu.vue +4 -1
- package/src/layouts/workspace/viewer/Stage.vue +80 -10
- package/src/layouts/workspace/viewer/ViewerMenu.vue +7 -7
- package/src/plugin.ts +35 -25
- package/src/services/codeBlock.ts +4 -0
- package/src/services/dataSource.ts +3 -0
- package/src/services/dep.ts +292 -47
- package/src/services/editor.ts +383 -120
- package/src/services/events.ts +8 -7
- package/src/services/history.ts +22 -1
- package/src/services/keybinding.ts +3 -3
- package/src/services/ui.ts +26 -5
- package/src/theme/code-block.scss +0 -37
- package/src/theme/display-conds.scss +4 -0
- package/src/theme/event.scss +25 -33
- package/src/theme/floating-box.scss +9 -1
- package/src/theme/framework.scss +4 -1
- package/src/theme/layer-node-content.scss +14 -0
- package/src/theme/nav-menu.scss +5 -1
- package/src/theme/props-panel.scss +17 -3
- package/src/theme/scroll-bar.scss +64 -0
- package/src/theme/theme.scss +3 -0
- package/src/theme/themes/magic-admin/index.scss +109 -9
- package/src/theme/ui-select.scss +9 -0
- package/src/type.ts +216 -6
- package/src/utils/code-block.ts +1 -0
- package/src/utils/config.ts +9 -1
- package/src/utils/const.ts +3 -3
- package/src/utils/content-menu.ts +2 -2
- package/src/utils/data-source/index.ts +31 -12
- package/src/utils/dep/collect-worker-client.ts +215 -0
- package/src/utils/dep/idle-task.ts +104 -35
- package/src/utils/dep/worker.ts +113 -9
- package/src/utils/event.ts +237 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/monaco-editor.ts +27 -0
- package/src/utils/operator.ts +2 -2
- package/src/utils/props.ts +61 -122
- package/src/utils/type-match-rules.ts +697 -0
- package/types/headless.d.ts +17 -0
- package/types/index.d.ts +1494 -978
- package/src/fields/StyleSetter/icons/align-items/index copy.ts +0 -5
package/src/services/editor.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { reactive, toRaw } from 'vue';
|
|
19
|
+
import { nextTick, reactive, toRaw } from 'vue';
|
|
20
20
|
import { cloneDeep, isEmpty, isEqual, isObject, mergeWith, uniq } from 'lodash-es';
|
|
21
21
|
|
|
22
22
|
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
|
@@ -30,7 +30,9 @@ import {
|
|
|
30
30
|
guid,
|
|
31
31
|
isPage,
|
|
32
32
|
isPageFragment,
|
|
33
|
+
isPageOrFragment,
|
|
33
34
|
setValueByKeyPath,
|
|
35
|
+
traverseNode,
|
|
34
36
|
} from '@tmagic/utils';
|
|
35
37
|
|
|
36
38
|
import BaseService from '@editor/services//BaseService';
|
|
@@ -47,11 +49,14 @@ import type {
|
|
|
47
49
|
EditorNodeInfo,
|
|
48
50
|
HistoryOpSource,
|
|
49
51
|
HistoryOpType,
|
|
52
|
+
NodeInvalidInfo,
|
|
53
|
+
NodeInvalidSource,
|
|
50
54
|
PastePosition,
|
|
51
55
|
StepDiffItem,
|
|
52
56
|
StepValue,
|
|
53
57
|
StoreState,
|
|
54
58
|
StoreStateKey,
|
|
59
|
+
UpdateOptions,
|
|
55
60
|
} from '@editor/type';
|
|
56
61
|
import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
|
|
57
62
|
import {
|
|
@@ -79,6 +84,10 @@ import { beforePaste, getAddParent } from '@editor/utils/operator';
|
|
|
79
84
|
|
|
80
85
|
type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
|
|
81
86
|
|
|
87
|
+
/** 历史插回时把记录的下标收敛到 [0, length],越界(含未记录)一律追加到末尾 */
|
|
88
|
+
const clampIndex = (index: number | undefined, length: number): number =>
|
|
89
|
+
typeof index === 'number' && index >= 0 && index <= length ? index : length;
|
|
90
|
+
|
|
82
91
|
/**
|
|
83
92
|
* 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
|
|
84
93
|
* `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
|
|
@@ -104,18 +113,29 @@ class Editor extends BaseService {
|
|
|
104
113
|
stageLoading: true,
|
|
105
114
|
highlightNode: null,
|
|
106
115
|
modifiedNodeIds: new Map(),
|
|
116
|
+
invalidNodeIds: new Map(),
|
|
107
117
|
pageLength: 0,
|
|
108
118
|
pageFragmentLength: 0,
|
|
109
119
|
disabledMultiSelect: false,
|
|
110
120
|
alwaysMultiSelect: false,
|
|
111
121
|
});
|
|
112
122
|
private selectionBeforeOp: Id[] | null = null;
|
|
123
|
+
/**
|
|
124
|
+
* 操作前的节点校验错误快照,与 selectionBeforeOp 同时在 captureSelectionBeforeOp 中捕获,
|
|
125
|
+
* 供 pushOpHistory 写入 step.extra.invalidNodeIdsBefore,用于撤销时还原到「操作前」的错误状态。
|
|
126
|
+
*/
|
|
127
|
+
private invalidNodeIdsBeforeOp: Map<Id, NodeInvalidInfo> | null = null;
|
|
113
128
|
/**
|
|
114
129
|
* 最近一次 pushOpHistory 写入的历史记录 uuid。
|
|
115
130
|
* 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
|
|
116
131
|
* 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
|
|
117
132
|
*/
|
|
118
133
|
private lastPushedHistoryId: string | null = null;
|
|
134
|
+
/**
|
|
135
|
+
* 上一次 doAdd 的插入记忆:nodeId 为插入的节点,selectedId 为当时的选中节点。
|
|
136
|
+
* 仅在选中节点未变化时复用(连续 add / doNotSelect / 批量粘贴),选中变化后自动失效,无需手动清理。
|
|
137
|
+
*/
|
|
138
|
+
private lastAdded: { selectedId: Id; nodeId: Id } | null = null;
|
|
119
139
|
|
|
120
140
|
constructor() {
|
|
121
141
|
super(
|
|
@@ -127,7 +147,7 @@ class Editor extends BaseService {
|
|
|
127
147
|
|
|
128
148
|
/**
|
|
129
149
|
* 设置当前指点节点配置
|
|
130
|
-
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
150
|
+
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
131
151
|
* @param value MNode
|
|
132
152
|
* @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
|
|
133
153
|
*/
|
|
@@ -180,12 +200,15 @@ class Editor extends BaseService {
|
|
|
180
200
|
}
|
|
181
201
|
|
|
182
202
|
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
|
|
203
|
+
|
|
204
|
+
// 整体替换 DSL 后,清理不再存在于新 DSL 中的失效节点错误记录,避免残留误报。
|
|
205
|
+
this.pruneInvalidNodeIds();
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
|
|
186
209
|
/**
|
|
187
210
|
* 获取当前指点节点配置
|
|
188
|
-
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength'
|
|
211
|
+
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength'
|
|
189
212
|
* @returns MNode
|
|
190
213
|
*/
|
|
191
214
|
public get<K extends StoreStateKey>(name: K): StoreState[K] {
|
|
@@ -260,7 +283,7 @@ class Editor extends BaseService {
|
|
|
260
283
|
public isOnDifferentPage(node: MNode): boolean {
|
|
261
284
|
const currentPageId = this.get('page')?.id;
|
|
262
285
|
if (currentPageId === undefined || currentPageId === null) return false;
|
|
263
|
-
if (
|
|
286
|
+
if (isPageOrFragment(node)) {
|
|
264
287
|
return `${node.id}` !== `${currentPageId}`;
|
|
265
288
|
}
|
|
266
289
|
const nodePage = this.getNodeInfo(node.id, false).page;
|
|
@@ -391,7 +414,7 @@ class Editor extends BaseService {
|
|
|
391
414
|
this.set('highlightNode', null);
|
|
392
415
|
}
|
|
393
416
|
|
|
394
|
-
public async doAdd(node: MNode, parent: MContainer): Promise<MNode> {
|
|
417
|
+
public async doAdd(node: MNode, parent: MContainer, _options: DslOpOptions = {}): Promise<MNode> {
|
|
395
418
|
const root = this.get('root');
|
|
396
419
|
|
|
397
420
|
if (!root) throw new Error('root为空');
|
|
@@ -401,17 +424,20 @@ class Editor extends BaseService {
|
|
|
401
424
|
|
|
402
425
|
if (!curNode) throw new Error('当前选中节点为空');
|
|
403
426
|
|
|
404
|
-
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !(
|
|
427
|
+
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !isPageOrFragment(node)) {
|
|
405
428
|
throw new Error('app下不能添加组件');
|
|
406
429
|
}
|
|
407
430
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
431
|
+
// 连续 add 时接在上一个新增节点之后,否则接在当前选中节点之后;
|
|
432
|
+
// 锚点节点可能已不在 items 中(被删 / DSL 整体替换),回退到当前选中节点,都不在则追加到末尾
|
|
433
|
+
const anchorId = this.lastAdded?.selectedId === curNode.id ? this.lastAdded.nodeId : curNode.id;
|
|
434
|
+
const anchorIndex = isPageOrFragment(node)
|
|
435
|
+
? -1
|
|
436
|
+
: Math.max(getNodeIndex(anchorId, parent), getNodeIndex(curNode.id, parent));
|
|
437
|
+
const insertIndex = anchorIndex < 0 ? parent.items.length : anchorIndex + 1;
|
|
438
|
+
|
|
439
|
+
parent.items.splice(insertIndex, 0, node);
|
|
440
|
+
this.lastAdded = { selectedId: curNode.id, nodeId: node.id };
|
|
415
441
|
|
|
416
442
|
const layout = await this.getLayout(toRaw(parent), node as MNode);
|
|
417
443
|
node.style = getInitPositionStyle(node.style, layout);
|
|
@@ -421,6 +447,7 @@ class Editor extends BaseService {
|
|
|
421
447
|
parent: cloneDeep(parent),
|
|
422
448
|
parentId: parent.id,
|
|
423
449
|
root: cloneDeep(root),
|
|
450
|
+
index: insertIndex,
|
|
424
451
|
});
|
|
425
452
|
|
|
426
453
|
const newStyle = fixNodePosition(node, parent, stage);
|
|
@@ -439,24 +466,23 @@ class Editor extends BaseService {
|
|
|
439
466
|
* 向指点容器添加组件节点
|
|
440
467
|
* @param addConfig 将要添加的组件节点配置
|
|
441
468
|
* @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
|
442
|
-
* @param options
|
|
443
|
-
* @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
|
444
|
-
* @param options.doNotSwitchPage 添加后是否不切换当前页面(默认 false;新增页面 / 跨页新增时为 true 会跳过会引发页面切换的选中操作)
|
|
445
|
-
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
469
|
+
* @param options 可选配置,见 {@link DslOpOptions}
|
|
446
470
|
* @returns 添加后的节点
|
|
447
471
|
*/
|
|
448
472
|
public async add(
|
|
449
473
|
addNode: AddMNode | MNode[],
|
|
450
474
|
parent?: MContainer | null,
|
|
451
|
-
{
|
|
475
|
+
options: DslOpOptions = {},
|
|
476
|
+
): Promise<MNode | MNode[]> {
|
|
477
|
+
this.captureSelectionBeforeOp();
|
|
478
|
+
|
|
479
|
+
const {
|
|
452
480
|
doNotSelect = false,
|
|
453
481
|
doNotSwitchPage = false,
|
|
454
482
|
doNotPushHistory = false,
|
|
455
483
|
historyDescription,
|
|
456
484
|
historySource,
|
|
457
|
-
}
|
|
458
|
-
): Promise<MNode | MNode[]> {
|
|
459
|
-
this.captureSelectionBeforeOp();
|
|
485
|
+
} = options;
|
|
460
486
|
|
|
461
487
|
const stage = this.get('stage');
|
|
462
488
|
|
|
@@ -472,17 +498,15 @@ class Editor extends BaseService {
|
|
|
472
498
|
addNodes.push(...addNode);
|
|
473
499
|
}
|
|
474
500
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
}),
|
|
485
|
-
);
|
|
501
|
+
// 必须串行:每个节点的插入位置依赖上一个已插入的节点,并行时插入顺序会受
|
|
502
|
+
// doAdd 前置耗时(如异步 beforeDoAdd 插件钩子)影响而错乱
|
|
503
|
+
const newNodes: MNode[] = [];
|
|
504
|
+
for (const node of addNodes) {
|
|
505
|
+
const root = this.get('root');
|
|
506
|
+
const parentNode = isPageOrFragment(node) && root ? root : (parent ?? getAddParent(node));
|
|
507
|
+
if (!parentNode) throw new Error('未找到父元素');
|
|
508
|
+
newNodes.push(await this.doAdd(node, parentNode, options));
|
|
509
|
+
}
|
|
486
510
|
|
|
487
511
|
if (newNodes.length > 1) {
|
|
488
512
|
// 多选时只要任一新增节点位于非当前页面,触发的 multiSelect 就会引起页面切换
|
|
@@ -511,7 +535,7 @@ class Editor extends BaseService {
|
|
|
511
535
|
}
|
|
512
536
|
}
|
|
513
537
|
|
|
514
|
-
if (!(
|
|
538
|
+
if (!isPageOrFragment(newNodes[0])) {
|
|
515
539
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
516
540
|
if (!doNotPushHistory) {
|
|
517
541
|
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
|
@@ -535,6 +559,18 @@ class Editor extends BaseService {
|
|
|
535
559
|
}
|
|
536
560
|
|
|
537
561
|
this.emit('add', newNodes);
|
|
562
|
+
this.emit('change', {
|
|
563
|
+
type: 'add',
|
|
564
|
+
data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
565
|
+
historySource,
|
|
566
|
+
doNotPushHistory,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// 页面 / 页面片新增不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
|
570
|
+
const addedPages = newNodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
|
571
|
+
if (addedPages.length) {
|
|
572
|
+
historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
|
|
573
|
+
}
|
|
538
574
|
|
|
539
575
|
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
|
540
576
|
}
|
|
@@ -554,12 +590,14 @@ class Editor extends BaseService {
|
|
|
554
590
|
|
|
555
591
|
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
|
|
556
592
|
|
|
557
|
-
parent.items?.splice(index, 1);
|
|
558
593
|
const stage = this.get('stage');
|
|
559
|
-
|
|
594
|
+
const currentPage = this.get('page');
|
|
595
|
+
const isDeletingCurrentPage = isPageOrFragment(node) && !!currentPage && `${currentPage.id}` === `${node.id}`;
|
|
596
|
+
|
|
597
|
+
parent.items?.splice(index, 1);
|
|
560
598
|
|
|
561
599
|
// 始终清理已删除节点在 state 中的残留引用:
|
|
562
|
-
// - 即使后续会调用
|
|
600
|
+
// - 即使后续会调用 select 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
|
|
563
601
|
const selectedNodes = this.get('nodes');
|
|
564
602
|
const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`);
|
|
565
603
|
if (removedSelectedIndex !== -1) {
|
|
@@ -567,38 +605,44 @@ class Editor extends BaseService {
|
|
|
567
605
|
nextSelected.splice(removedSelectedIndex, 1);
|
|
568
606
|
this.set('nodes', nextSelected);
|
|
569
607
|
}
|
|
570
|
-
if (isPage(node) || isPageFragment(node)) {
|
|
571
|
-
const currentPage = this.get('page');
|
|
572
|
-
if (currentPage && `${currentPage.id}` === `${node.id}`) {
|
|
573
|
-
this.set('page', null);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
const selectDefault = async (pages: MNode[]) => {
|
|
578
|
-
if (pages[0]) {
|
|
579
|
-
await this.select(pages[0]);
|
|
580
|
-
stage?.select(pages[0].id);
|
|
581
|
-
} else {
|
|
582
|
-
this.selectRoot();
|
|
583
|
-
}
|
|
584
|
-
};
|
|
585
608
|
|
|
609
|
+
const removeData = { id: node.id, parentId: parent.id, root: cloneDeep(root) };
|
|
586
610
|
const rootItems = root.items || [];
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
611
|
+
// 删非当前页时画布不应该变动,所以只有删的是当前页才重新选中
|
|
612
|
+
const shouldReselect = isDeletingCurrentPage && !doNotSelect && !doNotSwitchPage;
|
|
613
|
+
// 还有剩余页面才能切过去,否则退回选中 root
|
|
614
|
+
const shouldSwitchPage = shouldReselect && rootItems.length > 0;
|
|
615
|
+
|
|
616
|
+
if (isPageOrFragment(node)) {
|
|
617
|
+
if (isPage(node)) {
|
|
618
|
+
this.state.pageLength -= 1;
|
|
619
|
+
} else {
|
|
620
|
+
this.state.pageFragmentLength -= 1;
|
|
594
621
|
}
|
|
595
|
-
} else if (isPageFragment(node)) {
|
|
596
|
-
this.state.pageFragmentLength -= 1;
|
|
597
622
|
|
|
598
|
-
if (
|
|
599
|
-
|
|
623
|
+
if (shouldSwitchPage) {
|
|
624
|
+
// runtime 收到页面删除时会销毁当前渲染的 page 实例,必须先把画布切到剩余页面再通知删除,
|
|
625
|
+
// 否则画布会被清空,且要等下一次 updatePageId 才能恢复。
|
|
626
|
+
// nextTick 用于等 Stage 的 page watch 把新页面 id 同步给 runtime。
|
|
627
|
+
await this.select(rootItems[0]);
|
|
628
|
+
stage?.select(rootItems[0].id);
|
|
629
|
+
await nextTick();
|
|
630
|
+
stage?.remove(removeData);
|
|
631
|
+
} else {
|
|
632
|
+
// page 置空会让 Workspace 卸载 Stage 并销毁 renderer,删除通知必须在卸载前发出,
|
|
633
|
+
// 且不能 await:runtime 未 ready 时 getRuntime 的监听会随 destroy 一起被移除,永远不会 resolve
|
|
634
|
+
stage?.remove(removeData);
|
|
635
|
+
|
|
636
|
+
if (shouldReselect) {
|
|
637
|
+
// 页面已全部删完
|
|
638
|
+
this.selectRoot();
|
|
639
|
+
} else if (isDeletingCurrentPage) {
|
|
640
|
+
this.set('page', null);
|
|
641
|
+
}
|
|
600
642
|
}
|
|
601
643
|
} else {
|
|
644
|
+
stage?.remove(removeData);
|
|
645
|
+
|
|
602
646
|
if (!doNotSelect) {
|
|
603
647
|
await this.select(parent);
|
|
604
648
|
stage?.select(parent.id);
|
|
@@ -621,23 +665,18 @@ class Editor extends BaseService {
|
|
|
621
665
|
* @param options.doNotSwitchPage 删除后是否不切换当前页面(默认 false;删除页面 / 页面片段时为 true 会跳过自动切换到首个剩余页面)
|
|
622
666
|
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
623
667
|
*/
|
|
624
|
-
public async remove(
|
|
625
|
-
|
|
626
|
-
{
|
|
627
|
-
doNotSelect = false,
|
|
628
|
-
doNotSwitchPage = false,
|
|
629
|
-
doNotPushHistory = false,
|
|
630
|
-
historyDescription,
|
|
631
|
-
historySource,
|
|
632
|
-
}: DslOpOptions = {},
|
|
633
|
-
): Promise<void> {
|
|
668
|
+
public async remove(nodeOrNodeList: MNode | MNode[], options: DslOpOptions = {}): Promise<void> {
|
|
669
|
+
const { doNotPushHistory = false, historyDescription, historySource } = options;
|
|
634
670
|
this.captureSelectionBeforeOp();
|
|
635
671
|
|
|
636
672
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
|
637
673
|
|
|
674
|
+
// 删除后节点已从树中移除,无法再反查所属 page,这里在删除前先逐个捕获用于 `change` 事件
|
|
675
|
+
const changeItems = nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) }));
|
|
676
|
+
|
|
638
677
|
const removedItems: StepDiffItem<MNode>[] = [];
|
|
639
678
|
let pageForOp: { name: string; id: Id } | null = null;
|
|
640
|
-
if (!(
|
|
679
|
+
if (!isPageOrFragment(nodes[0])) {
|
|
641
680
|
for (const n of nodes) {
|
|
642
681
|
const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
|
|
643
682
|
if (parent && curNode) {
|
|
@@ -654,7 +693,10 @@ class Editor extends BaseService {
|
|
|
654
693
|
}
|
|
655
694
|
}
|
|
656
695
|
|
|
657
|
-
await Promise.all(nodes.map((node) => this.doRemove(node,
|
|
696
|
+
await Promise.all(nodes.map((node) => this.doRemove(node, options)));
|
|
697
|
+
|
|
698
|
+
// 删除节点时同步清理其(含子树)的校验错误记录;置于 pushOpHistory 之前,使历史快照与本次删除对齐。
|
|
699
|
+
this.removeInvalidNodesBySubtree(nodes);
|
|
658
700
|
|
|
659
701
|
if (removedItems.length > 0 && pageForOp) {
|
|
660
702
|
if (!doNotPushHistory) {
|
|
@@ -670,12 +712,20 @@ class Editor extends BaseService {
|
|
|
670
712
|
}
|
|
671
713
|
|
|
672
714
|
this.emit('remove', nodes);
|
|
715
|
+
this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
|
|
716
|
+
|
|
717
|
+
// 页面 / 页面片删除不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
|
718
|
+
const removedPages = nodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
|
719
|
+
if (removedPages.length) {
|
|
720
|
+
historyService.notifyPageStructureChange({ add: [], remove: removedPages });
|
|
721
|
+
}
|
|
673
722
|
}
|
|
674
723
|
|
|
675
724
|
public async doUpdate(
|
|
676
725
|
config: MNode,
|
|
677
|
-
|
|
726
|
+
data: UpdateOptions = {},
|
|
678
727
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
|
728
|
+
const { changeRecords = [], historySource, replace = false } = data;
|
|
679
729
|
const root = this.get('root');
|
|
680
730
|
if (!root) throw new Error('root为空');
|
|
681
731
|
|
|
@@ -687,9 +737,14 @@ class Editor extends BaseService {
|
|
|
687
737
|
|
|
688
738
|
const node = toRaw(info.node);
|
|
689
739
|
|
|
690
|
-
|
|
740
|
+
// replace=true 时跳过 toggleFixedPosition / mergeWith / setChildrenLayout,直接用传入配置整节点替换
|
|
741
|
+
let newConfig = replace
|
|
742
|
+
? cloneDeep(toRaw(config))
|
|
743
|
+
: await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
|
|
691
744
|
|
|
692
|
-
|
|
745
|
+
if (!replace) {
|
|
746
|
+
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
|
747
|
+
}
|
|
693
748
|
|
|
694
749
|
if (!newConfig.type) throw new Error('配置缺少type值');
|
|
695
750
|
|
|
@@ -710,10 +765,12 @@ class Editor extends BaseService {
|
|
|
710
765
|
|
|
711
766
|
if (!parentNodeItems || typeof index === 'undefined' || index === -1) throw new Error('更新的节点未找到');
|
|
712
767
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
768
|
+
if (!replace) {
|
|
769
|
+
const newLayout = await this.getLayout(newConfig);
|
|
770
|
+
const layout = await this.getLayout(node);
|
|
771
|
+
if (Array.isArray(newConfig.items) && newLayout !== layout) {
|
|
772
|
+
newConfig = setChildrenLayout(newConfig as MContainer, newLayout);
|
|
773
|
+
}
|
|
717
774
|
}
|
|
718
775
|
|
|
719
776
|
parentNodeItems[index] = newConfig;
|
|
@@ -727,7 +784,7 @@ class Editor extends BaseService {
|
|
|
727
784
|
}
|
|
728
785
|
|
|
729
786
|
// 只有被更新节点正好是当前选中页面时才同步 state.page,避免「更新非当前页」误将编辑器切到该页
|
|
730
|
-
if (
|
|
787
|
+
if (isPageOrFragment(newConfig)) {
|
|
731
788
|
const currentPage = this.get('page');
|
|
732
789
|
if (currentPage && `${currentPage.id}` === `${newConfig.id}`) {
|
|
733
790
|
this.set('page', newConfig as MPage | MPageFragment);
|
|
@@ -747,26 +804,20 @@ class Editor extends BaseService {
|
|
|
747
804
|
* 更新节点
|
|
748
805
|
* update后会触发依赖收集,收集完后会掉stage.update方法
|
|
749
806
|
* @param config 新的节点配置,配置中需要有id信息
|
|
750
|
-
* @param data
|
|
751
|
-
* @param data.changeRecords 单节点 form 端变更记录(多节点场景下被忽略,使用 changeRecordList)
|
|
752
|
-
* @param data.changeRecordList 多节点 form 端变更记录列表,按 config 数组同序对应每个节点;优先级高于 changeRecords
|
|
753
|
-
* @param data.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
754
|
-
* @param data.historyDescription 入栈时附带的人类可读描述,用于历史面板展示(不影响 undo/redo 行为)
|
|
807
|
+
* @param data 额外数据,见 {@link UpdateOptions}
|
|
755
808
|
* @returns 更新后的节点配置
|
|
756
809
|
*/
|
|
757
|
-
public async update(
|
|
758
|
-
config: MNode | MNode[],
|
|
759
|
-
data: {
|
|
760
|
-
changeRecords?: ChangeRecord[];
|
|
761
|
-
changeRecordList?: ChangeRecord[][];
|
|
762
|
-
doNotPushHistory?: boolean;
|
|
763
|
-
historyDescription?: string;
|
|
764
|
-
historySource?: HistoryOpSource;
|
|
765
|
-
} = {},
|
|
766
|
-
): Promise<MNode | MNode[]> {
|
|
810
|
+
public async update(config: MNode | MNode[], data: UpdateOptions = {}): Promise<MNode | MNode[]> {
|
|
767
811
|
this.captureSelectionBeforeOp();
|
|
768
812
|
|
|
769
|
-
const {
|
|
813
|
+
const {
|
|
814
|
+
doNotPushHistory = false,
|
|
815
|
+
changeRecordList,
|
|
816
|
+
changeRecords,
|
|
817
|
+
historyDescription,
|
|
818
|
+
historySource,
|
|
819
|
+
invalidInfo,
|
|
820
|
+
} = data;
|
|
770
821
|
|
|
771
822
|
const nodes = Array.isArray(config) ? config : [config];
|
|
772
823
|
|
|
@@ -775,10 +826,13 @@ class Editor extends BaseService {
|
|
|
775
826
|
const updateData = await Promise.all(
|
|
776
827
|
nodes.map((node, index) => {
|
|
777
828
|
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
778
|
-
return this.doUpdate(node, { changeRecords: recordsForNode
|
|
829
|
+
return this.doUpdate(node, { ...data, changeRecords: recordsForNode });
|
|
779
830
|
}),
|
|
780
831
|
);
|
|
781
832
|
|
|
833
|
+
// 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。
|
|
834
|
+
this.applyInvalidInfo(config, invalidInfo);
|
|
835
|
+
|
|
782
836
|
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
|
|
783
837
|
const curNodes = this.get('nodes');
|
|
784
838
|
if (curNodes.length) {
|
|
@@ -805,6 +859,12 @@ class Editor extends BaseService {
|
|
|
805
859
|
}
|
|
806
860
|
|
|
807
861
|
this.emit('update', updateData);
|
|
862
|
+
this.emit('change', {
|
|
863
|
+
type: 'update',
|
|
864
|
+
data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
|
|
865
|
+
historySource,
|
|
866
|
+
doNotPushHistory,
|
|
867
|
+
});
|
|
808
868
|
return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
|
|
809
869
|
}
|
|
810
870
|
|
|
@@ -1046,6 +1106,13 @@ class Editor extends BaseService {
|
|
|
1046
1106
|
}
|
|
1047
1107
|
|
|
1048
1108
|
this.emit('move-layer', offset);
|
|
1109
|
+
this.emit('change', {
|
|
1110
|
+
type: 'move-layer',
|
|
1111
|
+
data: [{ node, page: this.getPageOfNode(node.id) }],
|
|
1112
|
+
offset,
|
|
1113
|
+
historySource,
|
|
1114
|
+
doNotPushHistory,
|
|
1115
|
+
});
|
|
1049
1116
|
}
|
|
1050
1117
|
|
|
1051
1118
|
/**
|
|
@@ -1074,7 +1141,7 @@ class Editor extends BaseService {
|
|
|
1074
1141
|
}: DslOpOptions = {},
|
|
1075
1142
|
): Promise<MNode | MNode[]> {
|
|
1076
1143
|
const isBatch = Array.isArray(config);
|
|
1077
|
-
const configs = (isBatch ? config : [config]).filter((item) => !(
|
|
1144
|
+
const configs = (isBatch ? config : [config]).filter((item) => !isPageOrFragment(item));
|
|
1078
1145
|
|
|
1079
1146
|
if (configs.length === 0) {
|
|
1080
1147
|
throw new Error('没有可移动的节点');
|
|
@@ -1225,6 +1292,14 @@ class Editor extends BaseService {
|
|
|
1225
1292
|
}
|
|
1226
1293
|
|
|
1227
1294
|
this.emit('drag-to', { targetIndex, configs, targetParent });
|
|
1295
|
+
this.emit('change', {
|
|
1296
|
+
type: 'drag-to',
|
|
1297
|
+
data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
1298
|
+
targetIndex,
|
|
1299
|
+
targetParent,
|
|
1300
|
+
historySource,
|
|
1301
|
+
doNotPushHistory,
|
|
1302
|
+
});
|
|
1228
1303
|
}
|
|
1229
1304
|
|
|
1230
1305
|
// #region AndGetHistoryId
|
|
@@ -1260,13 +1335,7 @@ class Editor extends BaseService {
|
|
|
1260
1335
|
/** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1261
1336
|
public async updateAndGetHistoryId(
|
|
1262
1337
|
config: MNode | MNode[],
|
|
1263
|
-
data: {
|
|
1264
|
-
changeRecords?: ChangeRecord[];
|
|
1265
|
-
changeRecordList?: ChangeRecord[][];
|
|
1266
|
-
doNotPushHistory?: boolean;
|
|
1267
|
-
historyDescription?: string;
|
|
1268
|
-
historySource?: HistoryOpSource;
|
|
1269
|
-
} = {},
|
|
1338
|
+
data: UpdateOptions = {},
|
|
1270
1339
|
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1271
1340
|
this.lastPushedHistoryId = null;
|
|
1272
1341
|
const result = await this.update(config, data);
|
|
@@ -1519,6 +1588,7 @@ class Editor extends BaseService {
|
|
|
1519
1588
|
this.set('stage', null);
|
|
1520
1589
|
this.set('highlightNode', null);
|
|
1521
1590
|
this.set('modifiedNodeIds', new Map());
|
|
1591
|
+
this.set('invalidNodeIds', new Map());
|
|
1522
1592
|
this.set('pageLength', 0);
|
|
1523
1593
|
}
|
|
1524
1594
|
|
|
@@ -1532,6 +1602,62 @@ class Editor extends BaseService {
|
|
|
1532
1602
|
this.get('modifiedNodeIds').clear();
|
|
1533
1603
|
}
|
|
1534
1604
|
|
|
1605
|
+
/**
|
|
1606
|
+
* 记录(或覆盖)某个节点在指定来源(属性表单 / 样式表单)上的校验错误信息。
|
|
1607
|
+
* @param id 节点 id
|
|
1608
|
+
* @param source 错误来源:'props'(属性表单)| 'style'(样式表单)
|
|
1609
|
+
* @param message 错误文案(可能是包含 <br> 的 HTML)
|
|
1610
|
+
*/
|
|
1611
|
+
public setInvalidNode(id: Id, source: NodeInvalidSource, message: string) {
|
|
1612
|
+
const map = this.get('invalidNodeIds');
|
|
1613
|
+
const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
|
|
1614
|
+
info[source] = message;
|
|
1615
|
+
map.set(id, info);
|
|
1616
|
+
this.emit('invalid-node-change', map);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
/**
|
|
1620
|
+
* 删除节点的校验错误记录。
|
|
1621
|
+
* @param id 节点 id
|
|
1622
|
+
* @param source 指定来源则仅删除该来源;不传则删除该节点全部来源的错误
|
|
1623
|
+
*/
|
|
1624
|
+
public deleteInvalidNode(id: Id, source?: NodeInvalidSource) {
|
|
1625
|
+
const map = this.get('invalidNodeIds');
|
|
1626
|
+
if (!map.has(id)) return;
|
|
1627
|
+
|
|
1628
|
+
if (!source) {
|
|
1629
|
+
map.delete(id);
|
|
1630
|
+
} else {
|
|
1631
|
+
const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
|
|
1632
|
+
delete info[source];
|
|
1633
|
+
if (info.props || info.style) {
|
|
1634
|
+
map.set(id, info);
|
|
1635
|
+
} else {
|
|
1636
|
+
map.delete(id);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
this.emit('invalid-node-change', map);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/** 获取当前存在校验错误的节点错误 Map(key 为节点 id) */
|
|
1644
|
+
public getInvalidNodeIds(): Map<Id, NodeInvalidInfo> {
|
|
1645
|
+
return this.get('invalidNodeIds');
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
/** 获取指定节点的校验错误信息 */
|
|
1649
|
+
public getInvalidNodeInfo(id: Id): NodeInvalidInfo | undefined {
|
|
1650
|
+
return this.get('invalidNodeIds').get(id);
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/** 清空全部校验错误记录 */
|
|
1654
|
+
public resetInvalidNodeId() {
|
|
1655
|
+
const map = this.get('invalidNodeIds');
|
|
1656
|
+
if (map.size === 0) return;
|
|
1657
|
+
map.clear();
|
|
1658
|
+
this.emit('invalid-node-change', map);
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1535
1661
|
public usePlugin(options: AsyncHookPlugin<AsyncMethodName, Editor>): void {
|
|
1536
1662
|
super.usePlugin(options);
|
|
1537
1663
|
}
|
|
@@ -1554,13 +1680,92 @@ class Editor extends BaseService {
|
|
|
1554
1680
|
return super.emit(eventName, ...args);
|
|
1555
1681
|
}
|
|
1556
1682
|
|
|
1683
|
+
/**
|
|
1684
|
+
* 应用一次属性面板提交携带的校验错误信息(在写入历史记录之前调用,使历史快照与本次变更对齐)。
|
|
1685
|
+
* - invalidInfo 为空(调用方未携带):认为本次更新对应的节点已无校验错误,清除其全部来源的错误;
|
|
1686
|
+
* - error 非空则记录错误,为空则清除对应来源的错误。
|
|
1687
|
+
*/
|
|
1688
|
+
private applyInvalidInfo(
|
|
1689
|
+
config: MNode | MNode[],
|
|
1690
|
+
invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string },
|
|
1691
|
+
) {
|
|
1692
|
+
if (!invalidInfo) {
|
|
1693
|
+
// 调用方未携带 invalidInfo:本次更新对应节点不应再保留校验错误,清除其全部来源。
|
|
1694
|
+
const ids = (Array.isArray(config) ? config : [config])
|
|
1695
|
+
.map((node) => node.id)
|
|
1696
|
+
.filter((id): id is Id => id !== undefined);
|
|
1697
|
+
ids.forEach((id) => this.deleteInvalidNode(id));
|
|
1698
|
+
return;
|
|
1699
|
+
}
|
|
1700
|
+
const { id, source, error } = invalidInfo;
|
|
1701
|
+
if (error) {
|
|
1702
|
+
this.setInvalidNode(id, source, error);
|
|
1703
|
+
} else {
|
|
1704
|
+
this.deleteInvalidNode(id, source);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/** 删除被移除节点(含其子树)的校验错误记录,避免残留误报 */
|
|
1709
|
+
private removeInvalidNodesBySubtree(nodes: MNode[]) {
|
|
1710
|
+
const map = this.get('invalidNodeIds');
|
|
1711
|
+
if (map.size === 0) return;
|
|
1712
|
+
|
|
1713
|
+
let changed = false;
|
|
1714
|
+
nodes.forEach((node) => {
|
|
1715
|
+
traverseNode(node, (n) => {
|
|
1716
|
+
if (n.id !== undefined && map.delete(n.id)) {
|
|
1717
|
+
changed = true;
|
|
1718
|
+
}
|
|
1719
|
+
});
|
|
1720
|
+
});
|
|
1721
|
+
|
|
1722
|
+
if (changed) {
|
|
1723
|
+
this.emit('invalid-node-change', map);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
|
|
1727
|
+
/** 清理不在当前 DSL 中的失效节点错误记录(用于整体替换 root 后) */
|
|
1728
|
+
private pruneInvalidNodeIds() {
|
|
1729
|
+
const map = this.get('invalidNodeIds');
|
|
1730
|
+
if (map.size === 0) return;
|
|
1731
|
+
|
|
1732
|
+
let changed = false;
|
|
1733
|
+
for (const id of [...map.keys()]) {
|
|
1734
|
+
if (!this.getNodeById(id, false)) {
|
|
1735
|
+
map.delete(id);
|
|
1736
|
+
changed = true;
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
if (changed) {
|
|
1741
|
+
this.emit('invalid-node-change', map);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1557
1745
|
private addModifiedNodeId(id: Id) {
|
|
1558
1746
|
this.get('modifiedNodeIds').set(id, id);
|
|
1559
1747
|
}
|
|
1560
1748
|
|
|
1749
|
+
/**
|
|
1750
|
+
* 获取指定节点所属的页面 / 页面片:
|
|
1751
|
+
* - 普通节点返回其所在的 page;
|
|
1752
|
+
* - 节点本身就是 page / pageFragment 时返回它自己;
|
|
1753
|
+
* - 找不到时返回 null。
|
|
1754
|
+
* 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
|
|
1755
|
+
*/
|
|
1756
|
+
private getPageOfNode(id: Id): MPage | MPageFragment | null {
|
|
1757
|
+
const { node, page } = this.getNodeInfo(id, false);
|
|
1758
|
+
if (page) return page;
|
|
1759
|
+
if (node && isPageOrFragment(node)) return node as MPage | MPageFragment;
|
|
1760
|
+
return null;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1561
1763
|
private captureSelectionBeforeOp() {
|
|
1562
1764
|
if (this.selectionBeforeOp) return;
|
|
1563
1765
|
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
|
1766
|
+
// 与选区快照同步捕获「操作前」的校验错误状态;因 applyInvalidInfo 会在 pushOpHistory 前修改 invalidNodeIds,
|
|
1767
|
+
// 必须在此(操作最开始)留存操作前快照,供 undo 还原。
|
|
1768
|
+
this.invalidNodeIdsBeforeOp = new Map(this.get('invalidNodeIds'));
|
|
1564
1769
|
}
|
|
1565
1770
|
|
|
1566
1771
|
/**
|
|
@@ -1579,6 +1784,10 @@ class Editor extends BaseService {
|
|
|
1579
1784
|
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
|
1580
1785
|
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
|
1581
1786
|
|
|
1787
|
+
// 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
|
|
1788
|
+
const addedPages: (MPage | MPageFragment)[] = [];
|
|
1789
|
+
const removedPages: (MPage | MPageFragment)[] = [];
|
|
1790
|
+
|
|
1582
1791
|
nextPages.forEach((nextPage) => {
|
|
1583
1792
|
const prevPage = prevMap.get(`${nextPage.id}`);
|
|
1584
1793
|
if (!prevPage) {
|
|
@@ -1588,6 +1797,7 @@ class Editor extends BaseService {
|
|
|
1588
1797
|
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
|
1589
1798
|
source,
|
|
1590
1799
|
);
|
|
1800
|
+
addedPages.push(nextPage);
|
|
1591
1801
|
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
|
1592
1802
|
this.pushPageDiffStep(
|
|
1593
1803
|
'update',
|
|
@@ -1606,8 +1816,13 @@ class Editor extends BaseService {
|
|
|
1606
1816
|
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
|
1607
1817
|
source,
|
|
1608
1818
|
);
|
|
1819
|
+
removedPages.push(prevPage);
|
|
1609
1820
|
}
|
|
1610
1821
|
});
|
|
1822
|
+
|
|
1823
|
+
if (addedPages.length || removedPages.length) {
|
|
1824
|
+
historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
|
|
1825
|
+
}
|
|
1611
1826
|
}
|
|
1612
1827
|
|
|
1613
1828
|
/**
|
|
@@ -1667,6 +1882,9 @@ class Editor extends BaseService {
|
|
|
1667
1882
|
selectedBefore: this.selectionBeforeOp ?? [],
|
|
1668
1883
|
selectedAfter: this.get('nodes').map((n) => n.id),
|
|
1669
1884
|
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
|
1885
|
+
// 方向性双快照:undo 还原「操作前」错误状态,redo 还原「操作后」错误状态。
|
|
1886
|
+
invalidNodeIdsBefore: new Map(this.invalidNodeIdsBeforeOp ?? this.get('invalidNodeIds')),
|
|
1887
|
+
invalidNodeIdsAfter: new Map(this.get('invalidNodeIds')),
|
|
1670
1888
|
},
|
|
1671
1889
|
diff,
|
|
1672
1890
|
};
|
|
@@ -1679,16 +1897,19 @@ class Editor extends BaseService {
|
|
|
1679
1897
|
const historyId = pushed ? step.uuid : null;
|
|
1680
1898
|
this.lastPushedHistoryId = historyId;
|
|
1681
1899
|
this.selectionBeforeOp = null;
|
|
1900
|
+
this.invalidNodeIdsBeforeOp = null;
|
|
1682
1901
|
return historyId;
|
|
1683
1902
|
}
|
|
1684
1903
|
|
|
1685
1904
|
/**
|
|
1686
1905
|
* 应用历史操作(撤销 / 重做)
|
|
1687
1906
|
*
|
|
1688
|
-
*
|
|
1689
|
-
* `doNotSelect / doNotSwitchPage`
|
|
1907
|
+
* 删除 / 更新类修改走 `editor.remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
|
1908
|
+
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底;
|
|
1909
|
+
* 「重新插回节点」(撤销 remove / 重做 add)需按 step 记录的 parentId / index 精确还原,
|
|
1910
|
+
* 不走 this.add,由手工 splice + stage.add 完成后补发等价的 add / change 事件(见 emitHistoryInsertEvents)。
|
|
1690
1911
|
*
|
|
1691
|
-
*
|
|
1912
|
+
* 注意:这些路径都会发出 add / remove / update / change 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
|
1692
1913
|
* 请监听 `history-change` 事件配合判断。
|
|
1693
1914
|
*
|
|
1694
1915
|
* @param step 操作记录
|
|
@@ -1701,7 +1922,14 @@ class Editor extends BaseService {
|
|
|
1701
1922
|
const stage = this.get('stage');
|
|
1702
1923
|
if (!root) return;
|
|
1703
1924
|
|
|
1704
|
-
|
|
1925
|
+
// 撤销/重做内部复用 add/remove/update,透传被应用 step 上记录的 source,
|
|
1926
|
+
// 使其触发的 change 事件同样携带「历史来源」
|
|
1927
|
+
const commonOpts = {
|
|
1928
|
+
doNotSelect: true,
|
|
1929
|
+
doNotSwitchPage: true,
|
|
1930
|
+
doNotPushHistory: true,
|
|
1931
|
+
historySource: step.source,
|
|
1932
|
+
} as const;
|
|
1705
1933
|
|
|
1706
1934
|
switch (step.opType) {
|
|
1707
1935
|
case 'add': {
|
|
@@ -1717,46 +1945,56 @@ class Editor extends BaseService {
|
|
|
1717
1945
|
}
|
|
1718
1946
|
} else {
|
|
1719
1947
|
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
|
1720
|
-
// 按目标 index
|
|
1948
|
+
// 按目标 index 升序逐个插入,先小后大避免索引漂移。
|
|
1949
|
+
// 不走 this.add:doAdd 依赖当前选中节点(无选区时直接抛错),且只能插到选中节点之后 / 末尾,
|
|
1950
|
+
// 无法按 step 记录的 parentId / index 精确还原,还会重算 style 破坏已记录的终态位置。
|
|
1721
1951
|
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1952
|
+
const addedNodes: MNode[] = [];
|
|
1722
1953
|
for (const { newSchema, parentId, index } of sorted) {
|
|
1723
1954
|
if (!newSchema || parentId === undefined) continue;
|
|
1724
1955
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1725
1956
|
if (parent?.items) {
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
}
|
|
1957
|
+
const addedNode = cloneDeep(newSchema);
|
|
1958
|
+
const insertIndex = clampIndex(index, parent.items.length);
|
|
1959
|
+
parent.items.splice(insertIndex, 0, addedNode);
|
|
1960
|
+
addedNodes.push(addedNode);
|
|
1731
1961
|
await stage?.add({
|
|
1732
1962
|
config: cloneDeep(newSchema),
|
|
1733
1963
|
parent: cloneDeep(parent),
|
|
1734
1964
|
parentId: parent.id,
|
|
1735
1965
|
root: cloneDeep(root),
|
|
1966
|
+
index: insertIndex,
|
|
1736
1967
|
});
|
|
1737
1968
|
}
|
|
1738
1969
|
}
|
|
1970
|
+
this.emitHistoryInsertEvents(addedNodes, step.source);
|
|
1739
1971
|
}
|
|
1740
1972
|
break;
|
|
1741
1973
|
}
|
|
1742
1974
|
case 'remove': {
|
|
1743
1975
|
const items = step.diff ?? [];
|
|
1744
1976
|
if (reverse) {
|
|
1745
|
-
// 撤销 remove:按原 index
|
|
1977
|
+
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移);不走 this.add 的原因同上
|
|
1746
1978
|
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1979
|
+
const addedNodes: MNode[] = [];
|
|
1747
1980
|
for (const { oldSchema, parentId, index } of sorted) {
|
|
1748
1981
|
if (!oldSchema || parentId === undefined) continue;
|
|
1749
1982
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1750
1983
|
if (parent?.items) {
|
|
1751
|
-
|
|
1984
|
+
const addedNode = cloneDeep(oldSchema);
|
|
1985
|
+
const insertIndex = clampIndex(index, parent.items.length);
|
|
1986
|
+
parent.items.splice(insertIndex, 0, addedNode);
|
|
1987
|
+
addedNodes.push(addedNode);
|
|
1752
1988
|
await stage?.add({
|
|
1753
1989
|
config: cloneDeep(oldSchema),
|
|
1754
1990
|
parent: cloneDeep(parent),
|
|
1755
1991
|
parentId,
|
|
1756
1992
|
root: cloneDeep(root),
|
|
1993
|
+
index: insertIndex,
|
|
1757
1994
|
});
|
|
1758
1995
|
}
|
|
1759
1996
|
}
|
|
1997
|
+
this.emitHistoryInsertEvents(addedNodes, step.source);
|
|
1760
1998
|
} else {
|
|
1761
1999
|
// 重做 remove:再删一次
|
|
1762
2000
|
for (const { oldSchema } of items) {
|
|
@@ -1796,7 +2034,7 @@ class Editor extends BaseService {
|
|
|
1796
2034
|
return cloneDeep(reverse ? oldNode : newNode);
|
|
1797
2035
|
});
|
|
1798
2036
|
if (configs.length) {
|
|
1799
|
-
await this.update(configs, { doNotPushHistory: true });
|
|
2037
|
+
await this.update(configs, { doNotPushHistory: true, historySource: step.source });
|
|
1800
2038
|
}
|
|
1801
2039
|
break;
|
|
1802
2040
|
}
|
|
@@ -1804,6 +2042,15 @@ class Editor extends BaseService {
|
|
|
1804
2042
|
|
|
1805
2043
|
this.set('modifiedNodeIds', step.extra?.modifiedNodeIds ?? new Map());
|
|
1806
2044
|
|
|
2045
|
+
// 还原校验错误标记:因 undo/redo 复用同一 step,需按方向取「操作前 / 操作后」快照——
|
|
2046
|
+
// undo(reverse=true) 还原到操作前的错误状态(撤销一个「校验失败」的改动后错误消失),
|
|
2047
|
+
// redo(reverse=false) 还原到操作后的错误状态(重做后错误恢复)。
|
|
2048
|
+
// 浅拷贝一份以隔离历史快照,避免后续 set/deleteInvalidNode 反向污染 step.extra。
|
|
2049
|
+
const invalidToRestore =
|
|
2050
|
+
(reverse ? step.extra?.invalidNodeIdsBefore : step.extra?.invalidNodeIdsAfter) ?? new Map();
|
|
2051
|
+
this.set('invalidNodeIds', new Map(invalidToRestore));
|
|
2052
|
+
this.emit('invalid-node-change', this.get('invalidNodeIds'));
|
|
2053
|
+
|
|
1807
2054
|
const page = toRaw(this.get('page'));
|
|
1808
2055
|
if (page) {
|
|
1809
2056
|
const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];
|
|
@@ -1826,6 +2073,22 @@ class Editor extends BaseService {
|
|
|
1826
2073
|
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|
|
1827
2074
|
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
|
|
1828
2075
|
}
|
|
2076
|
+
|
|
2077
|
+
/**
|
|
2078
|
+
* 撤销 remove / 重做 add 通过手工 splice 按 step 记录的 parentId / index 精确还原节点(不走 this.add,原因见调用处注释),
|
|
2079
|
+
* 这里补齐与 this.add 等价的 add / change 事件,保证撤销/重做路径的事件行为与正向操作一致。
|
|
2080
|
+
*/
|
|
2081
|
+
private emitHistoryInsertEvents(nodes: MNode[], source?: HistoryOpSource) {
|
|
2082
|
+
if (!nodes.length) return;
|
|
2083
|
+
this.emit('add', nodes);
|
|
2084
|
+
this.emit('change', {
|
|
2085
|
+
type: 'add',
|
|
2086
|
+
data: nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
2087
|
+
historySource: source,
|
|
2088
|
+
// 撤销/重做本身不再写入历史记录
|
|
2089
|
+
doNotPushHistory: true,
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
1829
2092
|
}
|
|
1830
2093
|
|
|
1831
2094
|
export type EditorService = Editor;
|