@tmagic/editor 1.8.0-beta.2 → 1.8.0-beta.20
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 +43 -9
- package/dist/es/components/CodeBlockEditor.vue_vue_type_script_setup_true_lang.js +8 -4
- package/dist/es/components/CodeParams.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/components/CompareForm.vue_vue_type_script_setup_true_lang.js +22 -98
- package/dist/es/components/ContentMenu.vue_vue_type_script_setup_true_lang.js +32 -9
- package/dist/es/components/FloatingBox.vue_vue_type_script_setup_true_lang.js +68 -17
- 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 +1 -1
- package/dist/es/components/ScrollBar.js +0 -1
- package/dist/es/components/ScrollBar.vue_vue_type_script_setup_true_lang.js +1 -1
- 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 +7 -7
- 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 +58 -0
- package/dist/es/editorProps.js +9 -1
- package/dist/es/fields/Code.vue_vue_type_script_setup_true_lang.js +16 -5
- package/dist/es/fields/CodeLink.vue_vue_type_script_setup_true_lang.js +7 -7
- package/dist/es/fields/CodeSelect.vue_vue_type_script_setup_true_lang.js +81 -44
- package/dist/es/fields/CodeSelectCol.vue_vue_type_script_setup_true_lang.js +10 -3
- 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 +13 -6
- package/dist/es/fields/DataSourceFieldSelect/Index.vue_vue_type_script_setup_true_lang.js +11 -8
- package/dist/es/fields/DataSourceFields.vue_vue_type_script_setup_true_lang.js +39 -12
- package/dist/es/fields/DataSourceInput.vue_vue_type_script_setup_true_lang.js +11 -20
- package/dist/es/fields/DataSourceMethodSelect.vue_vue_type_script_setup_true_name_true_lang.js +11 -6
- package/dist/es/fields/DataSourceMethods.vue_vue_type_script_setup_true_lang.js +30 -14
- package/dist/es/fields/DataSourceMocks.vue_vue_type_script_setup_true_lang.js +11 -4
- package/dist/es/fields/DataSourceSelect.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/DisplayConds.vue_vue_type_script_setup_true_lang.js +38 -6
- package/dist/es/fields/EventSelect.vue_vue_type_script_setup_true_lang.js +151 -153
- package/dist/es/fields/KeyValue.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/PageFragmentSelect.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +8 -10
- package/dist/es/fields/StyleSetter/components/BackgroundPosition.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/fields/StyleSetter/components/Border.vue_vue_type_script_setup_true_lang.js +28 -6
- package/dist/es/fields/StyleSetter/components/Box.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/fields/StyleSetter/icons/align-items/Center.js +31 -0
- package/dist/es/fields/StyleSetter/icons/align-items/FlexEnd.js +31 -0
- package/dist/es/fields/StyleSetter/icons/align-items/FlexStart.js +31 -0
- package/dist/es/fields/StyleSetter/icons/align-items/SpaceAround.js +50 -0
- package/dist/es/fields/StyleSetter/icons/align-items/SpaceBetween.js +50 -0
- package/dist/es/fields/StyleSetter/icons/background-repeat/NoRepeat.js +7 -7
- package/dist/es/fields/StyleSetter/icons/background-repeat/Repeat.js +6 -3
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatX.js +29 -6
- package/dist/es/fields/StyleSetter/icons/background-repeat/RepeatY.js +32 -6
- 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 +11 -3
- package/dist/es/fields/StyleSetter/icons/flex-direction/ColumnReverse.js +11 -3
- package/dist/es/fields/StyleSetter/icons/flex-direction/Row.js +11 -3
- package/dist/es/fields/StyleSetter/icons/flex-direction/RowReverse.js +11 -3
- package/dist/es/fields/StyleSetter/icons/justify-content/Center.js +17 -3
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexEnd.js +19 -3
- package/dist/es/fields/StyleSetter/icons/justify-content/FlexStart.js +19 -3
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceAround.js +37 -3
- package/dist/es/fields/StyleSetter/icons/justify-content/SpaceBetween.js +33 -3
- 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 +56 -30
- package/dist/es/fields/StyleSetter/pro/Border.vue_vue_type_script_setup_true_lang.js +7 -4
- package/dist/es/fields/StyleSetter/pro/Font.vue_vue_type_script_setup_true_lang.js +35 -26
- package/dist/es/fields/StyleSetter/pro/Layout.vue_vue_type_script_setup_true_lang.js +76 -48
- package/dist/es/fields/StyleSetter/pro/Position.vue_vue_type_script_setup_true_lang.js +29 -23
- package/dist/es/fields/StyleSetter/pro/Transform.vue_vue_type_script_setup_true_lang.js +7 -4
- package/dist/es/fields/UISelect.js +0 -1
- package/dist/es/fields/UISelect.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/hooks/use-code-block-edit.js +6 -3
- package/dist/es/hooks/use-compare-form.js +119 -0
- package/dist/es/hooks/use-data-source-edit.js +5 -2
- package/dist/es/hooks/use-stage.js +9 -4
- 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 +17 -0
- package/dist/es/icons/FolderMinusIcon.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/index.js +24 -10
- package/dist/es/initService.js +19 -13
- 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 +24 -12
- package/dist/es/layouts/Framework.vue_vue_type_script_setup_true_lang.js +25 -18
- package/dist/es/layouts/NavMenu.vue_vue_type_script_setup_true_lang.js +4 -4
- 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 +25 -38
- package/dist/es/layouts/history-list/BucketTab.js +5 -0
- package/dist/es/layouts/history-list/BucketTab.vue_vue_type_script_setup_true_lang.js +62 -0
- package/dist/es/layouts/history-list/GroupRow.vue_vue_type_script_setup_true_lang.js +177 -88
- package/dist/es/layouts/history-list/HistoryDiffDialog.vue_vue_type_script_setup_true_lang.js +139 -37
- package/dist/es/layouts/history-list/HistoryListPanel.vue_vue_type_script_setup_true_lang.js +164 -107
- package/dist/es/layouts/history-list/InitialRow.vue_vue_type_script_setup_true_lang.js +44 -9
- package/dist/es/layouts/history-list/PageTab.vue_vue_type_script_setup_true_lang.js +49 -53
- package/dist/es/layouts/history-list/composables.js +138 -108
- package/dist/es/layouts/history-list/useHistoryList.js +56 -0
- package/dist/es/layouts/history-list/useHistoryRevert.js +317 -0
- 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 +5 -3
- 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 -7
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +33 -13
- 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 +26 -38
- package/dist/es/layouts/sidebar/code-block/CodeBlockList.vue_vue_type_script_setup_true_lang.js +4 -4
- 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 +23 -6
- 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 +27 -5
- 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 +8 -8
- 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 +2 -2
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +3 -2
- package/dist/es/layouts/sidebar/layer/use-click.js +3 -3
- 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 +2 -2
- 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 +16 -16
- package/dist/es/plugin.js +5 -2
- package/dist/es/services/codeBlock.js +137 -56
- package/dist/es/services/dataSource.js +122 -52
- package/dist/es/services/dep.js +162 -41
- package/dist/es/services/editor.js +613 -179
- package/dist/es/services/history.js +350 -386
- package/dist/es/services/keybinding.js +6 -6
- package/dist/es/services/props.js +3 -2
- package/dist/es/services/storage.js +2 -2
- package/dist/es/services/ui.js +10 -2
- package/dist/es/style.css +549 -113
- package/dist/es/utils/config.js +4 -1
- package/dist/es/utils/const.js +2 -2
- package/dist/es/utils/content-menu.js +19 -11
- package/dist/es/utils/data-source/index.js +34 -11
- package/dist/es/utils/dep/collect-worker-client.js +114 -0
- package/dist/es/utils/dep/idle-task.js +47 -22
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/editor.js +103 -49
- package/dist/es/utils/event.js +123 -0
- package/dist/es/utils/history.js +220 -0
- package/dist/es/utils/indexed-db.js +86 -0
- package/dist/es/utils/monaco-editor.js +22 -6
- package/dist/es/utils/operator.js +2 -2
- package/dist/es/utils/props.js +114 -33
- package/dist/es/utils/type-match-rules.js +367 -0
- package/dist/es/utils/undo-redo.js +60 -1
- package/dist/style.css +549 -113
- package/dist/themes/magic-admin.css +3028 -0
- package/dist/tmagic-editor.umd.cjs +14273 -10029
- package/package.json +8 -8
- package/src/Editor.vue +43 -2
- package/src/components/CodeBlockEditor.vue +3 -0
- package/src/components/CompareForm.vue +18 -146
- package/src/components/ContentMenu.vue +44 -8
- package/src/components/FloatingBox.vue +84 -9
- package/src/components/ScrollBar.vue +0 -67
- package/src/components/ToolButton.vue +2 -2
- package/src/components/ViewForm.vue +55 -0
- package/src/editorProps.ts +30 -0
- package/src/fields/Code.vue +10 -2
- package/src/fields/CodeLink.vue +2 -5
- package/src/fields/CodeSelect.vue +54 -40
- package/src/fields/CodeSelectCol.vue +8 -1
- package/src/fields/CondOpSelect.vue +2 -24
- package/src/fields/DataSourceFieldSelect/FieldSelect.vue +16 -2
- package/src/fields/DataSourceFieldSelect/Index.vue +14 -5
- package/src/fields/DataSourceFields.vue +42 -8
- package/src/fields/DataSourceInput.vue +10 -28
- package/src/fields/DataSourceMethodSelect.vue +9 -4
- package/src/fields/DataSourceMethods.vue +42 -21
- package/src/fields/DataSourceMocks.vue +3 -0
- package/src/fields/DisplayConds.vue +18 -1
- package/src/fields/EventSelect.vue +145 -161
- package/src/fields/StyleSetter/Index.vue +3 -12
- package/src/fields/StyleSetter/components/Border.vue +15 -6
- package/src/fields/StyleSetter/icons/align-items/Center.vue +19 -0
- package/src/fields/StyleSetter/icons/align-items/FlexEnd.vue +19 -0
- package/src/fields/StyleSetter/icons/align-items/FlexStart.vue +19 -0
- package/src/fields/StyleSetter/icons/align-items/SpaceAround.vue +34 -0
- package/src/fields/StyleSetter/icons/align-items/SpaceBetween.vue +34 -0
- package/src/fields/StyleSetter/icons/align-items/index.ts +5 -0
- package/src/fields/StyleSetter/icons/background-repeat/NoRepeat.vue +20 -4
- package/src/fields/StyleSetter/icons/background-repeat/Repeat.vue +10 -28
- package/src/fields/StyleSetter/icons/background-repeat/RepeatX.vue +4 -5
- package/src/fields/StyleSetter/icons/background-repeat/RepeatY.vue +4 -5
- package/src/fields/StyleSetter/icons/flex-direction/Column.vue +4 -3
- package/src/fields/StyleSetter/icons/flex-direction/ColumnReverse.vue +4 -3
- package/src/fields/StyleSetter/icons/flex-direction/Row.vue +4 -3
- package/src/fields/StyleSetter/icons/flex-direction/RowReverse.vue +4 -3
- package/src/fields/StyleSetter/icons/justify-content/Center.vue +5 -2
- package/src/fields/StyleSetter/icons/justify-content/FlexEnd.vue +15 -2
- package/src/fields/StyleSetter/icons/justify-content/FlexStart.vue +15 -2
- package/src/fields/StyleSetter/icons/justify-content/SpaceAround.vue +28 -3
- package/src/fields/StyleSetter/icons/justify-content/SpaceBetween.vue +28 -2
- package/src/fields/StyleSetter/pro/Background.vue +99 -55
- package/src/fields/StyleSetter/pro/Border.vue +9 -11
- package/src/fields/StyleSetter/pro/Font.vue +66 -64
- package/src/fields/StyleSetter/pro/Layout.vue +211 -139
- package/src/fields/StyleSetter/pro/Position.vue +69 -67
- package/src/fields/StyleSetter/pro/Transform.vue +23 -25
- package/src/fields/UISelect.vue +0 -12
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-code-block-edit.ts +4 -3
- package/src/hooks/use-compare-form.ts +187 -0
- package/src/hooks/use-data-source-edit.ts +2 -2
- package/src/hooks/use-stage.ts +5 -3
- package/src/icons/DatasourceIcon.vue +7 -0
- package/src/index.ts +12 -0
- package/src/initService.ts +26 -16
- package/src/layouts/CodeEditor.vue +34 -11
- package/src/layouts/Framework.vue +15 -8
- package/src/layouts/NavMenu.vue +3 -3
- package/src/layouts/history-list/Bucket.vue +33 -41
- package/src/layouts/history-list/BucketTab.vue +72 -0
- package/src/layouts/history-list/GroupRow.vue +190 -93
- package/src/layouts/history-list/HistoryDiffDialog.vue +169 -90
- package/src/layouts/history-list/HistoryListPanel.vue +191 -95
- package/src/layouts/history-list/InitialRow.vue +38 -8
- package/src/layouts/history-list/PageTab.vue +67 -43
- package/src/layouts/history-list/composables.ts +239 -125
- package/src/layouts/history-list/useHistoryList.ts +60 -0
- package/src/layouts/history-list/useHistoryRevert.ts +425 -0
- package/src/layouts/page-bar/PageBar.vue +4 -2
- package/src/layouts/props-panel/FormPanel.vue +72 -6
- package/src/layouts/props-panel/PropsPanel.vue +62 -18
- package/src/layouts/props-panel/use-style-panel.ts +4 -3
- package/src/layouts/sidebar/ComponentListPanel.vue +10 -6
- package/src/layouts/sidebar/Sidebar.vue +13 -6
- package/src/layouts/sidebar/code-block/CodeBlockList.vue +5 -5
- 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 +34 -2
- package/src/layouts/sidebar/data-source/DataSourceListPanel.vue +29 -4
- package/src/layouts/sidebar/data-source/useContentMenu.ts +14 -2
- package/src/layouts/sidebar/layer/LayerMenu.vue +21 -13
- package/src/layouts/sidebar/layer/LayerNodeContent.vue +57 -0
- package/src/layouts/sidebar/layer/LayerNodeTool.vue +7 -4
- package/src/layouts/sidebar/layer/LayerPanel.vue +4 -1
- 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 +1 -1
- package/src/layouts/workspace/viewer/ViewerMenu.vue +15 -15
- package/src/plugin.ts +6 -3
- package/src/services/codeBlock.ts +152 -47
- package/src/services/dataSource.ts +120 -41
- package/src/services/dep.ts +292 -47
- package/src/services/editor.ts +820 -224
- package/src/services/history.ts +449 -412
- package/src/services/keybinding.ts +6 -6
- package/src/services/ui.ts +33 -5
- package/src/theme/code-block.scss +37 -0
- package/src/theme/common/var.scss +1 -1
- package/src/theme/component-list-panel.scss +2 -2
- package/src/theme/data-source-field.scss +4 -0
- package/src/theme/display-conds.scss +11 -0
- package/src/theme/event.scss +70 -1
- package/src/theme/floating-box.scss +9 -1
- package/src/theme/framework.scss +4 -1
- package/src/theme/history-list-panel.scss +157 -23
- package/src/theme/layer-node-content.scss +14 -0
- package/src/theme/nav-menu.scss +5 -1
- package/src/theme/page-bar.scss +0 -4
- package/src/theme/props-panel.scss +5 -4
- package/src/theme/scroll-bar.scss +64 -0
- package/src/theme/style-setter/border.scss +17 -5
- package/src/theme/theme.scss +4 -0
- package/src/theme/themes/magic-admin/index.scss +214 -0
- package/src/theme/ui-select.scss +9 -0
- package/src/type.ts +668 -124
- package/src/utils/config.ts +9 -1
- package/src/utils/const.ts +3 -2
- package/src/utils/content-menu.ts +20 -11
- package/src/utils/data-source/index.ts +32 -12
- package/src/utils/dep/collect-worker-client.ts +149 -0
- package/src/utils/dep/idle-task.ts +72 -35
- package/src/utils/dep/worker.ts +113 -9
- package/src/utils/editor.ts +166 -60
- package/src/utils/event.ts +224 -0
- package/src/utils/history.ts +286 -0
- package/src/utils/index.ts +4 -0
- package/src/utils/indexed-db.ts +122 -0
- package/src/utils/monaco-editor.ts +27 -0
- package/src/utils/operator.ts +2 -2
- package/src/utils/props.ts +114 -11
- package/src/utils/type-match-rules.ts +684 -0
- package/src/utils/undo-redo.ts +88 -0
- package/types/index.d.ts +1979 -515
- package/dist/es/layouts/history-list/CodeBlockTab.js +0 -5
- package/dist/es/layouts/history-list/CodeBlockTab.vue_vue_type_script_setup_true_lang.js +0 -62
- package/dist/es/layouts/history-list/DataSourceTab.js +0 -5
- package/dist/es/layouts/history-list/DataSourceTab.vue_vue_type_script_setup_true_lang.js +0 -62
- package/src/layouts/history-list/CodeBlockTab.vue +0 -61
- package/src/layouts/history-list/DataSourceTab.vue +0 -61
package/src/services/editor.ts
CHANGED
|
@@ -16,14 +16,24 @@
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { reactive, toRaw } from 'vue';
|
|
20
|
-
import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
|
|
19
|
+
import { nextTick, reactive, toRaw } from 'vue';
|
|
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';
|
|
23
23
|
import { NodeType } from '@tmagic/core';
|
|
24
24
|
import type { ChangeRecord } from '@tmagic/form';
|
|
25
25
|
import { isFixed } from '@tmagic/stage';
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
getNodeInfo,
|
|
28
|
+
getNodePath,
|
|
29
|
+
getValueByKeyPath,
|
|
30
|
+
guid,
|
|
31
|
+
isPage,
|
|
32
|
+
isPageFragment,
|
|
33
|
+
isPageOrFragment,
|
|
34
|
+
setValueByKeyPath,
|
|
35
|
+
traverseNode,
|
|
36
|
+
} from '@tmagic/utils';
|
|
27
37
|
|
|
28
38
|
import BaseService from '@editor/services//BaseService';
|
|
29
39
|
import propsService from '@editor/services//props';
|
|
@@ -34,10 +44,15 @@ import type {
|
|
|
34
44
|
AsyncHookPlugin,
|
|
35
45
|
AsyncMethodName,
|
|
36
46
|
DslOpOptions,
|
|
47
|
+
DslOpWithHistoryIdsResult,
|
|
37
48
|
EditorEvents,
|
|
38
49
|
EditorNodeInfo,
|
|
50
|
+
HistoryOpSource,
|
|
39
51
|
HistoryOpType,
|
|
52
|
+
NodeInvalidInfo,
|
|
53
|
+
NodeInvalidSource,
|
|
40
54
|
PastePosition,
|
|
55
|
+
StepDiffItem,
|
|
41
56
|
StepValue,
|
|
42
57
|
StoreState,
|
|
43
58
|
StoreStateKey,
|
|
@@ -50,6 +65,7 @@ import {
|
|
|
50
65
|
classifyDragSources,
|
|
51
66
|
collectRelatedNodes,
|
|
52
67
|
COPY_STORAGE_KEY,
|
|
68
|
+
describeStepForRevert,
|
|
53
69
|
editorNodeMergeCustomizer,
|
|
54
70
|
fixNodePosition,
|
|
55
71
|
getInitPositionStyle,
|
|
@@ -62,41 +78,24 @@ import {
|
|
|
62
78
|
setLayout,
|
|
63
79
|
toggleFixedPosition,
|
|
64
80
|
} from '@editor/utils/editor';
|
|
81
|
+
import { getLastPushedHistoryIds } from '@editor/utils/history';
|
|
65
82
|
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
|
66
83
|
|
|
67
84
|
type MoveItem = { node: MNode; parent: MContainer; pageForOp: { name: string; id: Id } | null };
|
|
68
85
|
|
|
69
86
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
87
|
+
* 把「变更前后节点快照」列表归一成 update 类型的 {@link StepDiffItem} 列表,供 {@link StepValue.diff} 使用。
|
|
88
|
+
* `changeRecords` 来自 form 端的 propPath/value 列表,撤销/重做时只对这些 propPath 做局部更新;
|
|
89
|
+
* 缺省(未传 / 空数组)才退化为整节点替换。
|
|
72
90
|
*/
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
case 'remove': {
|
|
82
|
-
const count = step.removedItems?.length ?? 0;
|
|
83
|
-
const node = step.removedItems?.[0]?.node;
|
|
84
|
-
const label = node?.name || node?.type || (node?.id !== undefined ? `${node.id}` : '');
|
|
85
|
-
return `还原已删除的 ${count} 个节点${count === 1 && label ? `(${label})` : ''}`;
|
|
86
|
-
}
|
|
87
|
-
case 'update':
|
|
88
|
-
default: {
|
|
89
|
-
const items = step.updatedItems ?? [];
|
|
90
|
-
if (items.length === 1) {
|
|
91
|
-
const { newNode, oldNode, changeRecords } = items[0];
|
|
92
|
-
const target = newNode?.name || newNode?.type || oldNode?.name || oldNode?.type || `${newNode?.id ?? ''}`;
|
|
93
|
-
const propPath = changeRecords?.[0]?.propPath;
|
|
94
|
-
return propPath ? `还原 ${target} · ${propPath}` : `还原 ${target}`;
|
|
95
|
-
}
|
|
96
|
-
return `还原 ${items.length} 个节点的修改`;
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
};
|
|
91
|
+
const buildUpdateDiff = (
|
|
92
|
+
items: { oldNode: MNode; newNode: MNode; changeRecords?: ChangeRecord[] }[],
|
|
93
|
+
): StepDiffItem<MNode>[] =>
|
|
94
|
+
items.map(({ oldNode, newNode, changeRecords }) => ({
|
|
95
|
+
oldSchema: oldNode,
|
|
96
|
+
newSchema: newNode,
|
|
97
|
+
...(changeRecords?.length ? { changeRecords } : {}),
|
|
98
|
+
}));
|
|
100
99
|
|
|
101
100
|
class Editor extends BaseService {
|
|
102
101
|
public state: StoreState = reactive({
|
|
@@ -109,12 +108,24 @@ class Editor extends BaseService {
|
|
|
109
108
|
stageLoading: true,
|
|
110
109
|
highlightNode: null,
|
|
111
110
|
modifiedNodeIds: new Map(),
|
|
111
|
+
invalidNodeIds: new Map(),
|
|
112
112
|
pageLength: 0,
|
|
113
113
|
pageFragmentLength: 0,
|
|
114
114
|
disabledMultiSelect: false,
|
|
115
115
|
alwaysMultiSelect: false,
|
|
116
116
|
});
|
|
117
117
|
private selectionBeforeOp: Id[] | null = null;
|
|
118
|
+
/**
|
|
119
|
+
* 操作前的节点校验错误快照,与 selectionBeforeOp 同时在 captureSelectionBeforeOp 中捕获,
|
|
120
|
+
* 供 pushOpHistory 写入 step.extra.invalidNodeIdsBefore,用于撤销时还原到「操作前」的错误状态。
|
|
121
|
+
*/
|
|
122
|
+
private invalidNodeIdsBeforeOp: Map<Id, NodeInvalidInfo> | null = null;
|
|
123
|
+
/**
|
|
124
|
+
* 最近一次 pushOpHistory 写入的历史记录 uuid。
|
|
125
|
+
* 供 *AndGetHistoryId 系列方法在调用普通操作后取回本次产生的历史记录 id;
|
|
126
|
+
* 普通操作不会读取它,调用前由 *AndGetHistoryId 重置为 null。
|
|
127
|
+
*/
|
|
128
|
+
private lastPushedHistoryId: string | null = null;
|
|
118
129
|
|
|
119
130
|
constructor() {
|
|
120
131
|
super(
|
|
@@ -126,10 +137,15 @@ class Editor extends BaseService {
|
|
|
126
137
|
|
|
127
138
|
/**
|
|
128
139
|
* 设置当前指点节点配置
|
|
129
|
-
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
140
|
+
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength
|
|
130
141
|
* @param value MNode
|
|
142
|
+
* @param options.historySource 设置 root 时,本次变更写入历史记录的「操作来源」(仅 name === 'root' 时生效)
|
|
131
143
|
*/
|
|
132
|
-
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
144
|
+
public set<K extends StoreStateKey, T extends StoreState[K]>(
|
|
145
|
+
name: K,
|
|
146
|
+
value: T,
|
|
147
|
+
options: { historySource?: HistoryOpSource } = {},
|
|
148
|
+
) {
|
|
133
149
|
const preValue = this.state[name];
|
|
134
150
|
this.state[name] = value;
|
|
135
151
|
|
|
@@ -148,19 +164,41 @@ class Editor extends BaseService {
|
|
|
148
164
|
this.state.pageLength = getPageList(app).length || 0;
|
|
149
165
|
this.state.pageFragmentLength = getPageFragmentList(app).length || 0;
|
|
150
166
|
this.state.stageLoading = this.state.pageLength !== 0;
|
|
167
|
+
|
|
168
|
+
if (preValue && !isEmpty(preValue)) {
|
|
169
|
+
// 编辑期间再次整体设置 root(源码保存 / 外部重设 DSL / root 节点更新):与上一次 root
|
|
170
|
+
// 做页面级 diff,按 update / add / remove 入栈,作为正常历史记录体现整体替换。
|
|
171
|
+
this.pushRootDiffHistory(preValue as MApp, app, options.historySource);
|
|
172
|
+
} else {
|
|
173
|
+
// 首次设置 root:仅当该页面 / 页面片尚无基线标记时,才写入「未修改的初始状态」基线。
|
|
174
|
+
// 配合「先恢复历史再 set root」:若基线已随历史恢复建立(恢复后已有基线),则此处不再
|
|
175
|
+
// 重复创建,set root 不额外产生记录,由恢复出的历史栈作为当前状态来源。
|
|
176
|
+
// 标记不进入撤销/重做栈,仅作为该页历史列表底部的初始基线展示。
|
|
177
|
+
app.items?.forEach((pageNode) => {
|
|
178
|
+
if (pageNode?.id !== undefined && !historyService.getMarker('page', pageNode.id)) {
|
|
179
|
+
historyService.setMarker('page', pageNode.id, {
|
|
180
|
+
name: pageNode.name,
|
|
181
|
+
source: options.historySource,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
151
186
|
} else {
|
|
152
187
|
this.state.pageLength = 0;
|
|
153
188
|
this.state.pageFragmentLength = 0;
|
|
154
189
|
this.state.stageLoading = false;
|
|
155
190
|
}
|
|
156
191
|
|
|
157
|
-
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root']);
|
|
192
|
+
this.emit('root-change', value as StoreState['root'], preValue as StoreState['root'], options);
|
|
193
|
+
|
|
194
|
+
// 整体替换 DSL 后,清理不再存在于新 DSL 中的失效节点错误记录,避免残留误报。
|
|
195
|
+
this.pruneInvalidNodeIds();
|
|
158
196
|
}
|
|
159
197
|
}
|
|
160
198
|
|
|
161
199
|
/**
|
|
162
200
|
* 获取当前指点节点配置
|
|
163
|
-
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'pageLength' | 'pageFragmentLength'
|
|
201
|
+
* @param name 'root' | 'page' | 'parent' | 'node' | 'highlightNode' | 'nodes' | 'stage' | 'modifiedNodeIds' | 'invalidNodeIds' | 'pageLength' | 'pageFragmentLength'
|
|
164
202
|
* @returns MNode
|
|
165
203
|
*/
|
|
166
204
|
public get<K extends StoreStateKey>(name: K): StoreState[K] {
|
|
@@ -179,7 +217,30 @@ class Editor extends BaseService {
|
|
|
179
217
|
root = toRaw(root);
|
|
180
218
|
}
|
|
181
219
|
|
|
182
|
-
|
|
220
|
+
if (!root) {
|
|
221
|
+
return { node: null, parent: null, page: null, path: [] };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (id === root.id) {
|
|
225
|
+
return { node: root, parent: null, page: null, path: [] };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 大多数查找的目标都在当前页面内,优先在当前页面子树中查找以避免对整棵树做全量遍历。
|
|
229
|
+
// 注意:不能直接使用 state.page,它可能与当前 root 不同步(指向已脱离的旧页面对象),
|
|
230
|
+
// 因此仅借用其 id,再从当前 root 中取回真正的页面对象(页面均为 root 的直接子节点,数量很少)。
|
|
231
|
+
const pageIdStr = `${this.get('page')?.id || ''}`;
|
|
232
|
+
const currentPageNode = root.items?.find((item) => `${item.id}` === pageIdStr);
|
|
233
|
+
if (currentPageNode && `${id}` !== pageIdStr) {
|
|
234
|
+
// util 仅读取 root.id 与 root.items,按容器结构传入当前页面是安全的
|
|
235
|
+
const info = getNodeInfo(id, currentPageNode);
|
|
236
|
+
if (info.node) {
|
|
237
|
+
return info;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 回退:在完整 root 上查找;当前页面已搜索过,用 skip 跳过其子树避免重复遍历,
|
|
242
|
+
// 同时保留真实的 parent / page 引用(id 命中当前页面节点本身时会在跳过子树前先匹配到)
|
|
243
|
+
return getNodeInfo(id, root, currentPageNode);
|
|
183
244
|
}
|
|
184
245
|
|
|
185
246
|
/**
|
|
@@ -212,7 +273,7 @@ class Editor extends BaseService {
|
|
|
212
273
|
public isOnDifferentPage(node: MNode): boolean {
|
|
213
274
|
const currentPageId = this.get('page')?.id;
|
|
214
275
|
if (currentPageId === undefined || currentPageId === null) return false;
|
|
215
|
-
if (
|
|
276
|
+
if (isPageOrFragment(node)) {
|
|
216
277
|
return `${node.id}` !== `${currentPageId}`;
|
|
217
278
|
}
|
|
218
279
|
const nodePage = this.getNodeInfo(node.id, false).page;
|
|
@@ -249,12 +310,6 @@ class Editor extends BaseService {
|
|
|
249
310
|
this.set('page', page);
|
|
250
311
|
this.set('parent', parent);
|
|
251
312
|
|
|
252
|
-
if (page) {
|
|
253
|
-
historyService.changePage(toRaw(page));
|
|
254
|
-
} else {
|
|
255
|
-
historyService.resetState();
|
|
256
|
-
}
|
|
257
|
-
|
|
258
313
|
if (node?.id) {
|
|
259
314
|
this.get('stage')
|
|
260
315
|
?.renderer?.runtime?.getApp?.()
|
|
@@ -359,11 +414,11 @@ class Editor extends BaseService {
|
|
|
359
414
|
|
|
360
415
|
if (!curNode) throw new Error('当前选中节点为空');
|
|
361
416
|
|
|
362
|
-
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !(
|
|
417
|
+
if ((parent.type === NodeType.ROOT || curNode?.type === NodeType.ROOT) && !isPageOrFragment(node)) {
|
|
363
418
|
throw new Error('app下不能添加组件');
|
|
364
419
|
}
|
|
365
420
|
|
|
366
|
-
if (parent.id !== curNode.id && !(
|
|
421
|
+
if (parent.id !== curNode.id && !isPageOrFragment(node)) {
|
|
367
422
|
const index = parent.items.indexOf(curNode);
|
|
368
423
|
parent.items?.splice(index + 1, 0, node);
|
|
369
424
|
} else {
|
|
@@ -406,7 +461,13 @@ class Editor extends BaseService {
|
|
|
406
461
|
public async add(
|
|
407
462
|
addNode: AddMNode | MNode[],
|
|
408
463
|
parent?: MContainer | null,
|
|
409
|
-
{
|
|
464
|
+
{
|
|
465
|
+
doNotSelect = false,
|
|
466
|
+
doNotSwitchPage = false,
|
|
467
|
+
doNotPushHistory = false,
|
|
468
|
+
historyDescription,
|
|
469
|
+
historySource,
|
|
470
|
+
}: DslOpOptions = {},
|
|
410
471
|
): Promise<MNode | MNode[]> {
|
|
411
472
|
this.captureSelectionBeforeOp();
|
|
412
473
|
|
|
@@ -427,7 +488,7 @@ class Editor extends BaseService {
|
|
|
427
488
|
const newNodes = await Promise.all(
|
|
428
489
|
addNodes.map((node) => {
|
|
429
490
|
const root = this.get('root');
|
|
430
|
-
if ((
|
|
491
|
+
if (isPageOrFragment(node) && root) {
|
|
431
492
|
return this.doAdd(node, root);
|
|
432
493
|
}
|
|
433
494
|
const parentNode = parent ?? getAddParent(node);
|
|
@@ -463,30 +524,42 @@ class Editor extends BaseService {
|
|
|
463
524
|
}
|
|
464
525
|
}
|
|
465
526
|
|
|
466
|
-
if (!(
|
|
527
|
+
if (!isPageOrFragment(newNodes[0])) {
|
|
467
528
|
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
|
468
529
|
if (!doNotPushHistory) {
|
|
469
|
-
this.
|
|
470
|
-
|
|
471
|
-
{
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
},
|
|
481
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
530
|
+
const parentId = (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id;
|
|
531
|
+
this.pushOpHistory('add', {
|
|
532
|
+
diff: newNodes.map((n) => {
|
|
533
|
+
const p = this.getParentById(n.id, false) as MContainer;
|
|
534
|
+
const idx = p ? getNodeIndex(n.id, p) : -1;
|
|
535
|
+
return {
|
|
536
|
+
newSchema: cloneDeep(toRaw(n)),
|
|
537
|
+
parentId,
|
|
538
|
+
index: typeof idx === 'number' ? idx : -1,
|
|
539
|
+
};
|
|
540
|
+
}),
|
|
541
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
482
542
|
historyDescription,
|
|
483
|
-
|
|
543
|
+
source: historySource,
|
|
544
|
+
});
|
|
484
545
|
} else {
|
|
485
546
|
this.selectionBeforeOp = null;
|
|
486
547
|
}
|
|
487
548
|
}
|
|
488
549
|
|
|
489
550
|
this.emit('add', newNodes);
|
|
551
|
+
this.emit('change', {
|
|
552
|
+
type: 'add',
|
|
553
|
+
data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
554
|
+
historySource,
|
|
555
|
+
doNotPushHistory,
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// 页面 / 页面片新增不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
|
559
|
+
const addedPages = newNodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
|
560
|
+
if (addedPages.length) {
|
|
561
|
+
historyService.notifyPageStructureChange({ add: addedPages, remove: [] });
|
|
562
|
+
}
|
|
490
563
|
|
|
491
564
|
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
|
492
565
|
}
|
|
@@ -506,12 +579,14 @@ class Editor extends BaseService {
|
|
|
506
579
|
|
|
507
580
|
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
|
|
508
581
|
|
|
509
|
-
parent.items?.splice(index, 1);
|
|
510
582
|
const stage = this.get('stage');
|
|
511
|
-
|
|
583
|
+
const currentPage = this.get('page');
|
|
584
|
+
const isDeletingCurrentPage = isPageOrFragment(node) && !!currentPage && `${currentPage.id}` === `${node.id}`;
|
|
585
|
+
|
|
586
|
+
parent.items?.splice(index, 1);
|
|
512
587
|
|
|
513
588
|
// 始终清理已删除节点在 state 中的残留引用:
|
|
514
|
-
// - 即使后续会调用
|
|
589
|
+
// - 即使后续会调用 select 覆盖,跳过这些调用(doNotSelect / doNotSwitchPage)时也不能让 state 持有已删除节点
|
|
515
590
|
const selectedNodes = this.get('nodes');
|
|
516
591
|
const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`);
|
|
517
592
|
if (removedSelectedIndex !== -1) {
|
|
@@ -519,40 +594,44 @@ class Editor extends BaseService {
|
|
|
519
594
|
nextSelected.splice(removedSelectedIndex, 1);
|
|
520
595
|
this.set('nodes', nextSelected);
|
|
521
596
|
}
|
|
522
|
-
if (isPage(node) || isPageFragment(node)) {
|
|
523
|
-
const currentPage = this.get('page');
|
|
524
|
-
if (currentPage && `${currentPage.id}` === `${node.id}`) {
|
|
525
|
-
this.set('page', null);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
const selectDefault = async (pages: MNode[]) => {
|
|
530
|
-
if (pages[0]) {
|
|
531
|
-
await this.select(pages[0]);
|
|
532
|
-
stage?.select(pages[0].id);
|
|
533
|
-
} else {
|
|
534
|
-
this.selectRoot();
|
|
535
|
-
|
|
536
|
-
historyService.resetPage();
|
|
537
|
-
}
|
|
538
|
-
};
|
|
539
597
|
|
|
598
|
+
const removeData = { id: node.id, parentId: parent.id, root: cloneDeep(root) };
|
|
540
599
|
const rootItems = root.items || [];
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
600
|
+
// 删非当前页时画布不应该变动,所以只有删的是当前页才重新选中
|
|
601
|
+
const shouldReselect = isDeletingCurrentPage && !doNotSelect && !doNotSwitchPage;
|
|
602
|
+
// 还有剩余页面才能切过去,否则退回选中 root
|
|
603
|
+
const shouldSwitchPage = shouldReselect && rootItems.length > 0;
|
|
604
|
+
|
|
605
|
+
if (isPageOrFragment(node)) {
|
|
606
|
+
if (isPage(node)) {
|
|
607
|
+
this.state.pageLength -= 1;
|
|
608
|
+
} else {
|
|
609
|
+
this.state.pageFragmentLength -= 1;
|
|
548
610
|
}
|
|
549
|
-
} else if (isPageFragment(node)) {
|
|
550
|
-
this.state.pageFragmentLength -= 1;
|
|
551
611
|
|
|
552
|
-
if (
|
|
553
|
-
|
|
612
|
+
if (shouldSwitchPage) {
|
|
613
|
+
// runtime 收到页面删除时会销毁当前渲染的 page 实例,必须先把画布切到剩余页面再通知删除,
|
|
614
|
+
// 否则画布会被清空,且要等下一次 updatePageId 才能恢复。
|
|
615
|
+
// nextTick 用于等 Stage 的 page watch 把新页面 id 同步给 runtime。
|
|
616
|
+
await this.select(rootItems[0]);
|
|
617
|
+
stage?.select(rootItems[0].id);
|
|
618
|
+
await nextTick();
|
|
619
|
+
stage?.remove(removeData);
|
|
620
|
+
} else {
|
|
621
|
+
// page 置空会让 Workspace 卸载 Stage 并销毁 renderer,删除通知必须在卸载前发出,
|
|
622
|
+
// 且不能 await:runtime 未 ready 时 getRuntime 的监听会随 destroy 一起被移除,永远不会 resolve
|
|
623
|
+
stage?.remove(removeData);
|
|
624
|
+
|
|
625
|
+
if (shouldReselect) {
|
|
626
|
+
// 页面已全部删完
|
|
627
|
+
this.selectRoot();
|
|
628
|
+
} else if (isDeletingCurrentPage) {
|
|
629
|
+
this.set('page', null);
|
|
630
|
+
}
|
|
554
631
|
}
|
|
555
632
|
} else {
|
|
633
|
+
stage?.remove(removeData);
|
|
634
|
+
|
|
556
635
|
if (!doNotSelect) {
|
|
557
636
|
await this.select(parent);
|
|
558
637
|
stage?.select(parent.id);
|
|
@@ -577,15 +656,24 @@ class Editor extends BaseService {
|
|
|
577
656
|
*/
|
|
578
657
|
public async remove(
|
|
579
658
|
nodeOrNodeList: MNode | MNode[],
|
|
580
|
-
{
|
|
659
|
+
{
|
|
660
|
+
doNotSelect = false,
|
|
661
|
+
doNotSwitchPage = false,
|
|
662
|
+
doNotPushHistory = false,
|
|
663
|
+
historyDescription,
|
|
664
|
+
historySource,
|
|
665
|
+
}: DslOpOptions = {},
|
|
581
666
|
): Promise<void> {
|
|
582
667
|
this.captureSelectionBeforeOp();
|
|
583
668
|
|
|
584
669
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
|
585
670
|
|
|
586
|
-
|
|
671
|
+
// 删除后节点已从树中移除,无法再反查所属 page,这里在删除前先逐个捕获用于 `change` 事件
|
|
672
|
+
const changeItems = nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) }));
|
|
673
|
+
|
|
674
|
+
const removedItems: StepDiffItem<MNode>[] = [];
|
|
587
675
|
let pageForOp: { name: string; id: Id } | null = null;
|
|
588
|
-
if (!(
|
|
676
|
+
if (!isPageOrFragment(nodes[0])) {
|
|
589
677
|
for (const n of nodes) {
|
|
590
678
|
const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
|
|
591
679
|
if (parent && curNode) {
|
|
@@ -594,7 +682,7 @@ class Editor extends BaseService {
|
|
|
594
682
|
}
|
|
595
683
|
const idx = getNodeIndex(curNode.id, parent);
|
|
596
684
|
removedItems.push({
|
|
597
|
-
|
|
685
|
+
oldSchema: cloneDeep(toRaw(curNode)),
|
|
598
686
|
parentId: parent.id,
|
|
599
687
|
index: typeof idx === 'number' ? idx : -1,
|
|
600
688
|
});
|
|
@@ -604,20 +692,35 @@ class Editor extends BaseService {
|
|
|
604
692
|
|
|
605
693
|
await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect, doNotSwitchPage })));
|
|
606
694
|
|
|
695
|
+
// 删除节点时同步清理其(含子树)的校验错误记录;置于 pushOpHistory 之前,使历史快照与本次删除对齐。
|
|
696
|
+
this.removeInvalidNodesBySubtree(nodes);
|
|
697
|
+
|
|
607
698
|
if (removedItems.length > 0 && pageForOp) {
|
|
608
699
|
if (!doNotPushHistory) {
|
|
609
|
-
this.pushOpHistory('remove', {
|
|
700
|
+
this.pushOpHistory('remove', {
|
|
701
|
+
diff: removedItems,
|
|
702
|
+
pageData: pageForOp,
|
|
703
|
+
historyDescription,
|
|
704
|
+
source: historySource,
|
|
705
|
+
});
|
|
610
706
|
} else {
|
|
611
707
|
this.selectionBeforeOp = null;
|
|
612
708
|
}
|
|
613
709
|
}
|
|
614
710
|
|
|
615
711
|
this.emit('remove', nodes);
|
|
712
|
+
this.emit('change', { type: 'remove', data: changeItems, historySource, doNotPushHistory });
|
|
713
|
+
|
|
714
|
+
// 页面 / 页面片删除不入历史栈(见上方 isPageOrFragment 分支),这里合并补发一次结构变更通知
|
|
715
|
+
const removedPages = nodes.filter((node) => isPageOrFragment(node)) as (MPage | MPageFragment)[];
|
|
716
|
+
if (removedPages.length) {
|
|
717
|
+
historyService.notifyPageStructureChange({ add: [], remove: removedPages });
|
|
718
|
+
}
|
|
616
719
|
}
|
|
617
720
|
|
|
618
721
|
public async doUpdate(
|
|
619
722
|
config: MNode,
|
|
620
|
-
{ changeRecords = [] }: { changeRecords?: ChangeRecord[] } = {},
|
|
723
|
+
{ changeRecords = [], historySource }: { changeRecords?: ChangeRecord[]; historySource?: HistoryOpSource } = {},
|
|
621
724
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
|
622
725
|
const root = this.get('root');
|
|
623
726
|
if (!root) throw new Error('root为空');
|
|
@@ -630,14 +733,14 @@ class Editor extends BaseService {
|
|
|
630
733
|
|
|
631
734
|
const node = toRaw(info.node);
|
|
632
735
|
|
|
633
|
-
let newConfig = await toggleFixedPosition(toRaw(config), node,
|
|
736
|
+
let newConfig = await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
|
|
634
737
|
|
|
635
738
|
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
|
636
739
|
|
|
637
740
|
if (!newConfig.type) throw new Error('配置缺少type值');
|
|
638
741
|
|
|
639
742
|
if (newConfig.type === NodeType.ROOT) {
|
|
640
|
-
this.set('root', newConfig as MApp);
|
|
743
|
+
this.set('root', newConfig as MApp, { historySource });
|
|
641
744
|
return {
|
|
642
745
|
oldNode: node,
|
|
643
746
|
newNode: newConfig,
|
|
@@ -670,7 +773,7 @@ class Editor extends BaseService {
|
|
|
670
773
|
}
|
|
671
774
|
|
|
672
775
|
// 只有被更新节点正好是当前选中页面时才同步 state.page,避免「更新非当前页」误将编辑器切到该页
|
|
673
|
-
if (
|
|
776
|
+
if (isPageOrFragment(newConfig)) {
|
|
674
777
|
const currentPage = this.get('page');
|
|
675
778
|
if (currentPage && `${currentPage.id}` === `${newConfig.id}`) {
|
|
676
779
|
this.set('page', newConfig as MPage | MPageFragment);
|
|
@@ -704,11 +807,24 @@ class Editor extends BaseService {
|
|
|
704
807
|
changeRecordList?: ChangeRecord[][];
|
|
705
808
|
doNotPushHistory?: boolean;
|
|
706
809
|
historyDescription?: string;
|
|
810
|
+
historySource?: HistoryOpSource;
|
|
811
|
+
/**
|
|
812
|
+
* 属性面板提交时携带的校验错误信息,在写入历史记录之前落库,
|
|
813
|
+
* 使历史快照与本次变更对齐,从而 undo/redo 能正确还原错误标记。
|
|
814
|
+
*/
|
|
815
|
+
invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string };
|
|
707
816
|
} = {},
|
|
708
817
|
): Promise<MNode | MNode[]> {
|
|
709
818
|
this.captureSelectionBeforeOp();
|
|
710
819
|
|
|
711
|
-
const {
|
|
820
|
+
const {
|
|
821
|
+
doNotPushHistory = false,
|
|
822
|
+
changeRecordList,
|
|
823
|
+
changeRecords,
|
|
824
|
+
historyDescription,
|
|
825
|
+
historySource,
|
|
826
|
+
invalidInfo,
|
|
827
|
+
} = data;
|
|
712
828
|
|
|
713
829
|
const nodes = Array.isArray(config) ? config : [config];
|
|
714
830
|
|
|
@@ -717,29 +833,32 @@ class Editor extends BaseService {
|
|
|
717
833
|
const updateData = await Promise.all(
|
|
718
834
|
nodes.map((node, index) => {
|
|
719
835
|
const recordsForNode = changeRecordList ? (changeRecordList[index] ?? []) : (changeRecords ?? []);
|
|
720
|
-
return this.doUpdate(node, { changeRecords: recordsForNode });
|
|
836
|
+
return this.doUpdate(node, { changeRecords: recordsForNode, historySource });
|
|
721
837
|
}),
|
|
722
838
|
);
|
|
723
839
|
|
|
840
|
+
// 校验错误信息在 pushOpHistory 之前落库,保证历史快照包含本次变更对应的错误状态。
|
|
841
|
+
this.applyInvalidInfo(config, invalidInfo);
|
|
842
|
+
|
|
724
843
|
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
|
|
725
844
|
const curNodes = this.get('nodes');
|
|
726
845
|
if (curNodes.length) {
|
|
727
846
|
if (!doNotPushHistory) {
|
|
728
847
|
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
|
729
|
-
this.pushOpHistory(
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
848
|
+
this.pushOpHistory('update', {
|
|
849
|
+
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
850
|
+
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
851
|
+
diff: buildUpdateDiff(
|
|
852
|
+
updateData.map((d) => ({
|
|
733
853
|
oldNode: cloneDeep(d.oldNode),
|
|
734
|
-
newNode: cloneDeep(
|
|
735
|
-
// 每个节点单独保留自己的 changeRecords,便于撤销/重做时按 propPath 精细化更新;
|
|
736
|
-
// 没有 changeRecords 的(如内部 sort/moveLayer 等整节点替换操作)会退化为全节点替换。
|
|
854
|
+
newNode: cloneDeep(d.newNode),
|
|
737
855
|
changeRecords: d.changeRecords?.length ? cloneDeep(d.changeRecords) : undefined,
|
|
738
856
|
})),
|
|
739
|
-
|
|
740
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
857
|
+
),
|
|
858
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
741
859
|
historyDescription,
|
|
742
|
-
|
|
860
|
+
source: historySource,
|
|
861
|
+
});
|
|
743
862
|
} else {
|
|
744
863
|
this.selectionBeforeOp = null;
|
|
745
864
|
}
|
|
@@ -747,6 +866,12 @@ class Editor extends BaseService {
|
|
|
747
866
|
}
|
|
748
867
|
|
|
749
868
|
this.emit('update', updateData);
|
|
869
|
+
this.emit('change', {
|
|
870
|
+
type: 'update',
|
|
871
|
+
data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
|
|
872
|
+
historySource,
|
|
873
|
+
doNotPushHistory,
|
|
874
|
+
});
|
|
750
875
|
return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
|
|
751
876
|
}
|
|
752
877
|
|
|
@@ -763,7 +888,7 @@ class Editor extends BaseService {
|
|
|
763
888
|
public async sort(
|
|
764
889
|
id1: Id,
|
|
765
890
|
id2: Id,
|
|
766
|
-
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
891
|
+
{ doNotSelect = false, doNotPushHistory = false, historySource }: DslOpOptions = {},
|
|
767
892
|
): Promise<void> {
|
|
768
893
|
this.captureSelectionBeforeOp();
|
|
769
894
|
|
|
@@ -783,7 +908,7 @@ class Editor extends BaseService {
|
|
|
783
908
|
|
|
784
909
|
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
|
785
910
|
|
|
786
|
-
await this.update(parent, { doNotPushHistory });
|
|
911
|
+
await this.update(parent, { doNotPushHistory, historySource });
|
|
787
912
|
if (!doNotSelect) {
|
|
788
913
|
await this.select(node);
|
|
789
914
|
}
|
|
@@ -836,7 +961,13 @@ class Editor extends BaseService {
|
|
|
836
961
|
public async paste(
|
|
837
962
|
position: PastePosition = {},
|
|
838
963
|
collectorOptions?: TargetOptions,
|
|
839
|
-
{
|
|
964
|
+
{
|
|
965
|
+
doNotSelect = false,
|
|
966
|
+
doNotSwitchPage = false,
|
|
967
|
+
doNotPushHistory = false,
|
|
968
|
+
historyDescription,
|
|
969
|
+
historySource,
|
|
970
|
+
}: DslOpOptions = {},
|
|
840
971
|
): Promise<MNode | MNode[] | void> {
|
|
841
972
|
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
|
842
973
|
if (!Array.isArray(config)) return;
|
|
@@ -857,7 +988,13 @@ class Editor extends BaseService {
|
|
|
857
988
|
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
|
858
989
|
}
|
|
859
990
|
|
|
860
|
-
return this.add(pasteConfigs, parent, {
|
|
991
|
+
return this.add(pasteConfigs, parent, {
|
|
992
|
+
doNotSelect,
|
|
993
|
+
doNotSwitchPage,
|
|
994
|
+
doNotPushHistory,
|
|
995
|
+
historyDescription,
|
|
996
|
+
historySource,
|
|
997
|
+
});
|
|
861
998
|
}
|
|
862
999
|
|
|
863
1000
|
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
|
@@ -893,14 +1030,14 @@ class Editor extends BaseService {
|
|
|
893
1030
|
*/
|
|
894
1031
|
public async alignCenter(
|
|
895
1032
|
config: MNode | MNode[],
|
|
896
|
-
{ doNotSelect = false, doNotPushHistory = false }: DslOpOptions = {},
|
|
1033
|
+
{ doNotSelect = false, doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
897
1034
|
): Promise<MNode | MNode[]> {
|
|
898
1035
|
const nodes = Array.isArray(config) ? config : [config];
|
|
899
1036
|
const stage = this.get('stage');
|
|
900
1037
|
|
|
901
1038
|
const newNodes = await Promise.all(nodes.map((node) => this.doAlignCenter(node)));
|
|
902
1039
|
|
|
903
|
-
const newNode = await this.update(newNodes, { doNotPushHistory });
|
|
1040
|
+
const newNode = await this.update(newNodes, { doNotPushHistory, historyDescription, historySource });
|
|
904
1041
|
|
|
905
1042
|
if (!doNotSelect) {
|
|
906
1043
|
if (newNodes.length > 1) {
|
|
@@ -919,7 +1056,10 @@ class Editor extends BaseService {
|
|
|
919
1056
|
* @param options 可选配置
|
|
920
1057
|
* @param options.doNotPushHistory 是否不写入历史记录(默认 false)
|
|
921
1058
|
*/
|
|
922
|
-
public async moveLayer(
|
|
1059
|
+
public async moveLayer(
|
|
1060
|
+
offset: number | LayerOffset,
|
|
1061
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1062
|
+
): Promise<void> {
|
|
923
1063
|
this.captureSelectionBeforeOp();
|
|
924
1064
|
|
|
925
1065
|
const root = this.get('root');
|
|
@@ -960,16 +1100,26 @@ class Editor extends BaseService {
|
|
|
960
1100
|
const pageForOp = this.getNodeInfo(node.id, false).page;
|
|
961
1101
|
this.pushOpHistory(
|
|
962
1102
|
'update',
|
|
1103
|
+
|
|
963
1104
|
{
|
|
964
|
-
|
|
1105
|
+
diff: buildUpdateDiff([{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }]),
|
|
1106
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1107
|
+
historyDescription,
|
|
1108
|
+
source: historySource,
|
|
965
1109
|
},
|
|
966
|
-
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
|
967
1110
|
);
|
|
968
1111
|
} else {
|
|
969
1112
|
this.selectionBeforeOp = null;
|
|
970
1113
|
}
|
|
971
1114
|
|
|
972
1115
|
this.emit('move-layer', offset);
|
|
1116
|
+
this.emit('change', {
|
|
1117
|
+
type: 'move-layer',
|
|
1118
|
+
data: [{ node, page: this.getPageOfNode(node.id) }],
|
|
1119
|
+
offset,
|
|
1120
|
+
historySource,
|
|
1121
|
+
doNotPushHistory,
|
|
1122
|
+
});
|
|
973
1123
|
}
|
|
974
1124
|
|
|
975
1125
|
/**
|
|
@@ -989,10 +1139,16 @@ class Editor extends BaseService {
|
|
|
989
1139
|
public async moveToContainer(
|
|
990
1140
|
config: MNode | MNode[],
|
|
991
1141
|
targetId: Id,
|
|
992
|
-
{
|
|
1142
|
+
{
|
|
1143
|
+
doNotSelect = false,
|
|
1144
|
+
doNotSwitchPage = false,
|
|
1145
|
+
doNotPushHistory = false,
|
|
1146
|
+
historyDescription,
|
|
1147
|
+
historySource,
|
|
1148
|
+
}: DslOpOptions = {},
|
|
993
1149
|
): Promise<MNode | MNode[]> {
|
|
994
1150
|
const isBatch = Array.isArray(config);
|
|
995
|
-
const configs = (isBatch ? config : [config]).filter((item) => !(
|
|
1151
|
+
const configs = (isBatch ? config : [config]).filter((item) => !isPageOrFragment(item));
|
|
996
1152
|
|
|
997
1153
|
if (configs.length === 0) {
|
|
998
1154
|
throw new Error('没有可移动的节点');
|
|
@@ -1052,7 +1208,12 @@ class Editor extends BaseService {
|
|
|
1052
1208
|
newNode: cloneDeep(toRaw(this.getNodeById(id, false))) as MNode,
|
|
1053
1209
|
}));
|
|
1054
1210
|
const historyPage = moves[0].pageForOp ?? { name: '', id: target.id };
|
|
1055
|
-
this.pushOpHistory('update', {
|
|
1211
|
+
this.pushOpHistory('update', {
|
|
1212
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1213
|
+
pageData: historyPage,
|
|
1214
|
+
historyDescription,
|
|
1215
|
+
source: historySource,
|
|
1216
|
+
});
|
|
1056
1217
|
} else {
|
|
1057
1218
|
this.selectionBeforeOp = null;
|
|
1058
1219
|
}
|
|
@@ -1064,7 +1225,7 @@ class Editor extends BaseService {
|
|
|
1064
1225
|
config: MNode | MNode[],
|
|
1065
1226
|
targetParent: MContainer,
|
|
1066
1227
|
targetIndex: number,
|
|
1067
|
-
{ doNotPushHistory = false }: DslOpOptions = {},
|
|
1228
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1068
1229
|
) {
|
|
1069
1230
|
this.captureSelectionBeforeOp();
|
|
1070
1231
|
|
|
@@ -1127,20 +1288,115 @@ class Editor extends BaseService {
|
|
|
1127
1288
|
}
|
|
1128
1289
|
if (!doNotPushHistory) {
|
|
1129
1290
|
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
|
1130
|
-
this.pushOpHistory('update', {
|
|
1291
|
+
this.pushOpHistory('update', {
|
|
1292
|
+
diff: buildUpdateDiff(updatedItems),
|
|
1293
|
+
pageData: { name: pageForOp?.name || '', id: pageForOp!.id },
|
|
1294
|
+
historyDescription,
|
|
1295
|
+
source: historySource,
|
|
1296
|
+
});
|
|
1131
1297
|
} else {
|
|
1132
1298
|
this.selectionBeforeOp = null;
|
|
1133
1299
|
}
|
|
1134
1300
|
|
|
1135
1301
|
this.emit('drag-to', { targetIndex, configs, targetParent });
|
|
1302
|
+
this.emit('change', {
|
|
1303
|
+
type: 'drag-to',
|
|
1304
|
+
data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
1305
|
+
targetIndex,
|
|
1306
|
+
targetParent,
|
|
1307
|
+
historySource,
|
|
1308
|
+
doNotPushHistory,
|
|
1309
|
+
});
|
|
1136
1310
|
}
|
|
1137
1311
|
|
|
1312
|
+
// #region AndGetHistoryId
|
|
1313
|
+
/**
|
|
1314
|
+
* 下列 *AndGetHistoryId 方法与对应的普通操作(add / remove / update ...)行为完全一致,
|
|
1315
|
+
* 返回值在 {@link DslOpWithHistoryIdsResult} 中同时包含原操作结果与本次写入历史栈的 uuid 列表({@link StepValue.uuid}),
|
|
1316
|
+
* 可用于精确引用 / 定位该条历史记录(埋点、revert、跨端同步等)。
|
|
1317
|
+
*
|
|
1318
|
+
* 当本次操作未写入历史(doNotPushHistory 为 true、或操作无实际变更 / 提前返回)时 historyIds 为 `[]`。
|
|
1319
|
+
*/
|
|
1320
|
+
|
|
1321
|
+
/** 等价于 {@link add},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1322
|
+
public async addAndGetHistoryId(
|
|
1323
|
+
addNode: AddMNode | MNode[],
|
|
1324
|
+
parent?: MContainer | null,
|
|
1325
|
+
options: DslOpOptions = {},
|
|
1326
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1327
|
+
this.lastPushedHistoryId = null;
|
|
1328
|
+
const result = await this.add(addNode, parent, options);
|
|
1329
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
/** 等价于 {@link remove},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1333
|
+
public async removeAndGetHistoryId(
|
|
1334
|
+
nodeOrNodeList: MNode | MNode[],
|
|
1335
|
+
options: DslOpOptions = {},
|
|
1336
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1337
|
+
this.lastPushedHistoryId = null;
|
|
1338
|
+
await this.remove(nodeOrNodeList, options);
|
|
1339
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/** 等价于 {@link update},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1343
|
+
public async updateAndGetHistoryId(
|
|
1344
|
+
config: MNode | MNode[],
|
|
1345
|
+
data: {
|
|
1346
|
+
changeRecords?: ChangeRecord[];
|
|
1347
|
+
changeRecordList?: ChangeRecord[][];
|
|
1348
|
+
doNotPushHistory?: boolean;
|
|
1349
|
+
historyDescription?: string;
|
|
1350
|
+
historySource?: HistoryOpSource;
|
|
1351
|
+
} = {},
|
|
1352
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1353
|
+
this.lastPushedHistoryId = null;
|
|
1354
|
+
const result = await this.update(config, data);
|
|
1355
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
/** 等价于 {@link moveLayer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1359
|
+
public async moveLayerAndGetHistoryId(
|
|
1360
|
+
offset: number | LayerOffset,
|
|
1361
|
+
options: DslOpOptions = {},
|
|
1362
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1363
|
+
this.lastPushedHistoryId = null;
|
|
1364
|
+
await this.moveLayer(offset, options);
|
|
1365
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
/** 等价于 {@link moveToContainer},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1369
|
+
public async moveToContainerAndGetHistoryId(
|
|
1370
|
+
config: MNode | MNode[],
|
|
1371
|
+
targetId: Id,
|
|
1372
|
+
options: DslOpOptions = {},
|
|
1373
|
+
): Promise<DslOpWithHistoryIdsResult<MNode | MNode[]>> {
|
|
1374
|
+
this.lastPushedHistoryId = null;
|
|
1375
|
+
const result = await this.moveToContainer(config, targetId, options);
|
|
1376
|
+
return { result, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
/** 等价于 {@link dragTo},并额外返回本次写入历史记录的 uuid 列表(未入栈时 historyIds 为 `[]`)。 */
|
|
1380
|
+
public async dragToAndGetHistoryId(
|
|
1381
|
+
config: MNode | MNode[],
|
|
1382
|
+
targetParent: MContainer,
|
|
1383
|
+
targetIndex: number,
|
|
1384
|
+
options: DslOpOptions = {},
|
|
1385
|
+
): Promise<DslOpWithHistoryIdsResult<void>> {
|
|
1386
|
+
this.lastPushedHistoryId = null;
|
|
1387
|
+
await this.dragTo(config, targetParent, targetIndex, options);
|
|
1388
|
+
return { result: undefined, historyIds: getLastPushedHistoryIds(this.lastPushedHistoryId) };
|
|
1389
|
+
}
|
|
1390
|
+
// #endregion AndGetHistoryId
|
|
1391
|
+
|
|
1138
1392
|
/**
|
|
1139
1393
|
* 撤销当前操作
|
|
1140
1394
|
* @returns 被撤销的操作
|
|
1141
1395
|
*/
|
|
1142
1396
|
public async undo(): Promise<StepValue | null> {
|
|
1143
|
-
const
|
|
1397
|
+
const pageId = this.get('page')?.id;
|
|
1398
|
+
if (pageId === undefined) return null;
|
|
1399
|
+
const value = historyService.undo('page', pageId);
|
|
1144
1400
|
if (value) {
|
|
1145
1401
|
await this.applyHistoryOp(value, true);
|
|
1146
1402
|
}
|
|
@@ -1152,7 +1408,9 @@ class Editor extends BaseService {
|
|
|
1152
1408
|
* @returns 被恢复的操作
|
|
1153
1409
|
*/
|
|
1154
1410
|
public async redo(): Promise<StepValue | null> {
|
|
1155
|
-
const
|
|
1411
|
+
const pageId = this.get('page')?.id;
|
|
1412
|
+
if (pageId === undefined) return null;
|
|
1413
|
+
const value = historyService.redo('page', pageId);
|
|
1156
1414
|
if (value) {
|
|
1157
1415
|
await this.applyHistoryOp(value, false);
|
|
1158
1416
|
}
|
|
@@ -1176,32 +1434,41 @@ class Editor extends BaseService {
|
|
|
1176
1434
|
* @returns 反向后产生的新 step;目标不存在 / 未应用 / 反向失败时返回 null
|
|
1177
1435
|
*/
|
|
1178
1436
|
public async revertPageStep(index: number): Promise<StepValue | null> {
|
|
1179
|
-
const list = historyService.
|
|
1437
|
+
const list = historyService.getStepList('page', this.get('page')?.id);
|
|
1180
1438
|
const entry = list[index];
|
|
1181
1439
|
if (!entry?.applied) return null;
|
|
1182
1440
|
|
|
1183
1441
|
const { step } = entry;
|
|
1442
|
+
// 初始基线(index 0 的 initial step)是栈底线,不可回滚。
|
|
1443
|
+
if (step.opType === 'initial') return null;
|
|
1184
1444
|
const root = this.get('root');
|
|
1185
1445
|
if (!root) return null;
|
|
1186
1446
|
|
|
1447
|
+
// 更新类步骤必须带 changeRecords 才支持回滚:缺失时只能整节点替换,会冲掉后续无关变更,故不支持。
|
|
1448
|
+
if (step.opType === 'update') {
|
|
1449
|
+
const items = step.diff ?? [];
|
|
1450
|
+
if (!items.length || !items.every((item) => item.changeRecords?.length)) return null;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1187
1453
|
// 反向应用产生的新 step 由内部 pushOpHistory 触发 history `change` 事件,监听一次以拿到引用。
|
|
1188
1454
|
let revertedStep: StepValue | null = null;
|
|
1189
|
-
|
|
1455
|
+
// page 的 `change` 事件回调签名为 `(pageId, step)`,这里只关心被回滚产生的新 step。
|
|
1456
|
+
const captureRevert = (_pageId: Id, s: StepValue) => {
|
|
1190
1457
|
revertedStep = s;
|
|
1191
1458
|
};
|
|
1192
1459
|
historyService.once('change', captureRevert);
|
|
1193
1460
|
|
|
1194
1461
|
const historyDescription = `回滚 #${index + 1}: ${describeStepForRevert(step)}`;
|
|
1195
1462
|
// revert 走 public add/remove/update,让操作以一条普通新 step 入栈;不要切换选区与页面,避免打断用户。
|
|
1196
|
-
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription } as const;
|
|
1463
|
+
const opts = { doNotSelect: true, doNotSwitchPage: true, historyDescription, historySource: 'rollback' } as const;
|
|
1197
1464
|
|
|
1198
1465
|
try {
|
|
1199
1466
|
switch (step.opType) {
|
|
1200
1467
|
case 'add': {
|
|
1201
1468
|
// 原本是新增 → revert 即删除当时被加入的节点
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
const existing = this.getNodeById(
|
|
1469
|
+
for (const { newSchema } of step.diff ?? []) {
|
|
1470
|
+
if (!newSchema) continue;
|
|
1471
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1205
1472
|
if (existing) {
|
|
1206
1473
|
await this.remove(existing, opts);
|
|
1207
1474
|
}
|
|
@@ -1211,37 +1478,42 @@ class Editor extends BaseService {
|
|
|
1211
1478
|
case 'remove': {
|
|
1212
1479
|
// 原本是删除 → revert 即把节点按原父容器加回来。
|
|
1213
1480
|
// 按原 index 升序逐个插回,先小后大避免索引漂移。
|
|
1214
|
-
const items = step.
|
|
1215
|
-
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1216
|
-
for (const {
|
|
1481
|
+
const items = step.diff ?? [];
|
|
1482
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1483
|
+
for (const { oldSchema, parentId } of sorted) {
|
|
1484
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1217
1485
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1218
1486
|
if (parent) {
|
|
1219
|
-
await this.add([cloneDeep(
|
|
1487
|
+
await this.add([cloneDeep(oldSchema)] as MNode[], parent, opts);
|
|
1220
1488
|
}
|
|
1221
1489
|
}
|
|
1222
1490
|
break;
|
|
1223
1491
|
}
|
|
1224
1492
|
case 'update': {
|
|
1225
|
-
// 原本是更新 → revert 即把
|
|
1493
|
+
// 原本是更新 → revert 即把 oldSchema 的值写回;
|
|
1226
1494
|
// 优先按 changeRecords 局部 patch(仅触达 propPath 下的字段,避免冲掉同节点上其它无关变更)。
|
|
1227
|
-
const items = step.
|
|
1228
|
-
const configs = items
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1495
|
+
const items = step.diff ?? [];
|
|
1496
|
+
const configs = items
|
|
1497
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
1498
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
1499
|
+
const oldNode = oldSchema!;
|
|
1500
|
+
const newNode = newSchema!;
|
|
1501
|
+
if (changeRecords?.length) {
|
|
1502
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
1503
|
+
for (const record of changeRecords) {
|
|
1504
|
+
if (!record.propPath) {
|
|
1505
|
+
// 没有 propPath 视为整节点替换
|
|
1506
|
+
return cloneDeep(oldNode);
|
|
1507
|
+
}
|
|
1508
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, oldNode));
|
|
1509
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1235
1510
|
}
|
|
1236
|
-
|
|
1237
|
-
setValueByKeyPath(record.propPath, value, patch);
|
|
1511
|
+
return patch;
|
|
1238
1512
|
}
|
|
1239
|
-
return
|
|
1240
|
-
}
|
|
1241
|
-
return cloneDeep(oldNode);
|
|
1242
|
-
});
|
|
1513
|
+
return cloneDeep(oldNode);
|
|
1514
|
+
});
|
|
1243
1515
|
if (configs.length) {
|
|
1244
|
-
await this.update(configs, { historyDescription });
|
|
1516
|
+
await this.update(configs, { historyDescription, historySource: 'rollback' });
|
|
1245
1517
|
}
|
|
1246
1518
|
break;
|
|
1247
1519
|
}
|
|
@@ -1259,6 +1531,23 @@ class Editor extends BaseService {
|
|
|
1259
1531
|
return revertedStep;
|
|
1260
1532
|
}
|
|
1261
1533
|
|
|
1534
|
+
/**
|
|
1535
|
+
* 通过历史记录 uuid 回滚当前页面的历史步骤,语义与 {@link revertPageStep} 完全一致,
|
|
1536
|
+
* 仅入参从 index 改为 uuid 列表({@link StepValue.uuid})。按数组顺序依次回滚,
|
|
1537
|
+
* 返回与入参同序的结果列表(某项失败时为 `null`)。
|
|
1538
|
+
*
|
|
1539
|
+
* @param uuids 目标历史记录的 uuid 列表,通常由 *AndGetHistoryId 方法返回的 `historyIds`
|
|
1540
|
+
*/
|
|
1541
|
+
public async revertPageStepById(uuids: string[]): Promise<(StepValue | null)[]> {
|
|
1542
|
+
const results: (StepValue | null)[] = [];
|
|
1543
|
+
const pageId = this.get('page')?.id;
|
|
1544
|
+
for (const uuid of uuids) {
|
|
1545
|
+
const location = historyService.findStepLocationByUuid('page', uuid, pageId);
|
|
1546
|
+
results.push(!location ? null : await this.revertPageStep(location.index));
|
|
1547
|
+
}
|
|
1548
|
+
return results;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1262
1551
|
/**
|
|
1263
1552
|
* 跳转当前页面历史栈到指定游标位置。
|
|
1264
1553
|
*
|
|
@@ -1269,8 +1558,9 @@ class Editor extends BaseService {
|
|
|
1269
1558
|
* @returns 实际移动到的最终游标位置
|
|
1270
1559
|
*/
|
|
1271
1560
|
public async gotoPageStep(targetCursor: number): Promise<number> {
|
|
1272
|
-
|
|
1273
|
-
|
|
1561
|
+
const pageId = this.get('page')?.id;
|
|
1562
|
+
let cursor = historyService.getCursor('page', pageId);
|
|
1563
|
+
const { length } = historyService.getStepList('page', pageId);
|
|
1274
1564
|
const target = Math.max(0, Math.min(targetCursor, length));
|
|
1275
1565
|
while (cursor > target) {
|
|
1276
1566
|
const step = await this.undo();
|
|
@@ -1285,14 +1575,21 @@ class Editor extends BaseService {
|
|
|
1285
1575
|
return cursor;
|
|
1286
1576
|
}
|
|
1287
1577
|
|
|
1288
|
-
public async move(
|
|
1578
|
+
public async move(
|
|
1579
|
+
left: number,
|
|
1580
|
+
top: number,
|
|
1581
|
+
{ doNotPushHistory = false, historyDescription, historySource }: DslOpOptions = {},
|
|
1582
|
+
) {
|
|
1289
1583
|
const node = toRaw(this.get('node'));
|
|
1290
1584
|
if (!node || isPage(node)) return;
|
|
1291
1585
|
|
|
1292
1586
|
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
|
1293
1587
|
if (!newStyle) return;
|
|
1294
1588
|
|
|
1295
|
-
await this.update(
|
|
1589
|
+
await this.update(
|
|
1590
|
+
{ id: node.id, type: node.type, style: newStyle },
|
|
1591
|
+
{ doNotPushHistory, historyDescription, historySource },
|
|
1592
|
+
);
|
|
1296
1593
|
}
|
|
1297
1594
|
|
|
1298
1595
|
public resetState() {
|
|
@@ -1304,6 +1601,7 @@ class Editor extends BaseService {
|
|
|
1304
1601
|
this.set('stage', null);
|
|
1305
1602
|
this.set('highlightNode', null);
|
|
1306
1603
|
this.set('modifiedNodeIds', new Map());
|
|
1604
|
+
this.set('invalidNodeIds', new Map());
|
|
1307
1605
|
this.set('pageLength', 0);
|
|
1308
1606
|
}
|
|
1309
1607
|
|
|
@@ -1317,6 +1615,62 @@ class Editor extends BaseService {
|
|
|
1317
1615
|
this.get('modifiedNodeIds').clear();
|
|
1318
1616
|
}
|
|
1319
1617
|
|
|
1618
|
+
/**
|
|
1619
|
+
* 记录(或覆盖)某个节点在指定来源(属性表单 / 样式表单)上的校验错误信息。
|
|
1620
|
+
* @param id 节点 id
|
|
1621
|
+
* @param source 错误来源:'props'(属性表单)| 'style'(样式表单)
|
|
1622
|
+
* @param message 错误文案(可能是包含 <br> 的 HTML)
|
|
1623
|
+
*/
|
|
1624
|
+
public setInvalidNode(id: Id, source: NodeInvalidSource, message: string) {
|
|
1625
|
+
const map = this.get('invalidNodeIds');
|
|
1626
|
+
const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
|
|
1627
|
+
info[source] = message;
|
|
1628
|
+
map.set(id, info);
|
|
1629
|
+
this.emit('invalid-node-change', map);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* 删除节点的校验错误记录。
|
|
1634
|
+
* @param id 节点 id
|
|
1635
|
+
* @param source 指定来源则仅删除该来源;不传则删除该节点全部来源的错误
|
|
1636
|
+
*/
|
|
1637
|
+
public deleteInvalidNode(id: Id, source?: NodeInvalidSource) {
|
|
1638
|
+
const map = this.get('invalidNodeIds');
|
|
1639
|
+
if (!map.has(id)) return;
|
|
1640
|
+
|
|
1641
|
+
if (!source) {
|
|
1642
|
+
map.delete(id);
|
|
1643
|
+
} else {
|
|
1644
|
+
const info: NodeInvalidInfo = { ...(map.get(id) || {}) };
|
|
1645
|
+
delete info[source];
|
|
1646
|
+
if (info.props || info.style) {
|
|
1647
|
+
map.set(id, info);
|
|
1648
|
+
} else {
|
|
1649
|
+
map.delete(id);
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
this.emit('invalid-node-change', map);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
/** 获取当前存在校验错误的节点错误 Map(key 为节点 id) */
|
|
1657
|
+
public getInvalidNodeIds(): Map<Id, NodeInvalidInfo> {
|
|
1658
|
+
return this.get('invalidNodeIds');
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
/** 获取指定节点的校验错误信息 */
|
|
1662
|
+
public getInvalidNodeInfo(id: Id): NodeInvalidInfo | undefined {
|
|
1663
|
+
return this.get('invalidNodeIds').get(id);
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1666
|
+
/** 清空全部校验错误记录 */
|
|
1667
|
+
public resetInvalidNodeId() {
|
|
1668
|
+
const map = this.get('invalidNodeIds');
|
|
1669
|
+
if (map.size === 0) return;
|
|
1670
|
+
map.clear();
|
|
1671
|
+
this.emit('invalid-node-change', map);
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1320
1674
|
public usePlugin(options: AsyncHookPlugin<AsyncMethodName, Editor>): void {
|
|
1321
1675
|
super.usePlugin(options);
|
|
1322
1676
|
}
|
|
@@ -1339,113 +1693,326 @@ class Editor extends BaseService {
|
|
|
1339
1693
|
return super.emit(eventName, ...args);
|
|
1340
1694
|
}
|
|
1341
1695
|
|
|
1696
|
+
/**
|
|
1697
|
+
* 应用一次属性面板提交携带的校验错误信息(在写入历史记录之前调用,使历史快照与本次变更对齐)。
|
|
1698
|
+
* - invalidInfo 为空(调用方未携带):认为本次更新对应的节点已无校验错误,清除其全部来源的错误;
|
|
1699
|
+
* - error 非空则记录错误,为空则清除对应来源的错误。
|
|
1700
|
+
*/
|
|
1701
|
+
private applyInvalidInfo(
|
|
1702
|
+
config: MNode | MNode[],
|
|
1703
|
+
invalidInfo?: { id: Id; source: NodeInvalidSource; error?: string },
|
|
1704
|
+
) {
|
|
1705
|
+
if (!invalidInfo) {
|
|
1706
|
+
// 调用方未携带 invalidInfo:本次更新对应节点不应再保留校验错误,清除其全部来源。
|
|
1707
|
+
const ids = (Array.isArray(config) ? config : [config])
|
|
1708
|
+
.map((node) => node.id)
|
|
1709
|
+
.filter((id): id is Id => id !== undefined);
|
|
1710
|
+
ids.forEach((id) => this.deleteInvalidNode(id));
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
const { id, source, error } = invalidInfo;
|
|
1714
|
+
if (error) {
|
|
1715
|
+
this.setInvalidNode(id, source, error);
|
|
1716
|
+
} else {
|
|
1717
|
+
this.deleteInvalidNode(id, source);
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
/** 删除被移除节点(含其子树)的校验错误记录,避免残留误报 */
|
|
1722
|
+
private removeInvalidNodesBySubtree(nodes: MNode[]) {
|
|
1723
|
+
const map = this.get('invalidNodeIds');
|
|
1724
|
+
if (map.size === 0) return;
|
|
1725
|
+
|
|
1726
|
+
let changed = false;
|
|
1727
|
+
nodes.forEach((node) => {
|
|
1728
|
+
traverseNode(node, (n) => {
|
|
1729
|
+
if (n.id !== undefined && map.delete(n.id)) {
|
|
1730
|
+
changed = true;
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
});
|
|
1734
|
+
|
|
1735
|
+
if (changed) {
|
|
1736
|
+
this.emit('invalid-node-change', map);
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
/** 清理不在当前 DSL 中的失效节点错误记录(用于整体替换 root 后) */
|
|
1741
|
+
private pruneInvalidNodeIds() {
|
|
1742
|
+
const map = this.get('invalidNodeIds');
|
|
1743
|
+
if (map.size === 0) return;
|
|
1744
|
+
|
|
1745
|
+
let changed = false;
|
|
1746
|
+
for (const id of [...map.keys()]) {
|
|
1747
|
+
if (!this.getNodeById(id, false)) {
|
|
1748
|
+
map.delete(id);
|
|
1749
|
+
changed = true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (changed) {
|
|
1754
|
+
this.emit('invalid-node-change', map);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1342
1758
|
private addModifiedNodeId(id: Id) {
|
|
1343
1759
|
this.get('modifiedNodeIds').set(id, id);
|
|
1344
1760
|
}
|
|
1345
1761
|
|
|
1762
|
+
/**
|
|
1763
|
+
* 获取指定节点所属的页面 / 页面片:
|
|
1764
|
+
* - 普通节点返回其所在的 page;
|
|
1765
|
+
* - 节点本身就是 page / pageFragment 时返回它自己;
|
|
1766
|
+
* - 找不到时返回 null。
|
|
1767
|
+
* 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
|
|
1768
|
+
*/
|
|
1769
|
+
private getPageOfNode(id: Id): MPage | MPageFragment | null {
|
|
1770
|
+
const { node, page } = this.getNodeInfo(id, false);
|
|
1771
|
+
if (page) return page;
|
|
1772
|
+
if (node && isPageOrFragment(node)) return node as MPage | MPageFragment;
|
|
1773
|
+
return null;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1346
1776
|
private captureSelectionBeforeOp() {
|
|
1347
1777
|
if (this.selectionBeforeOp) return;
|
|
1348
1778
|
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
|
1779
|
+
// 与选区快照同步捕获「操作前」的校验错误状态;因 applyInvalidInfo 会在 pushOpHistory 前修改 invalidNodeIds,
|
|
1780
|
+
// 必须在此(操作最开始)留存操作前快照,供 undo 还原。
|
|
1781
|
+
this.invalidNodeIdsBeforeOp = new Map(this.get('invalidNodeIds'));
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1784
|
+
/**
|
|
1785
|
+
* 比较「上一次 root」与「新 root」的页面 / 页面片,按页面粒度把整体替换拆成历史记录:
|
|
1786
|
+
* - 新旧都存在且内容变化的页面 → 一条 `update`(整页快照替换,无 changeRecords);
|
|
1787
|
+
* - 仅新 root 存在的页面 → 一条 `add`;
|
|
1788
|
+
* - 仅旧 root 存在的页面 → 一条 `remove`。
|
|
1789
|
+
*
|
|
1790
|
+
* 每条记录落到对应页面自己的历史栈(与普通节点操作一致),并标记来源 `source`。
|
|
1791
|
+
* 内容未变化的页面不产生记录,避免重复设置相同 DSL 时产生噪声。
|
|
1792
|
+
*/
|
|
1793
|
+
private pushRootDiffHistory(preRoot: MApp, nextRoot: MApp, source?: HistoryOpSource): void {
|
|
1794
|
+
const prevPages = preRoot?.items || [];
|
|
1795
|
+
const nextPages = nextRoot?.items || [];
|
|
1796
|
+
const prevMap = new Map(prevPages.map((p) => [`${p.id}`, p]));
|
|
1797
|
+
const nextMap = new Map(nextPages.map((p) => [`${p.id}`, p]));
|
|
1798
|
+
const indexInItems = (root: MApp, id: Id) => (root.items ?? []).findIndex((item) => `${item.id}` === `${id}`);
|
|
1799
|
+
|
|
1800
|
+
// 收集本次整体替换中增删的页面,循环结束后合并为一次结构变更通知(避免逐页派发多个事件)
|
|
1801
|
+
const addedPages: (MPage | MPageFragment)[] = [];
|
|
1802
|
+
const removedPages: (MPage | MPageFragment)[] = [];
|
|
1803
|
+
|
|
1804
|
+
nextPages.forEach((nextPage) => {
|
|
1805
|
+
const prevPage = prevMap.get(`${nextPage.id}`);
|
|
1806
|
+
if (!prevPage) {
|
|
1807
|
+
this.pushPageDiffStep(
|
|
1808
|
+
'add',
|
|
1809
|
+
nextPage,
|
|
1810
|
+
{ newSchema: cloneDeep(toRaw(nextPage)), parentId: nextRoot.id, index: indexInItems(nextRoot, nextPage.id) },
|
|
1811
|
+
source,
|
|
1812
|
+
);
|
|
1813
|
+
addedPages.push(nextPage);
|
|
1814
|
+
} else if (!isEqual(toRaw(prevPage), toRaw(nextPage))) {
|
|
1815
|
+
this.pushPageDiffStep(
|
|
1816
|
+
'update',
|
|
1817
|
+
nextPage,
|
|
1818
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), newSchema: cloneDeep(toRaw(nextPage)) },
|
|
1819
|
+
source,
|
|
1820
|
+
);
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
prevPages.forEach((prevPage) => {
|
|
1825
|
+
if (!nextMap.has(`${prevPage.id}`)) {
|
|
1826
|
+
this.pushPageDiffStep(
|
|
1827
|
+
'remove',
|
|
1828
|
+
prevPage,
|
|
1829
|
+
{ oldSchema: cloneDeep(toRaw(prevPage)), parentId: preRoot.id, index: indexInItems(preRoot, prevPage.id) },
|
|
1830
|
+
source,
|
|
1831
|
+
);
|
|
1832
|
+
removedPages.push(prevPage);
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
|
|
1836
|
+
if (addedPages.length || removedPages.length) {
|
|
1837
|
+
historyService.notifyPageStructureChange({ add: addedPages, remove: removedPages });
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
/**
|
|
1842
|
+
* 构造一条页面级「set root」历史记录(不携带选区 / modifiedNodeIds 上下文)并落到该页面自己的栈。
|
|
1843
|
+
*
|
|
1844
|
+
* 连续 set root 替换:若该页栈最新一条已是**同来源**的 set root 记录({@link StepValue.rootStep} 且 `source` 相同),
|
|
1845
|
+
* 则用本次记录**替换**它而非新增,避免源码反复保存 / 外部重设 DSL 时堆积多条 root 记录;
|
|
1846
|
+
* 来源不同则照常新增(initial 基线不是 rootStep,不在此列)。
|
|
1847
|
+
*/
|
|
1848
|
+
private pushPageDiffStep(
|
|
1849
|
+
opType: HistoryOpType,
|
|
1850
|
+
page: MPage | MPageFragment,
|
|
1851
|
+
diffItem: StepDiffItem<MNode>,
|
|
1852
|
+
source?: HistoryOpSource,
|
|
1853
|
+
): void {
|
|
1854
|
+
const step: StepValue = {
|
|
1855
|
+
uuid: guid(),
|
|
1856
|
+
data: { name: page.name || '', id: page.id },
|
|
1857
|
+
opType,
|
|
1858
|
+
extra: {
|
|
1859
|
+
selectedBefore: [],
|
|
1860
|
+
selectedAfter: [],
|
|
1861
|
+
modifiedNodeIds: new Map(),
|
|
1862
|
+
},
|
|
1863
|
+
diff: [diffItem],
|
|
1864
|
+
rootStep: true,
|
|
1865
|
+
};
|
|
1866
|
+
if (source) step.source = source;
|
|
1867
|
+
|
|
1868
|
+
const top = historyService.getCurrentPageStep(page.id);
|
|
1869
|
+
if (top?.rootStep && top.source === source) {
|
|
1870
|
+
historyService.replaceCurrentStep('page', step, page.id);
|
|
1871
|
+
} else {
|
|
1872
|
+
historyService.push('page', step, page.id);
|
|
1873
|
+
}
|
|
1349
1874
|
}
|
|
1350
1875
|
|
|
1351
1876
|
private pushOpHistory(
|
|
1352
1877
|
opType: HistoryOpType,
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1878
|
+
{
|
|
1879
|
+
diff,
|
|
1880
|
+
pageData,
|
|
1881
|
+
historyDescription,
|
|
1882
|
+
source,
|
|
1883
|
+
}: {
|
|
1884
|
+
diff: StepDiffItem<MNode>[];
|
|
1885
|
+
pageData: { name: string; id: Id };
|
|
1886
|
+
historyDescription?: string;
|
|
1887
|
+
source?: HistoryOpSource;
|
|
1888
|
+
},
|
|
1889
|
+
): string | null {
|
|
1357
1890
|
const step: StepValue = {
|
|
1891
|
+
uuid: guid(),
|
|
1358
1892
|
data: pageData,
|
|
1359
1893
|
opType,
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1894
|
+
extra: {
|
|
1895
|
+
selectedBefore: this.selectionBeforeOp ?? [],
|
|
1896
|
+
selectedAfter: this.get('nodes').map((n) => n.id),
|
|
1897
|
+
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
|
1898
|
+
// 方向性双快照:undo 还原「操作前」错误状态,redo 还原「操作后」错误状态。
|
|
1899
|
+
invalidNodeIdsBefore: new Map(this.invalidNodeIdsBeforeOp ?? this.get('invalidNodeIds')),
|
|
1900
|
+
invalidNodeIdsAfter: new Map(this.get('invalidNodeIds')),
|
|
1901
|
+
},
|
|
1902
|
+
diff,
|
|
1364
1903
|
};
|
|
1365
1904
|
if (historyDescription) step.historyDescription = historyDescription;
|
|
1905
|
+
if (source) step.source = source;
|
|
1366
1906
|
// 显式按 step.data.id 入栈:跨页操作(如 moveToContainer 从源页搬到目标页)
|
|
1367
1907
|
// 必须落到正确的页面栈,否则会把记录错误地推到当前活动页 / 操作发起页。
|
|
1368
|
-
historyService.push(step, pageData.id);
|
|
1908
|
+
const pushed = historyService.push('page', step, pageData.id);
|
|
1909
|
+
// push 返回 null 表示当前没有可写入的页面栈(未真正入栈),此时不应返回 uuid。
|
|
1910
|
+
const historyId = pushed ? step.uuid : null;
|
|
1911
|
+
this.lastPushedHistoryId = historyId;
|
|
1369
1912
|
this.selectionBeforeOp = null;
|
|
1913
|
+
this.invalidNodeIdsBeforeOp = null;
|
|
1914
|
+
return historyId;
|
|
1370
1915
|
}
|
|
1371
1916
|
|
|
1372
1917
|
/**
|
|
1373
1918
|
* 应用历史操作(撤销 / 重做)
|
|
1374
1919
|
*
|
|
1375
|
-
*
|
|
1376
|
-
* `doNotSelect / doNotSwitchPage`
|
|
1920
|
+
* 删除 / 更新类修改走 `editor.remove / update`,并通过 `doNotPushHistory` 阻止再次入栈、
|
|
1921
|
+
* `doNotSelect / doNotSwitchPage` 让选区由方法末尾的统一逻辑兜底;
|
|
1922
|
+
* 「重新插回节点」(撤销 remove / 重做 add)需按 step 记录的 parentId / index 精确还原,
|
|
1923
|
+
* 不走 this.add,由手工 splice + stage.add 完成后补发等价的 add / change 事件(见 emitHistoryInsertEvents)。
|
|
1377
1924
|
*
|
|
1378
|
-
*
|
|
1925
|
+
* 注意:这些路径都会发出 add / remove / update / change 事件,业务侧若需要区分"用户操作"与"撤销重做触发",
|
|
1379
1926
|
* 请监听 `history-change` 事件配合判断。
|
|
1380
1927
|
*
|
|
1381
1928
|
* @param step 操作记录
|
|
1382
1929
|
* @param reverse true = 撤销,false = 重做
|
|
1383
1930
|
*/
|
|
1384
1931
|
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
|
1932
|
+
// 初始基线 step 仅作展示,不承载任何变更,撤销/重做时无需应用(正常流程下也不会被触达)。
|
|
1933
|
+
if (step.opType === 'initial') return;
|
|
1385
1934
|
const root = this.get('root');
|
|
1386
1935
|
const stage = this.get('stage');
|
|
1387
1936
|
if (!root) return;
|
|
1388
1937
|
|
|
1389
|
-
|
|
1938
|
+
// 撤销/重做内部复用 add/remove/update,透传被应用 step 上记录的 source,
|
|
1939
|
+
// 使其触发的 change 事件同样携带「历史来源」
|
|
1940
|
+
const commonOpts = {
|
|
1941
|
+
doNotSelect: true,
|
|
1942
|
+
doNotSwitchPage: true,
|
|
1943
|
+
doNotPushHistory: true,
|
|
1944
|
+
historySource: step.source,
|
|
1945
|
+
} as const;
|
|
1390
1946
|
|
|
1391
1947
|
switch (step.opType) {
|
|
1392
1948
|
case 'add': {
|
|
1393
|
-
const
|
|
1949
|
+
const items = step.diff ?? [];
|
|
1394
1950
|
if (reverse) {
|
|
1395
1951
|
// 撤销 add:把当时加入的节点删除
|
|
1396
|
-
for (const
|
|
1397
|
-
|
|
1952
|
+
for (const { newSchema } of items) {
|
|
1953
|
+
if (!newSchema) continue;
|
|
1954
|
+
const existing = this.getNodeById(newSchema.id, false);
|
|
1398
1955
|
if (existing) {
|
|
1399
1956
|
await this.remove(existing, commonOpts);
|
|
1400
1957
|
}
|
|
1401
1958
|
}
|
|
1402
1959
|
} else {
|
|
1403
|
-
// 重做 add:按记录的
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
parent: cloneDeep(parent),
|
|
1419
|
-
parentId: parent.id,
|
|
1420
|
-
root: cloneDeep(root),
|
|
1421
|
-
});
|
|
1960
|
+
// 重做 add:按记录的 parentId / index 把节点重新插回父容器。
|
|
1961
|
+
// 按目标 index 升序逐个插入,先小后大避免索引漂移。
|
|
1962
|
+
// 不走 this.add:doAdd 依赖当前选中节点(无选区时直接抛错),且只能插到选中节点之后 / 末尾,
|
|
1963
|
+
// 无法按 step 记录的 parentId / index 精确还原,还会重算 style 破坏已记录的终态位置。
|
|
1964
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1965
|
+
const addedNodes: MNode[] = [];
|
|
1966
|
+
for (const { newSchema, parentId, index } of sorted) {
|
|
1967
|
+
if (!newSchema || parentId === undefined) continue;
|
|
1968
|
+
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1969
|
+
if (parent?.items) {
|
|
1970
|
+
const addedNode = cloneDeep(newSchema);
|
|
1971
|
+
if (typeof index === 'number' && index >= 0 && index < parent.items.length) {
|
|
1972
|
+
parent.items.splice(index, 0, addedNode);
|
|
1973
|
+
} else {
|
|
1974
|
+
parent.items.push(addedNode);
|
|
1422
1975
|
}
|
|
1976
|
+
addedNodes.push(addedNode);
|
|
1977
|
+
await stage?.add({
|
|
1978
|
+
config: cloneDeep(newSchema),
|
|
1979
|
+
parent: cloneDeep(parent),
|
|
1980
|
+
parentId: parent.id,
|
|
1981
|
+
root: cloneDeep(root),
|
|
1982
|
+
});
|
|
1423
1983
|
}
|
|
1424
1984
|
}
|
|
1985
|
+
this.emitHistoryInsertEvents(addedNodes, step.source);
|
|
1425
1986
|
}
|
|
1426
1987
|
break;
|
|
1427
1988
|
}
|
|
1428
1989
|
case 'remove': {
|
|
1429
|
-
const items = step.
|
|
1990
|
+
const items = step.diff ?? [];
|
|
1430
1991
|
if (reverse) {
|
|
1431
|
-
// 撤销 remove:按原 index
|
|
1432
|
-
const sorted = [...items].sort((a, b) => a.index - b.index);
|
|
1433
|
-
|
|
1992
|
+
// 撤销 remove:按原 index 升序逐个插回(先小后大避免索引漂移);不走 this.add 的原因同上
|
|
1993
|
+
const sorted = [...items].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
|
|
1994
|
+
const addedNodes: MNode[] = [];
|
|
1995
|
+
for (const { oldSchema, parentId, index } of sorted) {
|
|
1996
|
+
if (!oldSchema || parentId === undefined) continue;
|
|
1434
1997
|
const parent = this.getNodeById(parentId, false) as MContainer | null;
|
|
1435
1998
|
if (parent?.items) {
|
|
1436
|
-
|
|
1999
|
+
const addedNode = cloneDeep(oldSchema);
|
|
2000
|
+
parent.items.splice(index ?? parent.items.length, 0, addedNode);
|
|
2001
|
+
addedNodes.push(addedNode);
|
|
1437
2002
|
await stage?.add({
|
|
1438
|
-
config: cloneDeep(
|
|
2003
|
+
config: cloneDeep(oldSchema),
|
|
1439
2004
|
parent: cloneDeep(parent),
|
|
1440
2005
|
parentId,
|
|
1441
2006
|
root: cloneDeep(root),
|
|
1442
2007
|
});
|
|
1443
2008
|
}
|
|
1444
2009
|
}
|
|
2010
|
+
this.emitHistoryInsertEvents(addedNodes, step.source);
|
|
1445
2011
|
} else {
|
|
1446
2012
|
// 重做 remove:再删一次
|
|
1447
|
-
for (const {
|
|
1448
|
-
|
|
2013
|
+
for (const { oldSchema } of items) {
|
|
2014
|
+
if (!oldSchema) continue;
|
|
2015
|
+
const existing = this.getNodeById(oldSchema.id, false);
|
|
1449
2016
|
if (existing) {
|
|
1450
2017
|
await this.remove(existing, commonOpts);
|
|
1451
2018
|
}
|
|
@@ -1454,39 +2021,52 @@ class Editor extends BaseService {
|
|
|
1454
2021
|
break;
|
|
1455
2022
|
}
|
|
1456
2023
|
case 'update': {
|
|
1457
|
-
const items = step.
|
|
2024
|
+
const items = step.diff ?? [];
|
|
1458
2025
|
// 优先按 changeRecords 局部 patch:仅触达 propPath 下的字段,避免整节点替换冲掉同节点上其它无关变更。
|
|
1459
2026
|
// 没有 changeRecords 的(如内部 sort/moveLayer/拖动等整节点快照场景)才退化为整节点替换。
|
|
1460
|
-
const configs = items
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
2027
|
+
const configs = items
|
|
2028
|
+
.filter((item) => item.oldSchema && item.newSchema)
|
|
2029
|
+
.map(({ oldSchema, newSchema, changeRecords }) => {
|
|
2030
|
+
const oldNode = oldSchema!;
|
|
2031
|
+
const newNode = newSchema!;
|
|
2032
|
+
if (changeRecords?.length) {
|
|
2033
|
+
const sourceForValues = reverse ? oldNode : newNode;
|
|
2034
|
+
// 仅保留 id / type 作为最小骨架,再按 propPath 写入需要回滚/重做的字段;
|
|
2035
|
+
// 后续 update -> mergeWith 会与现有节点深合并,patch 中未涉及的字段不会被改动。
|
|
2036
|
+
const patch: MNode = { id: newNode.id, type: newNode.type };
|
|
2037
|
+
for (const record of changeRecords) {
|
|
2038
|
+
if (!record.propPath) {
|
|
2039
|
+
// 没有 propPath 视为整节点替换
|
|
2040
|
+
return cloneDeep(sourceForValues);
|
|
2041
|
+
}
|
|
2042
|
+
const value = cloneDeep(getValueByKeyPath(record.propPath, sourceForValues));
|
|
2043
|
+
setValueByKeyPath(record.propPath, value, patch);
|
|
1470
2044
|
}
|
|
1471
|
-
|
|
1472
|
-
setValueByKeyPath(record.propPath, value, patch);
|
|
2045
|
+
return patch;
|
|
1473
2046
|
}
|
|
1474
|
-
return
|
|
1475
|
-
}
|
|
1476
|
-
return cloneDeep(reverse ? oldNode : newNode);
|
|
1477
|
-
});
|
|
2047
|
+
return cloneDeep(reverse ? oldNode : newNode);
|
|
2048
|
+
});
|
|
1478
2049
|
if (configs.length) {
|
|
1479
|
-
await this.update(configs, { doNotPushHistory: true });
|
|
2050
|
+
await this.update(configs, { doNotPushHistory: true, historySource: step.source });
|
|
1480
2051
|
}
|
|
1481
2052
|
break;
|
|
1482
2053
|
}
|
|
1483
2054
|
}
|
|
1484
2055
|
|
|
1485
|
-
this.set('modifiedNodeIds', step.modifiedNodeIds);
|
|
2056
|
+
this.set('modifiedNodeIds', step.extra?.modifiedNodeIds ?? new Map());
|
|
2057
|
+
|
|
2058
|
+
// 还原校验错误标记:因 undo/redo 复用同一 step,需按方向取「操作前 / 操作后」快照——
|
|
2059
|
+
// undo(reverse=true) 还原到操作前的错误状态(撤销一个「校验失败」的改动后错误消失),
|
|
2060
|
+
// redo(reverse=false) 还原到操作后的错误状态(重做后错误恢复)。
|
|
2061
|
+
// 浅拷贝一份以隔离历史快照,避免后续 set/deleteInvalidNode 反向污染 step.extra。
|
|
2062
|
+
const invalidToRestore =
|
|
2063
|
+
(reverse ? step.extra?.invalidNodeIdsBefore : step.extra?.invalidNodeIdsAfter) ?? new Map();
|
|
2064
|
+
this.set('invalidNodeIds', new Map(invalidToRestore));
|
|
2065
|
+
this.emit('invalid-node-change', this.get('invalidNodeIds'));
|
|
1486
2066
|
|
|
1487
2067
|
const page = toRaw(this.get('page'));
|
|
1488
2068
|
if (page) {
|
|
1489
|
-
const selectIds = reverse ? step.selectedBefore : step.selectedAfter;
|
|
2069
|
+
const selectIds = (reverse ? step.extra?.selectedBefore : step.extra?.selectedAfter) ?? [];
|
|
1490
2070
|
setTimeout(() => {
|
|
1491
2071
|
if (!selectIds.length) return;
|
|
1492
2072
|
|
|
@@ -1506,6 +2086,22 @@ class Editor extends BaseService {
|
|
|
1506
2086
|
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|
|
1507
2087
|
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
|
|
1508
2088
|
}
|
|
2089
|
+
|
|
2090
|
+
/**
|
|
2091
|
+
* 撤销 remove / 重做 add 通过手工 splice 按 step 记录的 parentId / index 精确还原节点(不走 this.add,原因见调用处注释),
|
|
2092
|
+
* 这里补齐与 this.add 等价的 add / change 事件,保证撤销/重做路径的事件行为与正向操作一致。
|
|
2093
|
+
*/
|
|
2094
|
+
private emitHistoryInsertEvents(nodes: MNode[], source?: HistoryOpSource) {
|
|
2095
|
+
if (!nodes.length) return;
|
|
2096
|
+
this.emit('add', nodes);
|
|
2097
|
+
this.emit('change', {
|
|
2098
|
+
type: 'add',
|
|
2099
|
+
data: nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
|
2100
|
+
historySource: source,
|
|
2101
|
+
// 撤销/重做本身不再写入历史记录
|
|
2102
|
+
doNotPushHistory: true,
|
|
2103
|
+
});
|
|
2104
|
+
}
|
|
1509
2105
|
}
|
|
1510
2106
|
|
|
1511
2107
|
export type EditorService = Editor;
|