@tmagic/editor 1.7.13 → 1.7.14-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +21 -3
  2. package/dist/es/components/ScrollViewer.vue_vue_type_script_setup_true_lang.js +1 -1
  3. package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +8 -4
  4. package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +19 -9
  5. package/dist/es/editorProps.js +1 -0
  6. package/dist/es/fields/StyleSetter/Index.vue_vue_type_script_setup_true_lang.js +2 -1
  7. package/dist/es/hooks/use-stage.js +5 -0
  8. package/dist/es/index.js +2 -2
  9. package/dist/es/initService.js +3 -0
  10. package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +1 -1
  11. package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +12 -3
  12. package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +21 -5
  13. package/dist/es/layouts/sidebar/layer/use-click.js +22 -3
  14. package/dist/es/layouts/sidebar/layer/use-drag.js +28 -6
  15. package/dist/es/layouts/workspace/Breadcrumb.vue_vue_type_script_setup_true_lang.js +87 -12
  16. package/dist/es/layouts/workspace/Workspace.vue_vue_type_script_setup_true_lang.js +5 -2
  17. package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +16 -4
  18. package/dist/es/services/editor.js +2 -1
  19. package/dist/es/services/props.js +14 -2
  20. package/dist/es/style.css +20 -0
  21. package/dist/es/utils/dep/worker.js +1 -1
  22. package/dist/es/utils/tree.js +3 -1
  23. package/dist/style.css +20 -0
  24. package/dist/tmagic-editor.umd.cjs +266 -52
  25. package/package.json +7 -7
  26. package/src/Editor.vue +17 -0
  27. package/src/components/ScrollViewer.vue +4 -1
  28. package/src/components/Tree.vue +5 -1
  29. package/src/components/TreeNode.vue +14 -9
  30. package/src/editorProps.ts +23 -0
  31. package/src/fields/StyleSetter/Index.vue +5 -1
  32. package/src/hooks/use-stage.ts +9 -0
  33. package/src/initService.ts +10 -0
  34. package/src/layouts/props-panel/PropsPanel.vue +1 -1
  35. package/src/layouts/sidebar/Sidebar.vue +19 -0
  36. package/src/layouts/sidebar/layer/LayerPanel.vue +37 -3
  37. package/src/layouts/sidebar/layer/use-click.ts +36 -3
  38. package/src/layouts/sidebar/layer/use-drag.ts +62 -6
  39. package/src/layouts/workspace/Breadcrumb.vue +87 -6
  40. package/src/layouts/workspace/Workspace.vue +3 -1
  41. package/src/layouts/workspace/viewer/Stage.vue +31 -3
  42. package/src/services/editor.ts +1 -0
  43. package/src/services/props.ts +18 -2
  44. package/src/theme/breadcrumb.scss +24 -0
  45. package/src/type.ts +58 -1
  46. package/src/utils/tree.ts +5 -1
  47. package/types/index.d.ts +97 -8
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.7.13",
2
+ "version": "1.7.14-beta.1",
3
3
  "name": "@tmagic/editor",
4
4
  "type": "module",
5
5
  "sideEffects": [
@@ -58,11 +58,11 @@
58
58
  "moveable": "^0.53.0",
59
59
  "serialize-javascript": "^7.0.0",
60
60
  "sortablejs": "^1.15.6",
61
- "@tmagic/form": "1.7.13",
62
- "@tmagic/stage": "1.7.13",
63
- "@tmagic/design": "1.7.13",
64
- "@tmagic/table": "1.7.13",
65
- "@tmagic/utils": "1.7.13"
61
+ "@tmagic/design": "1.7.14-beta.1",
62
+ "@tmagic/form": "1.7.14-beta.1",
63
+ "@tmagic/table": "1.7.14-beta.1",
64
+ "@tmagic/stage": "1.7.14-beta.1",
65
+ "@tmagic/utils": "1.7.14-beta.1"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/events": "^3.0.3",
@@ -76,7 +76,7 @@
76
76
  "type-fest": "^5.2.0",
77
77
  "typescript": "^6.0.3",
78
78
  "vue": "^3.5.33",
79
- "@tmagic/core": "1.7.13"
79
+ "@tmagic/core": "1.7.14-beta.1"
80
80
  },
81
81
  "peerDependenciesMeta": {
82
82
  "typescript": {
package/src/Editor.vue CHANGED
@@ -26,6 +26,10 @@
26
26
  :custom-content-menu="customContentMenu"
27
27
  :indent="treeIndent"
28
28
  :next-level-indent-increment="treeNextLevelIndentIncrement"
29
+ :layer-node-is-expandable="layerNodeIsExpandable"
30
+ :can-drop-in="canDropIn"
31
+ :before-layer-node-dblclick="beforeLayerNodeDblclick"
32
+ @layer-node-dblclick="layerNodeDblclickHandler"
29
33
  >
30
34
  <template #layer-panel-header>
31
35
  <slot name="layer-panel-header"></slot>
@@ -85,6 +89,7 @@
85
89
  :stage-content-menu="stageContentMenu"
86
90
  :custom-content-menu="customContentMenu"
87
91
  >
92
+ <template #stage-top><slot name="stage-top"></slot></template>
88
93
  <template #stage><slot name="stage"></slot></template>
89
94
  <template #workspace-content><slot name="workspace-content" :editorService="editorService"></slot></template>
90
95
  </Workspace>
@@ -154,6 +159,7 @@ import uiService from './services/ui';
154
159
  import keybindingConfig from './utils/keybinding-config';
155
160
  import { defaultEditorProps, EditorProps } from './editorProps';
156
161
  import { initServiceEvents, initServiceState } from './initService';
162
+ import type { TreeNodeData } from './type';
157
163
  import type { EditorSlots, EventBus, Services, StageOptions } from './type';
158
164
 
159
165
  defineSlots<EditorSlots>();
@@ -168,6 +174,7 @@ const emit = defineEmits<{
168
174
  'update:modelValue': [value: MApp | null];
169
175
  'props-form-error': [e: any];
170
176
  'props-submit-error': [e: any];
177
+ 'layer-node-dblclick': [event: MouseEvent, data: TreeNodeData];
171
178
  }>();
172
179
 
173
180
  const props = withDefaults(defineProps<EditorProps>(), defaultEditorProps);
@@ -200,6 +207,11 @@ const stageOptions: StageOptions = {
200
207
  canSelect: props.canSelect,
201
208
  updateDragEl: props.updateDragEl,
202
209
  isContainer: props.isContainer,
210
+ // sourceIds 为空表示从组件列表新增(尚无 id),否则是画布上拖动已有组件
211
+ canDropIn: props.canDropIn
212
+ ? (sourceIds, targetId) =>
213
+ props.canDropIn!(sourceIds, targetId, sourceIds.length === 0 ? 'stage-add' : 'stage-drag')
214
+ : undefined,
203
215
  containerHighlightClassName: props.containerHighlightClassName,
204
216
  containerHighlightDuration: props.containerHighlightDuration,
205
217
  containerHighlightType: props.containerHighlightType,
@@ -207,6 +219,7 @@ const stageOptions: StageOptions = {
207
219
  renderType: props.renderType,
208
220
  guidesOptions: props.guidesOptions,
209
221
  disabledMultiSelect: props.disabledMultiSelect,
222
+ alwaysMultiSelect: props.alwaysMultiSelect,
210
223
  beforeDblclick: props.beforeDblclick,
211
224
  };
212
225
 
@@ -234,5 +247,9 @@ const propsPanelFormErrorHandler = (e: any) => {
234
247
  emit('props-form-error', e);
235
248
  };
236
249
 
250
+ const layerNodeDblclickHandler = (event: MouseEvent, data: TreeNodeData) => {
251
+ emit('layer-node-dblclick', event, data);
252
+ };
253
+
237
254
  defineExpose(services);
238
255
  </script>
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <div class="m-editor-scroll-viewer-container" ref="container">
3
3
  <div ref="target" :style="style">
4
+ <slot name="before"></slot>
4
5
  <slot></slot>
5
6
  </div>
6
7
 
@@ -29,7 +30,7 @@ import { computed, onBeforeUnmount, onMounted, ref, useTemplateRef, watch } from
29
30
 
30
31
  import { isNumber } from '@tmagic/utils';
31
32
 
32
- import type { ScrollViewerEvent } from '@editor/type';
33
+ import type { ScrollViewerEvent, ScrollViewerSlots } from '@editor/type';
33
34
  import { ScrollViewer } from '@editor/utils/scroll-viewer';
34
35
 
35
36
  import ScrollBar from './ScrollBar.vue';
@@ -38,6 +39,8 @@ defineOptions({
38
39
  name: 'MEditorScrollViewer',
39
40
  });
40
41
 
42
+ defineSlots<ScrollViewerSlots>();
43
+
41
44
  const props = withDefaults(
42
45
  defineProps<{
43
46
  width?: number | string;
@@ -8,6 +8,7 @@
8
8
  :indent="indent"
9
9
  :next-level-indent-increment="nextLevelIndentIncrement"
10
10
  :node-status-map="nodeStatusMap"
11
+ :is-expandable="isExpandable"
11
12
  >
12
13
  <template #tree-node-content="{ data: nodeData }">
13
14
  <slot name="tree-node-content" :data="nodeData"> </slot>
@@ -33,7 +34,7 @@ import { provide } from 'vue';
33
34
 
34
35
  import type { Id } from '@tmagic/core';
35
36
 
36
- import type { LayerNodeStatus, TreeNodeData } from '@editor/type';
37
+ import type { IsExpandableFunction, LayerNodeStatus, TreeNodeData } from '@editor/type';
37
38
 
38
39
  import TreeNode from './TreeNode.vue';
39
40
 
@@ -55,6 +56,7 @@ const emit = defineEmits<{
55
56
  'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
56
57
  'node-mouseenter': [event: MouseEvent, data: TreeNodeData];
57
58
  'node-click': [event: MouseEvent, data: TreeNodeData];
59
+ 'node-dblclick': [event: MouseEvent, data: TreeNodeData];
58
60
  }>();
59
61
 
60
62
  provide('treeEmit', emit);
@@ -66,6 +68,8 @@ withDefaults(
66
68
  indent?: number;
67
69
  nextLevelIndentIncrement?: number;
68
70
  emptyText?: string;
71
+ /** 自定义判断节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
72
+ isExpandable?: IsExpandableFunction;
69
73
  }>(),
70
74
  {
71
75
  indent: 0,
@@ -20,12 +20,12 @@
20
20
  >
21
21
  <MIcon
22
22
  class="expand-icon"
23
- :style="hasChildren ? '' : 'color: transparent; cursor: default'"
23
+ :style="isExpandable(data, nodeStatusMap) ? '' : 'color: transparent; cursor: default'"
24
24
  :icon="expanded ? ArrowDown : ArrowRight"
25
25
  @click="expandHandler"
26
26
  ></MIcon>
27
27
 
28
- <div class="tree-node-content" @click="nodeClickHandler">
28
+ <div class="tree-node-content" @click="nodeClickHandler" @dblclick="nodeDblclickHandler">
29
29
  <slot name="tree-node-content" :data="data">
30
30
  <div class="tree-node-label">
31
31
  <slot name="tree-node-label" :data="data">{{ `${data.name} (${data.id})` }}</slot>
@@ -37,7 +37,7 @@
37
37
  </div>
38
38
  </div>
39
39
 
40
- <div v-if="hasChildren && expanded" class="m-editor-tree-node-children">
40
+ <div v-if="isExpandable(data, nodeStatusMap) && expanded" class="m-editor-tree-node-children">
41
41
  <TreeNode
42
42
  v-for="item in data.items"
43
43
  :key="item.id"
@@ -46,6 +46,7 @@
46
46
  :parentsId="[...parentsId, data.id]"
47
47
  :node-status-map="nodeStatusMap"
48
48
  :indent="indent + nextLevelIndentIncrement"
49
+ :is-expandable="isExpandable"
49
50
  >
50
51
  <template #tree-node-content="{ data: nodeData }">
51
52
  <slot name="tree-node-content" :data="nodeData"> </slot>
@@ -68,8 +69,8 @@ import { ArrowDown, ArrowRight } from '@element-plus/icons-vue';
68
69
  import type { Id } from '@tmagic/core';
69
70
 
70
71
  import MIcon from '@editor/components/Icon.vue';
71
- import type { LayerNodeStatus, TreeNodeData } from '@editor/type';
72
- import { updateStatus } from '@editor/utils/tree';
72
+ import type { IsExpandableFunction, LayerNodeStatus, TreeNodeData } from '@editor/type';
73
+ import { defaultIsExpandable, updateStatus } from '@editor/utils/tree';
73
74
 
74
75
  defineSlots<{
75
76
  'tree-node-label'(_props: { data: TreeNodeData }): any;
@@ -88,6 +89,7 @@ const emit = defineEmits<{
88
89
  'node-contextmenu': [event: MouseEvent, data: TreeNodeData];
89
90
  'node-mouseenter': [event: MouseEvent, data: TreeNodeData];
90
91
  'node-click': [event: MouseEvent, data: TreeNodeData];
92
+ 'node-dblclick': [event: MouseEvent, data: TreeNodeData];
91
93
  }>();
92
94
 
93
95
  const treeEmit = inject<typeof emit>('treeEmit');
@@ -100,11 +102,14 @@ const props = withDefaults(
100
102
  nodeStatusMap: Map<Id, LayerNodeStatus>;
101
103
  indent?: number;
102
104
  nextLevelIndentIncrement?: number;
105
+ /** 自定义判断节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
106
+ isExpandable?: IsExpandableFunction;
103
107
  }>(),
104
108
  {
105
109
  indent: 0,
106
110
  nextLevelIndentIncrement: 11,
107
111
  parentsId: () => [],
112
+ isExpandable: defaultIsExpandable,
108
113
  },
109
114
  );
110
115
 
@@ -123,10 +128,6 @@ const selected = computed(() => nodeStatus.value.selected);
123
128
  const visible = computed(() => nodeStatus.value.visible);
124
129
  const draggable = computed(() => nodeStatus.value.draggable);
125
130
 
126
- const hasChildren = computed(
127
- () => Array.isArray(props.data.items) && props.data.items.some((item) => props.nodeStatusMap.get(item.id)?.visible),
128
- );
129
-
130
131
  const handleDragStart = (event: DragEvent) => {
131
132
  treeEmit?.('node-dragstart', event, props.data);
132
133
  };
@@ -156,4 +157,8 @@ const expandHandler = () => {
156
157
  const nodeClickHandler = (event: MouseEvent) => {
157
158
  treeEmit?.('node-click', event, props.data);
158
159
  };
160
+
161
+ const nodeDblclickHandler = (event: MouseEvent) => {
162
+ treeEmit?.('node-dblclick', event, props.data);
163
+ };
159
164
  </script>
@@ -11,15 +11,18 @@ import StageCore, {
11
11
  import { getIdFromEl } from '@tmagic/utils';
12
12
 
13
13
  import type {
14
+ CanDropInFunction,
14
15
  ComponentGroup,
15
16
  CustomContentMenuFunction,
16
17
  DatasourceTypeOption,
18
+ IsExpandableFunction,
17
19
  MenuBarData,
18
20
  MenuButton,
19
21
  MenuComponent,
20
22
  PageBarSortOptions,
21
23
  SideBarData,
22
24
  StageRect,
25
+ TreeNodeData,
23
26
  } from './type';
24
27
 
25
28
  export interface EditorProps {
@@ -74,6 +77,11 @@ export interface EditorProps {
74
77
  guidesOptions?: Partial<GuidesOptions>;
75
78
  /** 禁止多选 */
76
79
  disabledMultiSelect?: boolean;
80
+ /**
81
+ * 始终启用多选模式:开启后无需按住 Ctrl/Meta,点击即多选;
82
+ * 默认 false。当 `disabledMultiSelect` 为 true 时本配置失效
83
+ */
84
+ alwaysMultiSelect?: boolean;
77
85
  /** 禁用页面片 */
78
86
  disabledPageFragment?: boolean;
79
87
  /** 禁用双击在浮层中单独编辑选中组件 */
@@ -98,8 +106,22 @@ export interface EditorProps {
98
106
  isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
99
107
  /** 用于自定义组件树与画布的右键菜单 */
100
108
  customContentMenu?: CustomContentMenuFunction;
109
+ /** 用于自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态) */
110
+ layerNodeIsExpandable?: IsExpandableFunction;
111
+ /**
112
+ * 用于自定义判断当前正在拖动的源是否可以拖入目标节点内部
113
+ *
114
+ * 同时覆盖以下两类场景,通过第三个参数 scene 区分:
115
+ * - `'layer'` :"已选组件"面板组件树拖动(返回 false 时仅禁用 inner,不影响 before/after)
116
+ * - `'stage'`:画布拖入组件(返回 false 时阻止该容器被高亮命中;适用于"组件列表拖入新组件"和"画布上拖动已有组件"两种细分情况)
117
+ *
118
+ * 注意:layer 场景目前只识别同步返回值;返回 Promise 时会按 true 处理(即允许)
119
+ */
120
+ canDropIn?: CanDropInFunction;
101
121
  /** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
102
122
  beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
123
+ /** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
124
+ beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void;
103
125
  extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
104
126
  /** 页面顺序拖拽配置参数 */
105
127
  pageBarSortOptions?: PageBarSortOptions;
@@ -110,6 +132,7 @@ export interface EditorProps {
110
132
  export const defaultEditorProps = {
111
133
  renderType: RenderType.IFRAME,
112
134
  disabledMultiSelect: false,
135
+ alwaysMultiSelect: false,
113
136
  disabledPageFragment: false,
114
137
  disabledStageOverlay: false,
115
138
  containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME,
@@ -74,7 +74,11 @@ const collapseValue = shallowRef(
74
74
 
75
75
  const change = (v: any, eventData: ContainerChangeEventData) => {
76
76
  eventData.changeRecords?.forEach((record) => {
77
- record.propPath = `${props.name}.${record.propPath}`;
77
+ if (props.prop) {
78
+ record.propPath = `${props.prop}.${record.propPath}`;
79
+ } else if (props.name) {
80
+ record.propPath = `${props.name}.${record.propPath}`;
81
+ }
78
82
  });
79
83
  emit('change', v, eventData);
80
84
  };
@@ -24,6 +24,7 @@ export const useStage = (stageOptions: StageOptions) => {
24
24
  zoom: stageOptions.zoom ?? zoom.value,
25
25
  autoScrollIntoView: stageOptions.autoScrollIntoView,
26
26
  isContainer: stageOptions.isContainer,
27
+ canDropIn: stageOptions.canDropIn,
27
28
  containerHighlightClassName: stageOptions.containerHighlightClassName,
28
29
  containerHighlightDuration: stageOptions.containerHighlightDuration,
29
30
  containerHighlightType: stageOptions.containerHighlightType,
@@ -45,6 +46,7 @@ export const useStage = (stageOptions: StageOptions) => {
45
46
  updateDragEl: stageOptions.updateDragEl,
46
47
  guidesOptions: stageOptions.guidesOptions,
47
48
  disabledMultiSelect: stageOptions.disabledMultiSelect,
49
+ alwaysMultiSelect: stageOptions.alwaysMultiSelect,
48
50
  disabledRule: stageOptions.disabledRule,
49
51
  });
50
52
 
@@ -59,6 +61,13 @@ export const useStage = (stageOptions: StageOptions) => {
59
61
  },
60
62
  );
61
63
 
64
+ watch(
65
+ () => editorService.get('alwaysMultiSelect'),
66
+ (alwaysMultiSelect) => {
67
+ stage.setAlwaysMultiSelect(Boolean(alwaysMultiSelect));
68
+ },
69
+ );
70
+
62
71
  const hGuidesCache = getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY));
63
72
  const vGuidesCache = getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY));
64
73
 
@@ -72,6 +72,16 @@ export const initServiceState = (
72
72
  },
73
73
  );
74
74
 
75
+ watch(
76
+ () => props.alwaysMultiSelect,
77
+ (alwaysMultiSelect) => {
78
+ editorService.set('alwaysMultiSelect', alwaysMultiSelect || false);
79
+ },
80
+ {
81
+ immediate: true,
82
+ },
83
+ );
84
+
75
85
  watch(
76
86
  () => props.componentGroupList,
77
87
  (componentGroupList) => componentGroupList && componentListService.setList(componentGroupList),
@@ -115,7 +115,7 @@ const init = async () => {
115
115
  }
116
116
 
117
117
  const type = node.value.type || (node.value.items ? 'container' : 'text');
118
- curFormConfig.value = await propsService.getPropsConfig(type);
118
+ curFormConfig.value = await propsService.getPropsConfig(type, { node: node.value });
119
119
  values.value = node.value;
120
120
  };
121
121
 
@@ -162,8 +162,10 @@ import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height'
162
162
  import { useFloatBox } from '@editor/hooks/use-float-box';
163
163
  import { useServices } from '@editor/hooks/use-services';
164
164
  import {
165
+ type CanDropInFunction,
165
166
  ColumnLayout,
166
167
  CustomContentMenuFunction,
168
+ type IsExpandableFunction,
167
169
  type MenuButton,
168
170
  type MenuComponent,
169
171
  type SideBarData,
@@ -171,6 +173,7 @@ import {
171
173
  type SideComponent,
172
174
  type SideItem,
173
175
  SideItemKey,
176
+ type TreeNodeData,
174
177
  } from '@editor/type';
175
178
 
176
179
  import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
@@ -184,6 +187,10 @@ defineOptions({
184
187
  name: 'MEditorSidebar',
185
188
  });
186
189
 
190
+ const emit = defineEmits<{
191
+ 'layer-node-dblclick': [event: MouseEvent, data: TreeNodeData];
192
+ }>();
193
+
187
194
  const props = withDefaults(
188
195
  defineProps<{
189
196
  data?: SideBarData;
@@ -191,6 +198,12 @@ const props = withDefaults(
191
198
  indent?: number;
192
199
  nextLevelIndentIncrement?: number;
193
200
  customContentMenu: CustomContentMenuFunction;
201
+ /** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
202
+ layerNodeIsExpandable?: IsExpandableFunction;
203
+ /** 自定义判断当前正在拖动的源是否可以拖入目标节点内部,详见 EditorProps.canDropIn */
204
+ canDropIn?: CanDropInFunction;
205
+ /** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
206
+ beforeLayerNodeDblclick?: (_event: MouseEvent, _data: TreeNodeData) => Promise<boolean | void> | boolean | void;
194
207
  }>(),
195
208
  {
196
209
  data: () => ({
@@ -248,6 +261,12 @@ const getItemConfig = (data: SideItem): SideComponent => {
248
261
  customContentMenu: props.customContentMenu,
249
262
  indent: props.indent,
250
263
  nextLevelIndentIncrement: props.nextLevelIndentIncrement,
264
+ isExpandable: props.layerNodeIsExpandable,
265
+ canDropIn: props.canDropIn,
266
+ beforeNodeDblclick: props.beforeLayerNodeDblclick,
267
+ },
268
+ listeners: {
269
+ 'node-dblclick': (event: MouseEvent, nodeData: TreeNodeData) => emit('layer-node-dblclick', event, nodeData),
251
270
  },
252
271
  component: LayerPanel,
253
272
  slots: {},
@@ -12,6 +12,7 @@
12
12
  :node-status-map="nodeStatusMap"
13
13
  :indent="indent"
14
14
  :next-level-indent-increment="nextLevelIndentIncrement"
15
+ :is-expandable="isExpandable"
15
16
  @node-dragover="handleDragOver"
16
17
  @node-dragstart="handleDragStart"
17
18
  @node-dragleave="handleDragLeave"
@@ -19,6 +20,7 @@
19
20
  @node-contextmenu="nodeContentMenuHandler"
20
21
  @node-mouseenter="mouseenterHandler"
21
22
  @node-click="nodeClickHandler"
23
+ @node-dblclick="dblclickHandler"
22
24
  >
23
25
  <template #tree-node-content="{ data: nodeData }">
24
26
  <slot name="layer-node-content" :data="nodeData"> </slot>
@@ -56,7 +58,15 @@ import SearchInput from '@editor/components/SearchInput.vue';
56
58
  import Tree from '@editor/components/Tree.vue';
57
59
  import { useFilter } from '@editor/hooks/use-filter';
58
60
  import { useServices } from '@editor/hooks/use-services';
59
- import type { CustomContentMenuFunction, LayerPanelSlots, MenuButton, MenuComponent, TreeNodeData } from '@editor/type';
61
+ import type {
62
+ CanDropInFunction,
63
+ CustomContentMenuFunction,
64
+ IsExpandableFunction,
65
+ LayerPanelSlots,
66
+ MenuButton,
67
+ MenuComponent,
68
+ TreeNodeData,
69
+ } from '@editor/type';
60
70
 
61
71
  import LayerMenu from './LayerMenu.vue';
62
72
  import LayerNodeTool from './LayerNodeTool.vue';
@@ -71,11 +81,21 @@ defineOptions({
71
81
  name: 'MEditorLayerPanel',
72
82
  });
73
83
 
74
- defineProps<{
84
+ const props = defineProps<{
75
85
  layerContentMenu: (MenuButton | MenuComponent)[];
76
86
  indent?: number;
77
87
  nextLevelIndentIncrement?: number;
78
88
  customContentMenu: CustomContentMenuFunction;
89
+ /** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
90
+ isExpandable?: IsExpandableFunction;
91
+ /** 自定义判断当前拖动节点是否可以拖入目标节点内部的函数,返回 false 则禁止拖入 */
92
+ canDropIn?: CanDropInFunction;
93
+ /** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
94
+ beforeNodeDblclick?: (_event: MouseEvent, _data: TreeNodeData) => Promise<boolean | void> | boolean | void;
95
+ }>();
96
+
97
+ const emit = defineEmits<{
98
+ 'node-dblclick': [event: MouseEvent, data: TreeNodeData];
79
99
  }>();
80
100
 
81
101
  const services = useServices();
@@ -113,13 +133,27 @@ const collapseAllHandler = () => {
113
133
  }
114
134
  };
115
135
 
116
- const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useDrag(services);
136
+ const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useDrag(services, {
137
+ canDropIn: (sourceIds, targetId) => props.canDropIn?.(sourceIds, targetId, 'layer'),
138
+ });
117
139
 
118
140
  // 右键菜单
119
141
  const menuRef = useTemplateRef<InstanceType<typeof LayerMenu>>('menu');
120
142
  const {
121
143
  nodeClickHandler,
144
+ nodeDblclickHandler,
122
145
  nodeContentMenuHandler,
123
146
  highlightHandler: mouseenterHandler,
124
147
  } = useClick(services, isCtrlKeyDown, nodeStatusMap, menuRef);
148
+
149
+ const dblclickHandler = async (event: MouseEvent, data: TreeNodeData) => {
150
+ if (props.beforeNodeDblclick) {
151
+ const result = await props.beforeNodeDblclick(event, data);
152
+ if (result === false) return;
153
+ }
154
+
155
+ nodeDblclickHandler(event, data);
156
+
157
+ emit('node-dblclick', event, data);
158
+ };
125
159
  </script>
@@ -2,7 +2,7 @@ import { computed, type ComputedRef, nextTick, type Ref, type ShallowRef } from
2
2
  import { throttle } from 'lodash-es';
3
3
 
4
4
  import { Id, MNode } from '@tmagic/core';
5
- import { isPage, isPageFragment } from '@tmagic/utils';
5
+ import { getElById, isPage, isPageFragment } from '@tmagic/utils';
6
6
 
7
7
  import type { LayerNodeStatus, Services, TreeNodeData } from '@editor/type';
8
8
  import { UI_SELECT_MODE_EVENT_NAME } from '@editor/utils/const';
@@ -16,7 +16,9 @@ export const useClick = (
16
16
  nodeStatusMap: ComputedRef<Map<Id, LayerNodeStatus> | undefined>,
17
17
  menuRef: ShallowRef<InstanceType<typeof LayerMenu> | null>,
18
18
  ) => {
19
- const isMultiSelect = computed(() => isCtrlKeyDown.value && !editorService.get('disabledMultiSelect'));
19
+ const isMultiSelect = computed(
20
+ () => !editorService.get('disabledMultiSelect') && (isCtrlKeyDown.value || editorService.get('alwaysMultiSelect')),
21
+ );
20
22
 
21
23
  // 触发画布选中
22
24
  const select = async (data: MNode) => {
@@ -81,6 +83,19 @@ export const useClick = (
81
83
  stageOverlayService.get('stage')?.highlight(data.id);
82
84
  };
83
85
 
86
+ const isNodeCanSelect = async (data: TreeNodeData) => {
87
+ const canSelect = stageOverlayService.get('stageOptions')?.canSelect;
88
+ if (!canSelect) return true;
89
+
90
+ const doc = editorService.get('stage')?.renderer?.contentWindow?.document;
91
+ if (!doc) return true;
92
+
93
+ const el = getElById()(doc, data.id);
94
+ if (!el) return true;
95
+
96
+ return Boolean(await canSelect(el));
97
+ };
98
+
84
99
  const nodeClickHandler = (event: MouseEvent, data: TreeNodeData): void => {
85
100
  if (!nodeStatusMap?.value) return;
86
101
 
@@ -95,16 +110,34 @@ export const useClick = (
95
110
  });
96
111
  }
97
112
 
98
- nextTick(() => {
113
+ nextTick(async () => {
114
+ if (!(await isNodeCanSelect(data))) return;
115
+
99
116
  select(data);
100
117
  });
101
118
  };
102
119
 
120
+ const nodeDblclickHandler = (event: MouseEvent, data: TreeNodeData): void => {
121
+ if (!nodeStatusMap?.value) return;
122
+
123
+ if (uiService.get('uiSelectMode')) return;
124
+
125
+ // 双击切换展开/收起状态
126
+ if (data.items && data.items.length > 0) {
127
+ const status = nodeStatusMap.value.get(data.id);
128
+ updateStatus(nodeStatusMap.value, data.id, {
129
+ expand: !status?.expand,
130
+ });
131
+ }
132
+ };
133
+
103
134
  return {
104
135
  menuRef,
105
136
 
106
137
  nodeClickHandler,
107
138
 
139
+ nodeDblclickHandler,
140
+
108
141
  nodeContentMenuHandler(event: MouseEvent, data: TreeNodeData): void {
109
142
  event.preventDefault();
110
143