@tmagic/editor 1.7.13 → 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 +21 -3
- package/dist/es/components/ScrollViewer.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/components/Tree.vue_vue_type_script_setup_true_lang.js +8 -4
- package/dist/es/components/TreeNode.vue_vue_type_script_setup_true_lang.js +19 -9
- package/dist/es/editorProps.js +1 -0
- package/dist/es/hooks/use-stage.js +5 -0
- package/dist/es/index.js +2 -2
- package/dist/es/initService.js +3 -0
- package/dist/es/layouts/props-panel/PropsPanel.vue_vue_type_script_setup_true_lang.js +1 -1
- package/dist/es/layouts/sidebar/Sidebar.vue_vue_type_script_setup_true_lang.js +12 -3
- package/dist/es/layouts/sidebar/layer/LayerPanel.vue_vue_type_script_setup_true_lang.js +21 -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/Breadcrumb.vue_vue_type_script_setup_true_lang.js +87 -12
- package/dist/es/layouts/workspace/Workspace.vue_vue_type_script_setup_true_lang.js +5 -2
- package/dist/es/layouts/workspace/viewer/Stage.vue_vue_type_script_setup_true_lang.js +16 -4
- package/dist/es/services/editor.js +2 -1
- package/dist/es/services/props.js +14 -2
- package/dist/es/style.css +20 -0
- package/dist/es/utils/dep/worker.js +1 -1
- package/dist/es/utils/tree.js +3 -1
- package/dist/style.css +20 -0
- package/dist/tmagic-editor.umd.cjs +264 -51
- package/package.json +7 -7
- package/src/Editor.vue +17 -0
- package/src/components/ScrollViewer.vue +4 -1
- package/src/components/Tree.vue +5 -1
- package/src/components/TreeNode.vue +14 -9
- package/src/editorProps.ts +23 -0
- package/src/hooks/use-stage.ts +9 -0
- package/src/initService.ts +10 -0
- package/src/layouts/props-panel/PropsPanel.vue +1 -1
- package/src/layouts/sidebar/Sidebar.vue +19 -0
- package/src/layouts/sidebar/layer/LayerPanel.vue +37 -3
- package/src/layouts/sidebar/layer/use-click.ts +36 -3
- package/src/layouts/sidebar/layer/use-drag.ts +62 -6
- package/src/layouts/workspace/Breadcrumb.vue +87 -6
- package/src/layouts/workspace/Workspace.vue +3 -1
- package/src/layouts/workspace/viewer/Stage.vue +31 -3
- package/src/services/editor.ts +1 -0
- package/src/services/props.ts +18 -2
- package/src/theme/breadcrumb.scss +24 -0
- package/src/type.ts +58 -1
- package/src/utils/tree.ts +5 -1
- package/types/index.d.ts +97 -8
|
@@ -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 {
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div v-if="nodes.length === 1" class="m-editor-breadcrumb">
|
|
3
|
-
<template v-for="(item, index) in
|
|
4
|
-
<
|
|
5
|
-
|
|
2
|
+
<div v-if="nodes.length === 1" ref="containerRef" class="m-editor-breadcrumb">
|
|
3
|
+
<template v-for="(item, index) in displayPath" :key="item.isEllipsis ? `ellipsis-${index}` : item.id">
|
|
4
|
+
<span v-if="item.isEllipsis" class="m-editor-breadcrumb-ellipsis">...</span>
|
|
5
|
+
<TMagicTooltip v-else :content="item.name" placement="top" :show-after="500">
|
|
6
|
+
<TMagicButton class="m-editor-breadcrumb-item" link :disabled="item.id === node?.id" @click="select(item)">{{
|
|
7
|
+
item.name
|
|
8
|
+
}}</TMagicButton>
|
|
9
|
+
</TMagicTooltip>
|
|
10
|
+
<span v-if="index < displayPath.length - 1" class="m-editor-breadcrumb-separator">/</span>
|
|
6
11
|
</template>
|
|
7
12
|
</div>
|
|
8
13
|
</template>
|
|
9
14
|
|
|
10
15
|
<script setup lang="ts">
|
|
11
|
-
import { computed } from 'vue';
|
|
16
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
|
12
17
|
|
|
13
18
|
import type { MNode } from '@tmagic/core';
|
|
14
|
-
import { TMagicButton } from '@tmagic/design';
|
|
19
|
+
import { TMagicButton, TMagicTooltip } from '@tmagic/design';
|
|
15
20
|
import { getNodePath } from '@tmagic/utils';
|
|
16
21
|
|
|
17
22
|
import { useServices } from '@editor/hooks/use-services';
|
|
@@ -20,6 +25,8 @@ defineOptions({
|
|
|
20
25
|
name: 'MEditorBreadcrumb',
|
|
21
26
|
});
|
|
22
27
|
|
|
28
|
+
type DisplayItem = (MNode & { isEllipsis?: false }) | { isEllipsis: true; id: string; name: string };
|
|
29
|
+
|
|
23
30
|
const { editorService } = useServices();
|
|
24
31
|
|
|
25
32
|
const node = computed(() => editorService.get('node'));
|
|
@@ -27,6 +34,80 @@ const nodes = computed(() => editorService.get('nodes'));
|
|
|
27
34
|
const root = computed(() => editorService.get('root'));
|
|
28
35
|
const path = computed(() => getNodePath(node.value?.id || '', root.value?.items || []));
|
|
29
36
|
|
|
37
|
+
const containerRef = ref<HTMLElement | null>(null);
|
|
38
|
+
// 当面包屑宽度超过父元素宽度的阈值时折叠
|
|
39
|
+
const COLLAPSE_RATIO = 0.8;
|
|
40
|
+
const collapsed = ref(false);
|
|
41
|
+
|
|
42
|
+
const displayPath = computed<DisplayItem[]>(() => {
|
|
43
|
+
const list = path.value;
|
|
44
|
+
// 折叠后视觉元素数(first + ... + last2 = 4 个),所以只有路径 > 3 时折叠才能减少占位
|
|
45
|
+
if (!collapsed.value || list.length <= 3) {
|
|
46
|
+
return list as DisplayItem[];
|
|
47
|
+
}
|
|
48
|
+
return [
|
|
49
|
+
list[0],
|
|
50
|
+
{ isEllipsis: true, id: '__ellipsis__', name: '...' },
|
|
51
|
+
list[list.length - 2],
|
|
52
|
+
list[list.length - 1],
|
|
53
|
+
] as DisplayItem[];
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const measureOverflow = async () => {
|
|
57
|
+
// 先恢复完整渲染再测量,避免折叠后误判
|
|
58
|
+
if (collapsed.value) {
|
|
59
|
+
collapsed.value = false;
|
|
60
|
+
await nextTick();
|
|
61
|
+
}
|
|
62
|
+
const el = containerRef.value;
|
|
63
|
+
const parent = el?.parentElement;
|
|
64
|
+
if (!el || !parent) return;
|
|
65
|
+
// scrollWidth 取内容自然宽度(不受自身 max-width 影响),与父容器宽度做比例判断
|
|
66
|
+
const contentWidth = el.scrollWidth;
|
|
67
|
+
const parentWidth = parent.clientWidth;
|
|
68
|
+
if (parentWidth <= 0) return;
|
|
69
|
+
collapsed.value = contentWidth > parentWidth * COLLAPSE_RATIO;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
let resizeObserver: ResizeObserver | null = null;
|
|
73
|
+
|
|
74
|
+
const observe = () => {
|
|
75
|
+
resizeObserver?.disconnect();
|
|
76
|
+
const el = containerRef.value;
|
|
77
|
+
if (!el || typeof ResizeObserver === 'undefined') return;
|
|
78
|
+
resizeObserver = new ResizeObserver(() => {
|
|
79
|
+
measureOverflow();
|
|
80
|
+
});
|
|
81
|
+
resizeObserver.observe(el);
|
|
82
|
+
if (el.parentElement) {
|
|
83
|
+
resizeObserver.observe(el.parentElement);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
onMounted(() => {
|
|
88
|
+
observe();
|
|
89
|
+
measureOverflow();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
onBeforeUnmount(() => {
|
|
93
|
+
resizeObserver?.disconnect();
|
|
94
|
+
resizeObserver = null;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
watch(
|
|
98
|
+
() => nodes.value.length,
|
|
99
|
+
async () => {
|
|
100
|
+
await nextTick();
|
|
101
|
+
observe();
|
|
102
|
+
measureOverflow();
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
watch(path, async () => {
|
|
107
|
+
await nextTick();
|
|
108
|
+
measureOverflow();
|
|
109
|
+
});
|
|
110
|
+
|
|
30
111
|
const select = async (node: MNode) => {
|
|
31
112
|
await editorService.select(node);
|
|
32
113
|
editorService.get('stage')?.select(node.id);
|
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
:disabled-stage-overlay="disabledStageOverlay"
|
|
10
10
|
:stage-content-menu="stageContentMenu"
|
|
11
11
|
:custom-content-menu="customContentMenu"
|
|
12
|
-
|
|
12
|
+
>
|
|
13
|
+
<template #stage-top><slot name="stage-top"></slot></template>
|
|
14
|
+
</MagicStage>
|
|
13
15
|
</slot>
|
|
14
16
|
|
|
15
17
|
<slot name="workspace-content"></slot>
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
|
|
28
28
|
<NodeListMenu></NodeListMenu>
|
|
29
29
|
|
|
30
|
+
<template #before>
|
|
31
|
+
<slot name="stage-top"></slot>
|
|
32
|
+
</template>
|
|
33
|
+
|
|
30
34
|
<template #content>
|
|
31
35
|
<StageOverlay v-if="!disabledStageOverlay"></StageOverlay>
|
|
32
36
|
|
|
@@ -52,7 +56,7 @@ import { calcValueByFontsize, getIdFromEl } from '@tmagic/utils';
|
|
|
52
56
|
import ScrollViewer from '@editor/components/ScrollViewer.vue';
|
|
53
57
|
import { useServices } from '@editor/hooks';
|
|
54
58
|
import { useStage } from '@editor/hooks/use-stage';
|
|
55
|
-
import type { CustomContentMenuFunction, MenuButton, MenuComponent, StageOptions } from '@editor/type';
|
|
59
|
+
import type { CustomContentMenuFunction, MenuButton, MenuComponent, StageOptions, StageSlots } from '@editor/type';
|
|
56
60
|
import { DragType, Layout } from '@editor/type';
|
|
57
61
|
import { getEditorConfig } from '@editor/utils/config';
|
|
58
62
|
import { KeyBindingContainerKey } from '@editor/utils/keybinding-config';
|
|
@@ -65,6 +69,8 @@ defineOptions({
|
|
|
65
69
|
name: 'MEditorStage',
|
|
66
70
|
});
|
|
67
71
|
|
|
72
|
+
defineSlots<StageSlots>();
|
|
73
|
+
|
|
68
74
|
const props = withDefaults(
|
|
69
75
|
defineProps<{
|
|
70
76
|
stageOptions: StageOptions;
|
|
@@ -311,12 +317,34 @@ const dropHandler = async (e: DragEvent) => {
|
|
|
311
317
|
);
|
|
312
318
|
|
|
313
319
|
let parent: MContainer | undefined | null = page.value;
|
|
320
|
+
let resolvedParentEl: HTMLElement | null | undefined = parentEl;
|
|
314
321
|
const parentId = getIdFromEl()(parentEl);
|
|
315
322
|
if (parentId) {
|
|
316
323
|
parent = editorService.getNodeById(parentId, false) as MContainer;
|
|
317
324
|
}
|
|
318
325
|
|
|
319
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
|
+
|
|
320
348
|
const layout = await editorService.getLayout(parent);
|
|
321
349
|
|
|
322
350
|
const containerRect = stageContainerEl.value.getBoundingClientRect();
|
|
@@ -336,8 +364,8 @@ const dropHandler = async (e: DragEvent) => {
|
|
|
336
364
|
top = e.clientY - containerRect.top + scrollTop;
|
|
337
365
|
left = e.clientX - containerRect.left + scrollLeft;
|
|
338
366
|
|
|
339
|
-
if (
|
|
340
|
-
const { left: parentLeft, top: parentTop } = getOffset(
|
|
367
|
+
if (resolvedParentEl) {
|
|
368
|
+
const { left: parentLeft, top: parentTop } = getOffset(resolvedParentEl);
|
|
341
369
|
left = left - parentLeft * zoom.value;
|
|
342
370
|
top = top - parentTop * zoom.value;
|
|
343
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',
|
|
@@ -117,20 +121,28 @@ class Props extends BaseService {
|
|
|
117
121
|
* @param type 组件类型
|
|
118
122
|
* @returns 组件属性表单配置
|
|
119
123
|
*/
|
|
120
|
-
public async getPropsConfig(type: string): Promise<FormConfig> {
|
|
124
|
+
public async getPropsConfig(type: string, data?: { node?: MNode | null }): Promise<FormConfig> {
|
|
121
125
|
if (type === 'area') {
|
|
122
|
-
return await this.getPropsConfig('button');
|
|
126
|
+
return await this.getPropsConfig('button', data);
|
|
123
127
|
}
|
|
124
128
|
|
|
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
|
}
|
|
@@ -3,4 +3,28 @@
|
|
|
3
3
|
left: 5px;
|
|
4
4
|
top: 5px;
|
|
5
5
|
z-index: 10;
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
white-space: nowrap;
|
|
9
|
+
|
|
10
|
+
.m-editor-breadcrumb-item {
|
|
11
|
+
max-width: 100px;
|
|
12
|
+
min-width: 0;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
|
|
15
|
+
// Element Plus button 内部文本节点
|
|
16
|
+
> span {
|
|
17
|
+
display: block;
|
|
18
|
+
max-width: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
text-overflow: ellipsis;
|
|
21
|
+
white-space: nowrap;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.m-editor-breadcrumb-separator,
|
|
26
|
+
.m-editor-breadcrumb-ellipsis {
|
|
27
|
+
margin: 0 4px;
|
|
28
|
+
flex-shrink: 0;
|
|
29
|
+
}
|
|
6
30
|
}
|
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,
|
|
@@ -73,7 +74,17 @@ export interface FrameworkSlots {
|
|
|
73
74
|
'page-list-popover'(props: { list: (MPage | MPageFragment)[] }): any;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
export interface
|
|
77
|
+
export interface ScrollViewerSlots {
|
|
78
|
+
before(props: {}): any;
|
|
79
|
+
content(props: {}): any;
|
|
80
|
+
default(props: {}): any;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface StageSlots extends ScrollViewerSlots {
|
|
84
|
+
'stage-top'(props: {}): any;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface WorkspaceSlots extends StageSlots {
|
|
77
88
|
stage(props: {}): any;
|
|
78
89
|
'workspace-content'(props: {}): any;
|
|
79
90
|
}
|
|
@@ -158,10 +169,23 @@ export interface StageOptions {
|
|
|
158
169
|
moveableOptions?: CustomizeMoveableOptions;
|
|
159
170
|
canSelect?: (el: HTMLElement) => boolean | Promise<boolean>;
|
|
160
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;
|
|
161
180
|
updateDragEl?: UpdateDragEl;
|
|
162
181
|
renderType?: RenderType;
|
|
163
182
|
guidesOptions?: Partial<GuidesOptions>;
|
|
164
183
|
disabledMultiSelect?: boolean;
|
|
184
|
+
/**
|
|
185
|
+
* 始终启用多选模式(无需按住 Ctrl/Meta),默认 false。
|
|
186
|
+
* 当 `disabledMultiSelect` 为 true 时本配置失效。
|
|
187
|
+
*/
|
|
188
|
+
alwaysMultiSelect?: boolean;
|
|
165
189
|
disabledRule?: boolean;
|
|
166
190
|
zoom?: number;
|
|
167
191
|
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
|
@@ -181,6 +205,8 @@ export interface StoreState {
|
|
|
181
205
|
pageLength: number;
|
|
182
206
|
pageFragmentLength: number;
|
|
183
207
|
disabledMultiSelect: boolean;
|
|
208
|
+
/** 是否始终启用多选模式(无需按住 Ctrl/Meta) */
|
|
209
|
+
alwaysMultiSelect: boolean;
|
|
184
210
|
}
|
|
185
211
|
|
|
186
212
|
export type StoreStateKey = keyof StoreState;
|
|
@@ -669,6 +695,37 @@ export interface TreeNodeData {
|
|
|
669
695
|
[key: string]: any;
|
|
670
696
|
}
|
|
671
697
|
|
|
698
|
+
/** 判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
|
699
|
+
export type IsExpandableFunction = (_data: TreeNodeData, _nodeStatusMap: Map<Id, LayerNodeStatus>) => boolean;
|
|
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
|
+
|
|
672
729
|
export type AsyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
|
|
673
730
|
[K in Value[number]]?: (...args: Parameters<C[K]>) => Promise<Parameters<C[K]>> | Parameters<C[K]>;
|
|
674
731
|
};
|
package/src/utils/tree.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Id } from '@tmagic/core';
|
|
2
2
|
import { getKeys } from '@tmagic/utils';
|
|
3
3
|
|
|
4
|
-
import type { LayerNodeStatus } from '@editor/type';
|
|
4
|
+
import type { IsExpandableFunction, LayerNodeStatus } from '@editor/type';
|
|
5
5
|
|
|
6
6
|
export const updateStatus = (nodeStatusMap: Map<Id, LayerNodeStatus>, id: Id, status: Partial<LayerNodeStatus>) => {
|
|
7
7
|
const nodeStatus = nodeStatusMap.get(id);
|
|
@@ -13,3 +13,7 @@ export const updateStatus = (nodeStatusMap: Map<Id, LayerNodeStatus>, id: Id, st
|
|
|
13
13
|
}
|
|
14
14
|
});
|
|
15
15
|
};
|
|
16
|
+
|
|
17
|
+
/** 默认的组件树节点是否可展开的判断函数:当节点的子项中至少存在一个可见节点时认为可展开 */
|
|
18
|
+
export const defaultIsExpandable: IsExpandableFunction = (data, nodeStatusMap) =>
|
|
19
|
+
Array.isArray(data.items) && data.items.some((item) => nodeStatusMap.get(item.id)?.visible);
|