@tmagic/editor 1.7.13-beta.0 → 1.7.14-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/Editor.vue_vue_type_script_setup_true_lang.js +17 -3
- package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +2 -1
- package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +8 -3
- package/dist/es/editorProps.js +1 -0
- package/dist/es/hooks/use-stage.js +5 -0
- package/dist/es/initService.js +3 -0
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +10 -3
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +18 -5
- package/dist/es/layouts/sidebar/layer/use-click.js +22 -3
- package/dist/es/layouts/sidebar/layer/use-drag.js +28 -6
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +13 -2
- package/dist/es/services/editor.js +2 -1
- package/dist/es/services/props.js +12 -0
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/tmagic-editor.umd.cjs +141 -27
- package/package.json +7 -7
- package/src/Editor.vue +15 -0
- package/src/components/Tree.vue +1 -0
- package/src/components/TreeNode.vue +6 -1
- package/src/editorProps.ts +20 -0
- package/src/hooks/use-stage.ts +9 -0
- package/src/initService.ts +10 -0
- package/src/layouts/sidebar/Sidebar.vue +15 -0
- package/src/layouts/sidebar/layer/LayerPanel.vue +26 -2
- package/src/layouts/sidebar/layer/use-click.ts +36 -3
- package/src/layouts/sidebar/layer/use-drag.ts +62 -6
- package/src/layouts/workspace/viewer/Stage.vue +24 -2
- package/src/services/editor.ts +1 -0
- package/src/services/props.ts +16 -0
- package/src/type.ts +44 -0
- package/types/index.d.ts +73 -4
package/src/editorProps.ts
CHANGED
|
@@ -11,6 +11,7 @@ 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,
|
|
@@ -21,6 +22,7 @@ import type {
|
|
|
21
22
|
PageBarSortOptions,
|
|
22
23
|
SideBarData,
|
|
23
24
|
StageRect,
|
|
25
|
+
TreeNodeData,
|
|
24
26
|
} from './type';
|
|
25
27
|
|
|
26
28
|
export interface EditorProps {
|
|
@@ -75,6 +77,11 @@ export interface EditorProps {
|
|
|
75
77
|
guidesOptions?: Partial<GuidesOptions>;
|
|
76
78
|
/** 禁止多选 */
|
|
77
79
|
disabledMultiSelect?: boolean;
|
|
80
|
+
/**
|
|
81
|
+
* 始终启用多选模式:开启后无需按住 Ctrl/Meta,点击即多选;
|
|
82
|
+
* 默认 false。当 `disabledMultiSelect` 为 true 时本配置失效
|
|
83
|
+
*/
|
|
84
|
+
alwaysMultiSelect?: boolean;
|
|
78
85
|
/** 禁用页面片 */
|
|
79
86
|
disabledPageFragment?: boolean;
|
|
80
87
|
/** 禁用双击在浮层中单独编辑选中组件 */
|
|
@@ -101,8 +108,20 @@ export interface EditorProps {
|
|
|
101
108
|
customContentMenu?: CustomContentMenuFunction;
|
|
102
109
|
/** 用于自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态) */
|
|
103
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;
|
|
104
121
|
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
|
105
122
|
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
|
123
|
+
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
|
124
|
+
beforeLayerNodeDblclick?: (event: MouseEvent, data: TreeNodeData) => Promise<boolean | void> | boolean | void;
|
|
106
125
|
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
|
107
126
|
/** 页面顺序拖拽配置参数 */
|
|
108
127
|
pageBarSortOptions?: PageBarSortOptions;
|
|
@@ -113,6 +132,7 @@ export interface EditorProps {
|
|
|
113
132
|
export const defaultEditorProps = {
|
|
114
133
|
renderType: RenderType.IFRAME,
|
|
115
134
|
disabledMultiSelect: false,
|
|
135
|
+
alwaysMultiSelect: false,
|
|
116
136
|
disabledPageFragment: false,
|
|
117
137
|
disabledStageOverlay: false,
|
|
118
138
|
containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME,
|
package/src/hooks/use-stage.ts
CHANGED
|
@@ -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
|
|
package/src/initService.ts
CHANGED
|
@@ -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),
|
|
@@ -162,6 +162,7 @@ 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,
|
|
167
168
|
type IsExpandableFunction,
|
|
@@ -172,6 +173,7 @@ import {
|
|
|
172
173
|
type SideComponent,
|
|
173
174
|
type SideItem,
|
|
174
175
|
SideItemKey,
|
|
176
|
+
type TreeNodeData,
|
|
175
177
|
} from '@editor/type';
|
|
176
178
|
|
|
177
179
|
import CodeBlockListPanel from './code-block/CodeBlockListPanel.vue';
|
|
@@ -185,6 +187,10 @@ defineOptions({
|
|
|
185
187
|
name: 'MEditorSidebar',
|
|
186
188
|
});
|
|
187
189
|
|
|
190
|
+
const emit = defineEmits<{
|
|
191
|
+
'layer-node-dblclick': [event: MouseEvent, data: TreeNodeData];
|
|
192
|
+
}>();
|
|
193
|
+
|
|
188
194
|
const props = withDefaults(
|
|
189
195
|
defineProps<{
|
|
190
196
|
data?: SideBarData;
|
|
@@ -194,6 +200,10 @@ const props = withDefaults(
|
|
|
194
200
|
customContentMenu: CustomContentMenuFunction;
|
|
195
201
|
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
|
196
202
|
layerNodeIsExpandable?: IsExpandableFunction;
|
|
203
|
+
/** 自定义判断当前正在拖动的源是否可以拖入目标节点内部,详见 EditorProps.canDropIn */
|
|
204
|
+
canDropIn?: CanDropInFunction;
|
|
205
|
+
/** 组件树节点双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
|
206
|
+
beforeLayerNodeDblclick?: (_event: MouseEvent, _data: TreeNodeData) => Promise<boolean | void> | boolean | void;
|
|
197
207
|
}>(),
|
|
198
208
|
{
|
|
199
209
|
data: () => ({
|
|
@@ -252,6 +262,11 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
|
|
252
262
|
indent: props.indent,
|
|
253
263
|
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
|
|
254
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),
|
|
255
270
|
},
|
|
256
271
|
component: LayerPanel,
|
|
257
272
|
slots: {},
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
@node-contextmenu="nodeContentMenuHandler"
|
|
21
21
|
@node-mouseenter="mouseenterHandler"
|
|
22
22
|
@node-click="nodeClickHandler"
|
|
23
|
+
@node-dblclick="dblclickHandler"
|
|
23
24
|
>
|
|
24
25
|
<template #tree-node-content="{ data: nodeData }">
|
|
25
26
|
<slot name="layer-node-content" :data="nodeData"> </slot>
|
|
@@ -58,6 +59,7 @@ import Tree from '@editor/components/Tree.vue';
|
|
|
58
59
|
import { useFilter } from '@editor/hooks/use-filter';
|
|
59
60
|
import { useServices } from '@editor/hooks/use-services';
|
|
60
61
|
import type {
|
|
62
|
+
CanDropInFunction,
|
|
61
63
|
CustomContentMenuFunction,
|
|
62
64
|
IsExpandableFunction,
|
|
63
65
|
LayerPanelSlots,
|
|
@@ -79,13 +81,21 @@ defineOptions({
|
|
|
79
81
|
name: 'MEditorLayerPanel',
|
|
80
82
|
});
|
|
81
83
|
|
|
82
|
-
defineProps<{
|
|
84
|
+
const props = defineProps<{
|
|
83
85
|
layerContentMenu: (MenuButton | MenuComponent)[];
|
|
84
86
|
indent?: number;
|
|
85
87
|
nextLevelIndentIncrement?: number;
|
|
86
88
|
customContentMenu: CustomContentMenuFunction;
|
|
87
89
|
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
|
88
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];
|
|
89
99
|
}>();
|
|
90
100
|
|
|
91
101
|
const services = useServices();
|
|
@@ -123,13 +133,27 @@ const collapseAllHandler = () => {
|
|
|
123
133
|
}
|
|
124
134
|
};
|
|
125
135
|
|
|
126
|
-
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
|
+
});
|
|
127
139
|
|
|
128
140
|
// 右键菜单
|
|
129
141
|
const menuRef = useTemplateRef<InstanceType<typeof LayerMenu>>('menu');
|
|
130
142
|
const {
|
|
131
143
|
nodeClickHandler,
|
|
144
|
+
nodeDblclickHandler,
|
|
132
145
|
nodeContentMenuHandler,
|
|
133
146
|
highlightHandler: mouseenterHandler,
|
|
134
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
|
+
};
|
|
135
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(
|
|
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
|
|
|
@@ -11,6 +11,8 @@ const dragState: {
|
|
|
11
11
|
dropType: NodeDropType | '';
|
|
12
12
|
container: HTMLElement | null;
|
|
13
13
|
nodeId?: Id;
|
|
14
|
+
/** canDropIn 返回 Id 时记录的重定向目标 id,handleDragEnd 阶段会改用该 id 对应的节点作为 inner 拖入的父节点 */
|
|
15
|
+
redirectedTargetId?: Id;
|
|
14
16
|
} = {
|
|
15
17
|
dragOverNodeId: '',
|
|
16
18
|
dropType: '',
|
|
@@ -37,12 +39,22 @@ const removeStatusClass = (el: HTMLElement | null) => {
|
|
|
37
39
|
});
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
export interface UseDragOptions {
|
|
43
|
+
/**
|
|
44
|
+
* 用于判断某个节点是否能被拖动到另一个节点内部
|
|
45
|
+
* - 返回 `false`:阻止 inner 拖入(before/after 仍然可用)
|
|
46
|
+
* - 返回 `Id`:将 inner 拖入的目标重定向到该 id 对应的节点
|
|
47
|
+
* - 其他:按原 targetId 正常拖入
|
|
48
|
+
*/
|
|
49
|
+
canDropIn?: (sourceIds: Id[], targetId: Id) => Id | boolean | void;
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
/**
|
|
41
53
|
* dragstart/dragleave/dragend 属于源节点
|
|
42
54
|
* dragover 属于目标节点
|
|
43
55
|
* 这些方法并不是同一个dom事件触发的
|
|
44
56
|
*/
|
|
45
|
-
export const useDrag = ({ editorService }: Services) => {
|
|
57
|
+
export const useDrag = ({ editorService }: Services, options: UseDragOptions = {}) => {
|
|
46
58
|
const handleDragStart = (event: DragEvent) => {
|
|
47
59
|
if (!event.dataTransfer || !event.target || !event.currentTarget) return;
|
|
48
60
|
|
|
@@ -102,13 +114,43 @@ export const useDrag = ({ editorService }: Services) => {
|
|
|
102
114
|
}
|
|
103
115
|
}
|
|
104
116
|
|
|
105
|
-
|
|
117
|
+
// 通过用户配置的钩子判断当前拖动节点是否允许拖入目标节点内部
|
|
118
|
+
// - false:禁止 inner 拖入
|
|
119
|
+
// - Id :将 inner 拖入的父节点重定向为该 id 对应的节点
|
|
120
|
+
// - 其他:按原 targetNodeId 正常拖入
|
|
121
|
+
let canDropInTarget = isContainer;
|
|
122
|
+
let redirectedTargetId: Id | undefined;
|
|
123
|
+
if (canDropInTarget && options.canDropIn && nodeId && targetNodeId !== nodeId) {
|
|
124
|
+
const result = options.canDropIn([nodeId], targetNodeId);
|
|
125
|
+
if (result === false) {
|
|
126
|
+
canDropInTarget = false;
|
|
127
|
+
} else if (typeof result === 'string' || typeof result === 'number') {
|
|
128
|
+
redirectedTargetId = result;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// before/after 模式下新节点会成为 target 的兄弟(即 target 的直接父节点的子节点),
|
|
133
|
+
// 所以应该用 target 的直接父节点 id 再次调用 canDropIn 校验。
|
|
134
|
+
// 若该父节点禁止拖入(false)或要求重定向(Id),都视为"不应放入此父节点",
|
|
135
|
+
// 故对应的 before/after 也禁用——避免绕过 inner 限制。
|
|
136
|
+
let canDropAsSibling = true;
|
|
137
|
+
const directParentId = parentsId?.[parentsId.length - 1];
|
|
138
|
+
if (options.canDropIn && nodeId && directParentId && directParentId !== nodeId) {
|
|
139
|
+
const siblingResult = options.canDropIn([nodeId], directParentId);
|
|
140
|
+
if (siblingResult === false || typeof siblingResult === 'string' || typeof siblingResult === 'number') {
|
|
141
|
+
canDropAsSibling = false;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 显式重置 dropType,避免上一次 dragover 的残留值影响本次判断
|
|
146
|
+
dragState.dropType = '';
|
|
147
|
+
if (distance < targetHeight / 3 && canDropAsSibling) {
|
|
106
148
|
dragState.dropType = 'before';
|
|
107
149
|
addClassName(labelEl, globalThis.document, 'drag-before');
|
|
108
|
-
} else if (distance > (targetHeight * 2) / 3) {
|
|
150
|
+
} else if (distance > (targetHeight * 2) / 3 && canDropAsSibling) {
|
|
109
151
|
dragState.dropType = 'after';
|
|
110
152
|
addClassName(labelEl, globalThis.document, 'drag-after');
|
|
111
|
-
} else if (
|
|
153
|
+
} else if (canDropInTarget) {
|
|
112
154
|
dragState.dropType = 'inner';
|
|
113
155
|
addClassName(labelEl, globalThis.document, 'drag-inner');
|
|
114
156
|
}
|
|
@@ -119,6 +161,8 @@ export const useDrag = ({ editorService }: Services) => {
|
|
|
119
161
|
|
|
120
162
|
dragState.dragOverNodeId = targetNodeId;
|
|
121
163
|
dragState.container = event.currentTarget as HTMLElement;
|
|
164
|
+
// 仅 inner 时才使用重定向,before/after 是相对于 dragOverNodeId 的兄弟插入,重定向无意义
|
|
165
|
+
dragState.redirectedTargetId = dragState.dropType === 'inner' ? redirectedTargetId : undefined;
|
|
122
166
|
|
|
123
167
|
event.preventDefault();
|
|
124
168
|
};
|
|
@@ -158,8 +202,19 @@ export const useDrag = ({ editorService }: Services) => {
|
|
|
158
202
|
let targetIndex = -1;
|
|
159
203
|
|
|
160
204
|
if (Array.isArray(targetNode.items) && dragState.dropType === 'inner') {
|
|
161
|
-
|
|
162
|
-
|
|
205
|
+
// 优先使用 canDropIn 返回的重定向 id 对应的节点作为父节点
|
|
206
|
+
if (dragState.redirectedTargetId !== undefined) {
|
|
207
|
+
const redirectedNode = editorService.getNodeInfo(dragState.redirectedTargetId, false).node;
|
|
208
|
+
if (!redirectedNode || !Array.isArray((redirectedNode as MContainer).items)) {
|
|
209
|
+
// 重定向目标无效或不是容器,放弃此次拖入
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
targetParent = redirectedNode as MContainer;
|
|
213
|
+
targetIndex = (redirectedNode as MContainer).items!.length;
|
|
214
|
+
} else {
|
|
215
|
+
targetIndex = targetNode.items.length;
|
|
216
|
+
targetParent = targetNode as MContainer;
|
|
217
|
+
}
|
|
163
218
|
} else {
|
|
164
219
|
targetIndex = getNodeIndex(dragState.dragOverNodeId, targetParent);
|
|
165
220
|
}
|
|
@@ -180,6 +235,7 @@ export const useDrag = ({ editorService }: Services) => {
|
|
|
180
235
|
dragState.dragOverNodeId = '';
|
|
181
236
|
dragState.dropType = '';
|
|
182
237
|
dragState.container = null;
|
|
238
|
+
dragState.redirectedTargetId = undefined;
|
|
183
239
|
};
|
|
184
240
|
|
|
185
241
|
return {
|
|
@@ -317,12 +317,34 @@ const dropHandler = async (e: DragEvent) => {
|
|
|
317
317
|
);
|
|
318
318
|
|
|
319
319
|
let parent: MContainer | undefined | null = page.value;
|
|
320
|
+
let resolvedParentEl: HTMLElement | null | undefined = parentEl;
|
|
320
321
|
const parentId = getIdFromEl()(parentEl);
|
|
321
322
|
if (parentId) {
|
|
322
323
|
parent = editorService.getNodeById(parentId, false) as MContainer;
|
|
323
324
|
}
|
|
324
325
|
|
|
325
326
|
if (parent && stageContainerEl.value && stage) {
|
|
327
|
+
// 通过用户配置的钩子再次确认当前拖入的新组件是否允许放入命中的高亮容器,
|
|
328
|
+
// 防止 delayedMarkContainer 的延迟/异步未生效或残留高亮导致命中错误容器
|
|
329
|
+
// - 返回 false:取消此次拖入
|
|
330
|
+
// - 返回 Id :将父节点重定向到该 id 对应的节点(layout 坐标也基于其 DOM 重新计算)
|
|
331
|
+
// - 其他 :使用原命中节点
|
|
332
|
+
// 从组件列表拖入新组件时 sourceIds 为空数组(尚无 id)
|
|
333
|
+
if (props.stageOptions.canDropIn) {
|
|
334
|
+
const result = props.stageOptions.canDropIn([], parent.id);
|
|
335
|
+
if (result === false) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (typeof result === 'string' || typeof result === 'number') {
|
|
339
|
+
const redirectedNode = editorService.getNodeById(result, false) as MContainer | undefined;
|
|
340
|
+
if (!redirectedNode) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
parent = redirectedNode;
|
|
344
|
+
resolvedParentEl = stage.renderer?.getTargetElement(result) ?? null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
326
348
|
const layout = await editorService.getLayout(parent);
|
|
327
349
|
|
|
328
350
|
const containerRect = stageContainerEl.value.getBoundingClientRect();
|
|
@@ -342,8 +364,8 @@ const dropHandler = async (e: DragEvent) => {
|
|
|
342
364
|
top = e.clientY - containerRect.top + scrollTop;
|
|
343
365
|
left = e.clientX - containerRect.left + scrollLeft;
|
|
344
366
|
|
|
345
|
-
if (
|
|
346
|
-
const { left: parentLeft, top: parentTop } = getOffset(
|
|
367
|
+
if (resolvedParentEl) {
|
|
368
|
+
const { left: parentLeft, top: parentTop } = getOffset(resolvedParentEl);
|
|
347
369
|
left = left - parentLeft * zoom.value;
|
|
348
370
|
top = top - parentTop * zoom.value;
|
|
349
371
|
}
|
package/src/services/editor.ts
CHANGED
package/src/services/props.ts
CHANGED
|
@@ -93,6 +93,10 @@ class Props extends BaseService {
|
|
|
93
93
|
this.emit('props-configs-change');
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
public getPropsConfigs(): Record<string, FormConfig> {
|
|
97
|
+
return this.state.propsConfigMap;
|
|
98
|
+
}
|
|
99
|
+
|
|
96
100
|
public async fillConfig(config: FormConfig, labelWidth?: string) {
|
|
97
101
|
return fillConfig(config, {
|
|
98
102
|
labelWidth: typeof labelWidth !== 'function' ? labelWidth : '80px',
|
|
@@ -125,12 +129,20 @@ class Props extends BaseService {
|
|
|
125
129
|
return cloneDeep(this.state.propsConfigMap[toLine(type)] || (await this.fillConfig([])));
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
public hasPropsConfig(type: string): boolean {
|
|
133
|
+
return !!this.state.propsConfigMap[toLine(type)];
|
|
134
|
+
}
|
|
135
|
+
|
|
128
136
|
public setPropsValues(values: Record<string, Partial<MNode> | PropsFormValueFunction>) {
|
|
129
137
|
Object.keys(values).forEach((type: string) => {
|
|
130
138
|
this.setPropsValue(toLine(type), values[type]);
|
|
131
139
|
});
|
|
132
140
|
}
|
|
133
141
|
|
|
142
|
+
public getPropsValues(): Record<string, Partial<MNode>> {
|
|
143
|
+
return this.state.propsValueMap;
|
|
144
|
+
}
|
|
145
|
+
|
|
134
146
|
/**
|
|
135
147
|
* 为指点类型组件设置组件初始值
|
|
136
148
|
* @param type 组件类型
|
|
@@ -178,6 +190,10 @@ class Props extends BaseService {
|
|
|
178
190
|
};
|
|
179
191
|
}
|
|
180
192
|
|
|
193
|
+
public hasPropsValue(type: string): boolean {
|
|
194
|
+
return !!this.state.propsValueMap[toLine(type)];
|
|
195
|
+
}
|
|
196
|
+
|
|
181
197
|
public createId(type: string | number): string {
|
|
182
198
|
return `${type}_${guid()}`;
|
|
183
199
|
}
|
package/src/type.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage
|
|
|
26
26
|
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
|
|
27
27
|
import type StageCore from '@tmagic/stage';
|
|
28
28
|
import type {
|
|
29
|
+
CanDropIn,
|
|
29
30
|
ContainerHighlightType,
|
|
30
31
|
CustomizeMoveableOptions,
|
|
31
32
|
GuidesOptions,
|
|
@@ -168,10 +169,23 @@ export interface StageOptions {
|
|
|
168
169
|
moveableOptions?: CustomizeMoveableOptions;
|
|
169
170
|
canSelect?: (el: HTMLElement) => boolean | Promise<boolean>;
|
|
170
171
|
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
|
|
172
|
+
/**
|
|
173
|
+
* 画布上拖入组件(包括从组件列表拖入新组件、画布上拖动已有组件)时,
|
|
174
|
+
* 对已通过 isContainer 命中的候选容器进行二次过滤;返回 false 时阻止该容器被高亮命中
|
|
175
|
+
* - 在画布上拖动已有组件时:sourceIds 为被拖动组件的 id 列表
|
|
176
|
+
* - 从组件列表拖入新组件时:sourceIds 为空数组(尚无 id,仅可依据 targetId 判断)
|
|
177
|
+
* 该选项会被透传给 StageCore 的 canDropIn
|
|
178
|
+
*/
|
|
179
|
+
canDropIn?: CanDropIn;
|
|
171
180
|
updateDragEl?: UpdateDragEl;
|
|
172
181
|
renderType?: RenderType;
|
|
173
182
|
guidesOptions?: Partial<GuidesOptions>;
|
|
174
183
|
disabledMultiSelect?: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
|
|
186
|
+
* 当 `disabledMultiSelect` 为 true 时本配置失效。
|
|
187
|
+
*/
|
|
188
|
+
alwaysMultiSelect?: boolean;
|
|
175
189
|
disabledRule?: boolean;
|
|
176
190
|
zoom?: number;
|
|
177
191
|
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
|
@@ -191,6 +205,8 @@ export interface StoreState {
|
|
|
191
205
|
pageLength: number;
|
|
192
206
|
pageFragmentLength: number;
|
|
193
207
|
disabledMultiSelect: boolean;
|
|
208
|
+
/** 是否始终启用多选模式(无需按住 Ctrl/Meta) */
|
|
209
|
+
alwaysMultiSelect: boolean;
|
|
194
210
|
}
|
|
195
211
|
|
|
196
212
|
export type StoreStateKey = keyof StoreState;
|
|
@@ -682,6 +698,34 @@ export interface TreeNodeData {
|
|
|
682
698
|
/** 判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
|
683
699
|
export type IsExpandableFunction = (_data: TreeNodeData, _nodeStatusMap: Map<Id, LayerNodeStatus>) => boolean;
|
|
684
700
|
|
|
701
|
+
/** canDropIn 的调用场景 */
|
|
702
|
+
export type CanDropInScene =
|
|
703
|
+
/** 在"已选组件"面板的组件树中拖动节点 */
|
|
704
|
+
| 'layer'
|
|
705
|
+
/** 在画布上拖动已有组件(被拖动组件本身已经存在于画布中,sourceIds 包含其 id) */
|
|
706
|
+
| 'stage-drag'
|
|
707
|
+
/** 从组件列表拖入新组件到画布(被拖入的组件尚不存在,sourceIds 为空数组) */
|
|
708
|
+
| 'stage-add';
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* 判断当前正在拖动的源节点是否可以拖入目标节点内部的函数
|
|
712
|
+
* @param _sourceIds 当前正在拖动的源节点 id 列表
|
|
713
|
+
* - `layer`:被拖动的组件树节点 id(单选时长度为 1)
|
|
714
|
+
* - `stage-drag`:被拖动组件的 id 列表(多选拖动时为多个)
|
|
715
|
+
* - `stage-add`:始终为空数组(从组件列表新增的组件尚无 id)
|
|
716
|
+
* @param _targetId 目标容器的节点 id
|
|
717
|
+
* @param _scene 调用场景:见 {@link CanDropInScene}
|
|
718
|
+
* @returns
|
|
719
|
+
* - `false`:阻止该容器被视为合法拖入目标
|
|
720
|
+
* - `layer`:禁用 inner 高亮(before/after 仍然可用)
|
|
721
|
+
* - `stage-drag`:阻止该容器被高亮命中
|
|
722
|
+
* - `stage-add`:阻止该容器被高亮命中并退化为放入当前页面
|
|
723
|
+
* - `Id`(string | number):将拖入目标重定向到该 id 对应的节点
|
|
724
|
+
* (例如把命中的"卡片外壳"节点重定向到其内层"卡片内容"容器节点)
|
|
725
|
+
* - 其他(`true` / `void` / `undefined`):按原 targetId 正常拖入
|
|
726
|
+
*/
|
|
727
|
+
export type CanDropInFunction = (_sourceIds: Id[], _targetId: Id, _scene: CanDropInScene) => Id | boolean | void;
|
|
728
|
+
|
|
685
729
|
export type AsyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
|
|
686
730
|
[K in Value[number]]?: (...args: Parameters<C[K]>) => Promise<Parameters<C[K]>> | Parameters<C[K]>;
|
|
687
731
|
};
|