mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-04 10:58:33 +08:00
feat(editor): 新增 canDropIn 配置统一控制 layer/stage 拖拽放入行为
支持通过 scene 区分图层树、画布拖动、组件库新增三种场景; 返回 false 阻止放入,返回 Id 可重定向放入目标节点。 layer 场景下若禁用某节点的 inner,其子节点的 before/after 也会被同步禁用以避免被绕过。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
d8133629b4
commit
5af9f6e27a
@ -793,6 +793,87 @@ const isContainer = (el) =>
|
|||||||
</script>
|
</script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## canDropIn
|
||||||
|
|
||||||
|
- **详情:**
|
||||||
|
|
||||||
|
用于自定义判断当前正在拖动的源是否可以拖入目标节点内部。同时覆盖"组件树拖动"和"画布拖入"两类场景,通过第三个参数 `scene` 区分;返回值有 3 种语义。
|
||||||
|
|
||||||
|
**scene 取值:**
|
||||||
|
|
||||||
|
| scene | 触发场景 | `sourceIds` | `targetId` |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `'layer'` | "已选组件"面板组件树拖动 | 被拖动节点 id(单选时长度为 1) | 目标节点 id |
|
||||||
|
| `'stage-drag'` | 画布上拖动已有组件 | 被拖动组件 id 列表(多选时为多个) | 候选容器节点 id |
|
||||||
|
| `'stage-add'` | 从左侧组件列表拖入新组件到画布 | 始终为空数组(尚无 id,可仅依据 `targetId` 判断) | 候选容器节点 id |
|
||||||
|
|
||||||
|
**返回值语义:**
|
||||||
|
|
||||||
|
| 返回值 | layer | stage-drag | stage-add |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `false` | 禁用 inner;同时禁用所有"target 子节点的 before/after"(这些位置等价于放入 target,避免被绕过) | 阻止该容器被高亮命中 | 取消此次拖入 |
|
||||||
|
| `Id`(string \| number) | 将 inner 拖入目标重定向为该 id 对应的节点;与 `false` 一致禁用所有"target 子节点的 before/after" | 高亮命中切换到该 id 对应元素,最终拖入到该节点 | 直接将组件添加到该 id 对应节点(layout 坐标也基于其 DOM 重新计算) |
|
||||||
|
| `true` / `void` / `undefined` | 按原 targetId 正常拖入 | 同左 | 同左 |
|
||||||
|
|
||||||
|
`scene` 取 `'stage-drag'` 或 `'stage-add'` 时该函数会被透传给 `StageCore` 的 `canDropIn`,因此直接使用 `@tmagic/stage` 时同样生效
|
||||||
|
|
||||||
|
:::tip
|
||||||
|
- 可通过 `editorService.getNodeById(id, false)` 把 id 还原为 `MNode` 以便基于业务字段(`type`、`name` 等)做判断。
|
||||||
|
- 该函数为**同步**调用(拖动事件在浏览器中需要立即响应,不接受异步返回)。
|
||||||
|
- 重定向到一个不存在或非容器的目标 id 时会被忽略:layer/stage-add 场景会取消此次拖入;stage-drag 场景不会高亮。
|
||||||
|
:::
|
||||||
|
|
||||||
|
- **默认值:** `undefined`
|
||||||
|
|
||||||
|
- **类型:** `(sourceIds: Id[], targetId: Id, scene: 'layer' | 'stage-drag' | 'stage-add') => Id | boolean | void`
|
||||||
|
|
||||||
|
- **示例 1:禁止某些组件拖入特定容器**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<m-editor :can-drop-in="canDropIn"></m-editor>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { editorService } from '@tmagic/editor';
|
||||||
|
|
||||||
|
// 禁止 button 类型的组件被拖入 list 容器内部,组件树拖动与画布拖入均生效
|
||||||
|
const canDropIn = (sourceIds, targetId, scene) => {
|
||||||
|
const targetNode = editorService.getNodeById(targetId, false);
|
||||||
|
if (targetNode?.type !== 'list') return true;
|
||||||
|
|
||||||
|
// 从组件列表新增组件时直接放行
|
||||||
|
if (scene === 'stage-add') return true;
|
||||||
|
|
||||||
|
return sourceIds.every((id) => {
|
||||||
|
const node = editorService.getNodeById(id, false);
|
||||||
|
return node?.type !== 'button';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
|
- **示例 2:将拖入"卡片外壳"重定向到"卡片内容"内层容器**
|
||||||
|
|
||||||
|
```html
|
||||||
|
<template>
|
||||||
|
<m-editor :can-drop-in="canDropIn"></m-editor>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { editorService } from '@tmagic/editor';
|
||||||
|
|
||||||
|
// 当用户拖入到 card 节点时,自动改为放入其 card-content 内层容器
|
||||||
|
const canDropIn = (sourceIds, targetId) => {
|
||||||
|
const targetNode = editorService.getNodeById(targetId, false);
|
||||||
|
if (targetNode?.type !== 'card') return true;
|
||||||
|
|
||||||
|
const innerContent = targetNode.items?.find((item) => item.type === 'card-content');
|
||||||
|
return innerContent?.id ?? true;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
|
||||||
## containerHighlightClassName
|
## containerHighlightClassName
|
||||||
|
|
||||||
- **详情:**
|
- **详情:**
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
:indent="treeIndent"
|
:indent="treeIndent"
|
||||||
:next-level-indent-increment="treeNextLevelIndentIncrement"
|
:next-level-indent-increment="treeNextLevelIndentIncrement"
|
||||||
:layer-node-is-expandable="layerNodeIsExpandable"
|
:layer-node-is-expandable="layerNodeIsExpandable"
|
||||||
|
:can-drop-in="canDropIn"
|
||||||
>
|
>
|
||||||
<template #layer-panel-header>
|
<template #layer-panel-header>
|
||||||
<slot name="layer-panel-header"></slot>
|
<slot name="layer-panel-header"></slot>
|
||||||
@ -202,6 +203,11 @@ const stageOptions: StageOptions = {
|
|||||||
canSelect: props.canSelect,
|
canSelect: props.canSelect,
|
||||||
updateDragEl: props.updateDragEl,
|
updateDragEl: props.updateDragEl,
|
||||||
isContainer: props.isContainer,
|
isContainer: props.isContainer,
|
||||||
|
// sourceIds 为空表示从组件列表新增(尚无 id),否则是画布上拖动已有组件
|
||||||
|
canDropIn: props.canDropIn
|
||||||
|
? (sourceIds, targetId) =>
|
||||||
|
props.canDropIn!(sourceIds, targetId, sourceIds.length === 0 ? 'stage-add' : 'stage-drag')
|
||||||
|
: undefined,
|
||||||
containerHighlightClassName: props.containerHighlightClassName,
|
containerHighlightClassName: props.containerHighlightClassName,
|
||||||
containerHighlightDuration: props.containerHighlightDuration,
|
containerHighlightDuration: props.containerHighlightDuration,
|
||||||
containerHighlightType: props.containerHighlightType,
|
containerHighlightType: props.containerHighlightType,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import StageCore, {
|
|||||||
import { getIdFromEl } from '@tmagic/utils';
|
import { getIdFromEl } from '@tmagic/utils';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
CanDropInFunction,
|
||||||
ComponentGroup,
|
ComponentGroup,
|
||||||
CustomContentMenuFunction,
|
CustomContentMenuFunction,
|
||||||
DatasourceTypeOption,
|
DatasourceTypeOption,
|
||||||
@ -101,6 +102,16 @@ export interface EditorProps {
|
|||||||
customContentMenu?: CustomContentMenuFunction;
|
customContentMenu?: CustomContentMenuFunction;
|
||||||
/** 用于自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态) */
|
/** 用于自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态) */
|
||||||
layerNodeIsExpandable?: IsExpandableFunction;
|
layerNodeIsExpandable?: IsExpandableFunction;
|
||||||
|
/**
|
||||||
|
* 用于自定义判断当前正在拖动的源是否可以拖入目标节点内部
|
||||||
|
*
|
||||||
|
* 同时覆盖以下两类场景,通过第三个参数 scene 区分:
|
||||||
|
* - `'layer'` :"已选组件"面板组件树拖动(返回 false 时仅禁用 inner,不影响 before/after)
|
||||||
|
* - `'stage'`:画布拖入组件(返回 false 时阻止该容器被高亮命中;适用于"组件列表拖入新组件"和"画布上拖动已有组件"两种细分情况)
|
||||||
|
*
|
||||||
|
* 注意:layer 场景目前只识别同步返回值;返回 Promise 时会按 true 处理(即允许)
|
||||||
|
*/
|
||||||
|
canDropIn?: CanDropInFunction;
|
||||||
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||||
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
||||||
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||||
|
|||||||
@ -24,6 +24,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
|||||||
zoom: stageOptions.zoom ?? zoom.value,
|
zoom: stageOptions.zoom ?? zoom.value,
|
||||||
autoScrollIntoView: stageOptions.autoScrollIntoView,
|
autoScrollIntoView: stageOptions.autoScrollIntoView,
|
||||||
isContainer: stageOptions.isContainer,
|
isContainer: stageOptions.isContainer,
|
||||||
|
canDropIn: stageOptions.canDropIn,
|
||||||
containerHighlightClassName: stageOptions.containerHighlightClassName,
|
containerHighlightClassName: stageOptions.containerHighlightClassName,
|
||||||
containerHighlightDuration: stageOptions.containerHighlightDuration,
|
containerHighlightDuration: stageOptions.containerHighlightDuration,
|
||||||
containerHighlightType: stageOptions.containerHighlightType,
|
containerHighlightType: stageOptions.containerHighlightType,
|
||||||
|
|||||||
@ -162,6 +162,7 @@ import { useEditorContentHeight } from '@editor/hooks/use-editor-content-height'
|
|||||||
import { useFloatBox } from '@editor/hooks/use-float-box';
|
import { useFloatBox } from '@editor/hooks/use-float-box';
|
||||||
import { useServices } from '@editor/hooks/use-services';
|
import { useServices } from '@editor/hooks/use-services';
|
||||||
import {
|
import {
|
||||||
|
type CanDropInFunction,
|
||||||
ColumnLayout,
|
ColumnLayout,
|
||||||
CustomContentMenuFunction,
|
CustomContentMenuFunction,
|
||||||
type IsExpandableFunction,
|
type IsExpandableFunction,
|
||||||
@ -194,6 +195,8 @@ const props = withDefaults(
|
|||||||
customContentMenu: CustomContentMenuFunction;
|
customContentMenu: CustomContentMenuFunction;
|
||||||
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
||||||
layerNodeIsExpandable?: IsExpandableFunction;
|
layerNodeIsExpandable?: IsExpandableFunction;
|
||||||
|
/** 自定义判断当前正在拖动的源是否可以拖入目标节点内部,详见 EditorProps.canDropIn */
|
||||||
|
canDropIn?: CanDropInFunction;
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -252,6 +255,7 @@ const getItemConfig = (data: SideItem): SideComponent => {
|
|||||||
indent: props.indent,
|
indent: props.indent,
|
||||||
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
|
nextLevelIndentIncrement: props.nextLevelIndentIncrement,
|
||||||
isExpandable: props.layerNodeIsExpandable,
|
isExpandable: props.layerNodeIsExpandable,
|
||||||
|
canDropIn: props.canDropIn,
|
||||||
},
|
},
|
||||||
component: LayerPanel,
|
component: LayerPanel,
|
||||||
slots: {},
|
slots: {},
|
||||||
|
|||||||
@ -58,6 +58,7 @@ import Tree from '@editor/components/Tree.vue';
|
|||||||
import { useFilter } from '@editor/hooks/use-filter';
|
import { useFilter } from '@editor/hooks/use-filter';
|
||||||
import { useServices } from '@editor/hooks/use-services';
|
import { useServices } from '@editor/hooks/use-services';
|
||||||
import type {
|
import type {
|
||||||
|
CanDropInFunction,
|
||||||
CustomContentMenuFunction,
|
CustomContentMenuFunction,
|
||||||
IsExpandableFunction,
|
IsExpandableFunction,
|
||||||
LayerPanelSlots,
|
LayerPanelSlots,
|
||||||
@ -79,13 +80,15 @@ defineOptions({
|
|||||||
name: 'MEditorLayerPanel',
|
name: 'MEditorLayerPanel',
|
||||||
});
|
});
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
layerContentMenu: (MenuButton | MenuComponent)[];
|
layerContentMenu: (MenuButton | MenuComponent)[];
|
||||||
indent?: number;
|
indent?: number;
|
||||||
nextLevelIndentIncrement?: number;
|
nextLevelIndentIncrement?: number;
|
||||||
customContentMenu: CustomContentMenuFunction;
|
customContentMenu: CustomContentMenuFunction;
|
||||||
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
/** 自定义判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
||||||
isExpandable?: IsExpandableFunction;
|
isExpandable?: IsExpandableFunction;
|
||||||
|
/** 自定义判断当前拖动节点是否可以拖入目标节点内部的函数,返回 false 则禁止拖入 */
|
||||||
|
canDropIn?: CanDropInFunction;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const services = useServices();
|
const services = useServices();
|
||||||
@ -123,7 +126,9 @@ const collapseAllHandler = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useDrag(services);
|
const { handleDragStart, handleDragEnd, handleDragLeave, handleDragOver } = useDrag(services, {
|
||||||
|
canDropIn: (sourceIds, targetId) => props.canDropIn?.(sourceIds, targetId, 'layer'),
|
||||||
|
});
|
||||||
|
|
||||||
// 右键菜单
|
// 右键菜单
|
||||||
const menuRef = useTemplateRef<InstanceType<typeof LayerMenu>>('menu');
|
const menuRef = useTemplateRef<InstanceType<typeof LayerMenu>>('menu');
|
||||||
|
|||||||
@ -11,6 +11,8 @@ const dragState: {
|
|||||||
dropType: NodeDropType | '';
|
dropType: NodeDropType | '';
|
||||||
container: HTMLElement | null;
|
container: HTMLElement | null;
|
||||||
nodeId?: Id;
|
nodeId?: Id;
|
||||||
|
/** canDropIn 返回 Id 时记录的重定向目标 id,handleDragEnd 阶段会改用该 id 对应的节点作为 inner 拖入的父节点 */
|
||||||
|
redirectedTargetId?: Id;
|
||||||
} = {
|
} = {
|
||||||
dragOverNodeId: '',
|
dragOverNodeId: '',
|
||||||
dropType: '',
|
dropType: '',
|
||||||
@ -37,12 +39,22 @@ const removeStatusClass = (el: HTMLElement | null) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface UseDragOptions {
|
||||||
|
/**
|
||||||
|
* 用于判断某个节点是否能被拖动到另一个节点内部
|
||||||
|
* - 返回 `false`:阻止 inner 拖入(before/after 仍然可用)
|
||||||
|
* - 返回 `Id`:将 inner 拖入的目标重定向到该 id 对应的节点
|
||||||
|
* - 其他:按原 targetId 正常拖入
|
||||||
|
*/
|
||||||
|
canDropIn?: (sourceIds: Id[], targetId: Id) => Id | boolean | void;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* dragstart/dragleave/dragend 属于源节点
|
* dragstart/dragleave/dragend 属于源节点
|
||||||
* dragover 属于目标节点
|
* dragover 属于目标节点
|
||||||
* 这些方法并不是同一个dom事件触发的
|
* 这些方法并不是同一个dom事件触发的
|
||||||
*/
|
*/
|
||||||
export const useDrag = ({ editorService }: Services) => {
|
export const useDrag = ({ editorService }: Services, options: UseDragOptions = {}) => {
|
||||||
const handleDragStart = (event: DragEvent) => {
|
const handleDragStart = (event: DragEvent) => {
|
||||||
if (!event.dataTransfer || !event.target || !event.currentTarget) return;
|
if (!event.dataTransfer || !event.target || !event.currentTarget) return;
|
||||||
|
|
||||||
@ -102,13 +114,43 @@ export const useDrag = ({ editorService }: Services) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (distance < targetHeight / 3) {
|
// 通过用户配置的钩子判断当前拖动节点是否允许拖入目标节点内部
|
||||||
|
// - false:禁止 inner 拖入
|
||||||
|
// - Id :将 inner 拖入的父节点重定向为该 id 对应的节点
|
||||||
|
// - 其他:按原 targetNodeId 正常拖入
|
||||||
|
let canDropInTarget = isContainer;
|
||||||
|
let redirectedTargetId: Id | undefined;
|
||||||
|
if (canDropInTarget && options.canDropIn && nodeId && targetNodeId !== nodeId) {
|
||||||
|
const result = options.canDropIn([nodeId], targetNodeId);
|
||||||
|
if (result === false) {
|
||||||
|
canDropInTarget = false;
|
||||||
|
} else if (typeof result === 'string' || typeof result === 'number') {
|
||||||
|
redirectedTargetId = result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// before/after 模式下新节点会成为 target 的兄弟(即 target 的直接父节点的子节点),
|
||||||
|
// 所以应该用 target 的直接父节点 id 再次调用 canDropIn 校验。
|
||||||
|
// 若该父节点禁止拖入(false)或要求重定向(Id),都视为"不应放入此父节点",
|
||||||
|
// 故对应的 before/after 也禁用——避免绕过 inner 限制。
|
||||||
|
let canDropAsSibling = true;
|
||||||
|
const directParentId = parentsId?.[parentsId.length - 1];
|
||||||
|
if (options.canDropIn && nodeId && directParentId && directParentId !== nodeId) {
|
||||||
|
const siblingResult = options.canDropIn([nodeId], directParentId);
|
||||||
|
if (siblingResult === false || typeof siblingResult === 'string' || typeof siblingResult === 'number') {
|
||||||
|
canDropAsSibling = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显式重置 dropType,避免上一次 dragover 的残留值影响本次判断
|
||||||
|
dragState.dropType = '';
|
||||||
|
if (distance < targetHeight / 3 && canDropAsSibling) {
|
||||||
dragState.dropType = 'before';
|
dragState.dropType = 'before';
|
||||||
addClassName(labelEl, globalThis.document, 'drag-before');
|
addClassName(labelEl, globalThis.document, 'drag-before');
|
||||||
} else if (distance > (targetHeight * 2) / 3) {
|
} else if (distance > (targetHeight * 2) / 3 && canDropAsSibling) {
|
||||||
dragState.dropType = 'after';
|
dragState.dropType = 'after';
|
||||||
addClassName(labelEl, globalThis.document, 'drag-after');
|
addClassName(labelEl, globalThis.document, 'drag-after');
|
||||||
} else if (isContainer) {
|
} else if (canDropInTarget) {
|
||||||
dragState.dropType = 'inner';
|
dragState.dropType = 'inner';
|
||||||
addClassName(labelEl, globalThis.document, 'drag-inner');
|
addClassName(labelEl, globalThis.document, 'drag-inner');
|
||||||
}
|
}
|
||||||
@ -119,6 +161,8 @@ export const useDrag = ({ editorService }: Services) => {
|
|||||||
|
|
||||||
dragState.dragOverNodeId = targetNodeId;
|
dragState.dragOverNodeId = targetNodeId;
|
||||||
dragState.container = event.currentTarget as HTMLElement;
|
dragState.container = event.currentTarget as HTMLElement;
|
||||||
|
// 仅 inner 时才使用重定向,before/after 是相对于 dragOverNodeId 的兄弟插入,重定向无意义
|
||||||
|
dragState.redirectedTargetId = dragState.dropType === 'inner' ? redirectedTargetId : undefined;
|
||||||
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
};
|
};
|
||||||
@ -158,8 +202,19 @@ export const useDrag = ({ editorService }: Services) => {
|
|||||||
let targetIndex = -1;
|
let targetIndex = -1;
|
||||||
|
|
||||||
if (Array.isArray(targetNode.items) && dragState.dropType === 'inner') {
|
if (Array.isArray(targetNode.items) && dragState.dropType === 'inner') {
|
||||||
|
// 优先使用 canDropIn 返回的重定向 id 对应的节点作为父节点
|
||||||
|
if (dragState.redirectedTargetId !== undefined) {
|
||||||
|
const redirectedNode = editorService.getNodeInfo(dragState.redirectedTargetId, false).node;
|
||||||
|
if (!redirectedNode || !Array.isArray((redirectedNode as MContainer).items)) {
|
||||||
|
// 重定向目标无效或不是容器,放弃此次拖入
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetParent = redirectedNode as MContainer;
|
||||||
|
targetIndex = (redirectedNode as MContainer).items!.length;
|
||||||
|
} else {
|
||||||
targetIndex = targetNode.items.length;
|
targetIndex = targetNode.items.length;
|
||||||
targetParent = targetNode as MContainer;
|
targetParent = targetNode as MContainer;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
targetIndex = getNodeIndex(dragState.dragOverNodeId, targetParent);
|
targetIndex = getNodeIndex(dragState.dragOverNodeId, targetParent);
|
||||||
}
|
}
|
||||||
@ -180,6 +235,7 @@ export const useDrag = ({ editorService }: Services) => {
|
|||||||
dragState.dragOverNodeId = '';
|
dragState.dragOverNodeId = '';
|
||||||
dragState.dropType = '';
|
dragState.dropType = '';
|
||||||
dragState.container = null;
|
dragState.container = null;
|
||||||
|
dragState.redirectedTargetId = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -317,12 +317,34 @@ const dropHandler = async (e: DragEvent) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let parent: MContainer | undefined | null = page.value;
|
let parent: MContainer | undefined | null = page.value;
|
||||||
|
let resolvedParentEl: HTMLElement | null | undefined = parentEl;
|
||||||
const parentId = getIdFromEl()(parentEl);
|
const parentId = getIdFromEl()(parentEl);
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
parent = editorService.getNodeById(parentId, false) as MContainer;
|
parent = editorService.getNodeById(parentId, false) as MContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parent && stageContainerEl.value && stage) {
|
if (parent && stageContainerEl.value && stage) {
|
||||||
|
// 通过用户配置的钩子再次确认当前拖入的新组件是否允许放入命中的高亮容器,
|
||||||
|
// 防止 delayedMarkContainer 的延迟/异步未生效或残留高亮导致命中错误容器
|
||||||
|
// - 返回 false:取消此次拖入
|
||||||
|
// - 返回 Id :将父节点重定向到该 id 对应的节点(layout 坐标也基于其 DOM 重新计算)
|
||||||
|
// - 其他 :使用原命中节点
|
||||||
|
// 从组件列表拖入新组件时 sourceIds 为空数组(尚无 id)
|
||||||
|
if (props.stageOptions.canDropIn) {
|
||||||
|
const result = props.stageOptions.canDropIn([], parent.id);
|
||||||
|
if (result === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof result === 'string' || typeof result === 'number') {
|
||||||
|
const redirectedNode = editorService.getNodeById(result, false) as MContainer | undefined;
|
||||||
|
if (!redirectedNode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
parent = redirectedNode;
|
||||||
|
resolvedParentEl = stage.renderer?.getTargetElement(result) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const layout = await editorService.getLayout(parent);
|
const layout = await editorService.getLayout(parent);
|
||||||
|
|
||||||
const containerRect = stageContainerEl.value.getBoundingClientRect();
|
const containerRect = stageContainerEl.value.getBoundingClientRect();
|
||||||
@ -342,8 +364,8 @@ const dropHandler = async (e: DragEvent) => {
|
|||||||
top = e.clientY - containerRect.top + scrollTop;
|
top = e.clientY - containerRect.top + scrollTop;
|
||||||
left = e.clientX - containerRect.left + scrollLeft;
|
left = e.clientX - containerRect.left + scrollLeft;
|
||||||
|
|
||||||
if (parentEl) {
|
if (resolvedParentEl) {
|
||||||
const { left: parentLeft, top: parentTop } = getOffset(parentEl);
|
const { left: parentLeft, top: parentTop } = getOffset(resolvedParentEl);
|
||||||
left = left - parentLeft * zoom.value;
|
left = left - parentLeft * zoom.value;
|
||||||
top = top - parentTop * zoom.value;
|
top = top - parentTop * zoom.value;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage
|
|||||||
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
|
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
|
||||||
import type StageCore from '@tmagic/stage';
|
import type StageCore from '@tmagic/stage';
|
||||||
import type {
|
import type {
|
||||||
|
CanDropIn,
|
||||||
ContainerHighlightType,
|
ContainerHighlightType,
|
||||||
CustomizeMoveableOptions,
|
CustomizeMoveableOptions,
|
||||||
GuidesOptions,
|
GuidesOptions,
|
||||||
@ -168,6 +169,14 @@ export interface StageOptions {
|
|||||||
moveableOptions?: CustomizeMoveableOptions;
|
moveableOptions?: CustomizeMoveableOptions;
|
||||||
canSelect?: (el: HTMLElement) => boolean | Promise<boolean>;
|
canSelect?: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||||
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
|
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* 画布上拖入组件(包括从组件列表拖入新组件、画布上拖动已有组件)时,
|
||||||
|
* 对已通过 isContainer 命中的候选容器进行二次过滤;返回 false 时阻止该容器被高亮命中
|
||||||
|
* - 在画布上拖动已有组件时:sourceIds 为被拖动组件的 id 列表
|
||||||
|
* - 从组件列表拖入新组件时:sourceIds 为空数组(尚无 id,仅可依据 targetId 判断)
|
||||||
|
* 该选项会被透传给 StageCore 的 canDropIn
|
||||||
|
*/
|
||||||
|
canDropIn?: CanDropIn;
|
||||||
updateDragEl?: UpdateDragEl;
|
updateDragEl?: UpdateDragEl;
|
||||||
renderType?: RenderType;
|
renderType?: RenderType;
|
||||||
guidesOptions?: Partial<GuidesOptions>;
|
guidesOptions?: Partial<GuidesOptions>;
|
||||||
@ -682,6 +691,34 @@ export interface TreeNodeData {
|
|||||||
/** 判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
/** 判断组件树节点是否可展开(即是否要展示为拥有子节点的形态)的函数 */
|
||||||
export type IsExpandableFunction = (_data: TreeNodeData, _nodeStatusMap: Map<Id, LayerNodeStatus>) => boolean;
|
export type IsExpandableFunction = (_data: TreeNodeData, _nodeStatusMap: Map<Id, LayerNodeStatus>) => boolean;
|
||||||
|
|
||||||
|
/** canDropIn 的调用场景 */
|
||||||
|
export type CanDropInScene =
|
||||||
|
/** 在"已选组件"面板的组件树中拖动节点 */
|
||||||
|
| 'layer'
|
||||||
|
/** 在画布上拖动已有组件(被拖动组件本身已经存在于画布中,sourceIds 包含其 id) */
|
||||||
|
| 'stage-drag'
|
||||||
|
/** 从组件列表拖入新组件到画布(被拖入的组件尚不存在,sourceIds 为空数组) */
|
||||||
|
| 'stage-add';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断当前正在拖动的源节点是否可以拖入目标节点内部的函数
|
||||||
|
* @param _sourceIds 当前正在拖动的源节点 id 列表
|
||||||
|
* - `layer`:被拖动的组件树节点 id(单选时长度为 1)
|
||||||
|
* - `stage-drag`:被拖动组件的 id 列表(多选拖动时为多个)
|
||||||
|
* - `stage-add`:始终为空数组(从组件列表新增的组件尚无 id)
|
||||||
|
* @param _targetId 目标容器的节点 id
|
||||||
|
* @param _scene 调用场景:见 {@link CanDropInScene}
|
||||||
|
* @returns
|
||||||
|
* - `false`:阻止该容器被视为合法拖入目标
|
||||||
|
* - `layer`:禁用 inner 高亮(before/after 仍然可用)
|
||||||
|
* - `stage-drag`:阻止该容器被高亮命中
|
||||||
|
* - `stage-add`:阻止该容器被高亮命中并退化为放入当前页面
|
||||||
|
* - `Id`(string | number):将拖入目标重定向到该 id 对应的节点
|
||||||
|
* (例如把命中的"卡片外壳"节点重定向到其内层"卡片内容"容器节点)
|
||||||
|
* - 其他(`true` / `void` / `undefined`):按原 targetId 正常拖入
|
||||||
|
*/
|
||||||
|
export type CanDropInFunction = (_sourceIds: Id[], _targetId: Id, _scene: CanDropInScene) => Id | boolean | void;
|
||||||
|
|
||||||
export type AsyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
|
export type AsyncBeforeHook<Value extends Array<string>, C extends Record<Value[number], (...args: any) => any>> = {
|
||||||
[K in Value[number]]?: (...args: Parameters<C[K]>) => Promise<Parameters<C[K]>> | Parameters<C[K]>;
|
[K in Value[number]]?: (...args: Parameters<C[K]>) => Promise<Parameters<C[K]>> | Parameters<C[K]>;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -42,6 +42,7 @@ import StageMultiDragResize from './StageMultiDragResize';
|
|||||||
import type {
|
import type {
|
||||||
ActionManagerConfig,
|
ActionManagerConfig,
|
||||||
ActionManagerEvents,
|
ActionManagerEvents,
|
||||||
|
CanDropIn,
|
||||||
CanSelect,
|
CanSelect,
|
||||||
CustomizeMoveableOptions,
|
CustomizeMoveableOptions,
|
||||||
CustomizeMoveableOptionsCallbackConfig,
|
CustomizeMoveableOptionsCallbackConfig,
|
||||||
@ -88,6 +89,8 @@ export default class ActionManager extends EventEmitter {
|
|||||||
private getElementsFromPoint: GetElementsFromPoint;
|
private getElementsFromPoint: GetElementsFromPoint;
|
||||||
private canSelect: CanSelect;
|
private canSelect: CanSelect;
|
||||||
private isContainer?: IsContainer;
|
private isContainer?: IsContainer;
|
||||||
|
/** 见 ActionManagerConfig.canDropIn */
|
||||||
|
private canDropIn?: CanDropIn;
|
||||||
private getRenderDocument: GetRenderDocument;
|
private getRenderDocument: GetRenderDocument;
|
||||||
private disabledMultiSelect = false;
|
private disabledMultiSelect = false;
|
||||||
private config: ActionManagerConfig;
|
private config: ActionManagerConfig;
|
||||||
@ -125,6 +128,7 @@ export default class ActionManager extends EventEmitter {
|
|||||||
this.canSelect = config.canSelect || ((el: HTMLElement) => Boolean(getIdFromEl()(el)));
|
this.canSelect = config.canSelect || ((el: HTMLElement) => Boolean(getIdFromEl()(el)));
|
||||||
this.getRenderDocument = config.getRenderDocument;
|
this.getRenderDocument = config.getRenderDocument;
|
||||||
this.isContainer = config.isContainer;
|
this.isContainer = config.isContainer;
|
||||||
|
this.canDropIn = config.canDropIn;
|
||||||
|
|
||||||
this.dr = this.createDr(config);
|
this.dr = this.createDr(config);
|
||||||
|
|
||||||
@ -371,14 +375,28 @@ export default class ActionManager extends EventEmitter {
|
|||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
|
|
||||||
const els = this.getElementsFromPoint(event);
|
const els = this.getElementsFromPoint(event);
|
||||||
|
// 取出源元素的节点 id 列表(多选拖动时为多个;从组件列表拖入时为空数组)
|
||||||
|
const sourceIds: Id[] = excludeElList
|
||||||
|
.map((el) => (el instanceof HTMLElement ? getIdFromEl()(el) : undefined))
|
||||||
|
.filter((id): id is string => Boolean(id));
|
||||||
|
|
||||||
for (const el of els) {
|
for (const el of els) {
|
||||||
if (
|
const targetId = getIdFromEl()(el);
|
||||||
!getIdFromEl()(el)?.startsWith(GHOST_EL_ID_PREFIX) &&
|
if (!targetId?.startsWith(GHOST_EL_ID_PREFIX) && (await this.isContainer?.(el)) && !excludeElList.includes(el)) {
|
||||||
(await this.isContainer?.(el)) &&
|
// 用户配置的钩子可以:
|
||||||
!excludeElList.includes(el)
|
// - 返回 false 阻止某些源拖入命中的容器(例如禁止 button 拖入 list)
|
||||||
) {
|
// - 返回 Id 将拖入目标重定向到另一个容器(例如把命中的 list 重定向到其内层的 list-content)
|
||||||
addClassName(el, doc, this.containerHighlightClassName);
|
let highlightEl: HTMLElement = el;
|
||||||
|
if (targetId && this.canDropIn) {
|
||||||
|
const result = this.canDropIn(sourceIds, targetId);
|
||||||
|
if (result === false) continue;
|
||||||
|
if (typeof result === 'string' || typeof result === 'number') {
|
||||||
|
const redirectedEl = this.getTargetElement(result);
|
||||||
|
if (!redirectedEl) continue;
|
||||||
|
highlightEl = redirectedEl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addClassName(highlightEl, doc, this.containerHighlightClassName);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -348,6 +348,7 @@ export default class StageCore extends EventEmitter {
|
|||||||
disabledMultiSelect: config.disabledMultiSelect,
|
disabledMultiSelect: config.disabledMultiSelect,
|
||||||
canSelect: config.canSelect,
|
canSelect: config.canSelect,
|
||||||
isContainer: config.isContainer,
|
isContainer: config.isContainer,
|
||||||
|
canDropIn: config.canDropIn,
|
||||||
updateDragEl: config.updateDragEl,
|
updateDragEl: config.updateDragEl,
|
||||||
getRootContainer: () => this.container,
|
getRootContainer: () => this.container,
|
||||||
getRenderDocument: () => this.renderer!.getDocument(),
|
getRenderDocument: () => this.renderer!.getDocument(),
|
||||||
|
|||||||
@ -30,6 +30,18 @@ export type TargetElement = HTMLElement | SVGElement;
|
|||||||
|
|
||||||
export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
|
export type CanSelect = (el: HTMLElement, event: MouseEvent, stop: () => boolean) => boolean | Promise<boolean>;
|
||||||
export type IsContainer = (el: HTMLElement) => boolean | Promise<boolean>;
|
export type IsContainer = (el: HTMLElement) => boolean | Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* 判断当前正在拖动的源是否可以拖入目标容器(用于画布上拖入组件时的容器高亮命中)
|
||||||
|
* @param sourceIds 当前正在拖动的源节点 id 列表
|
||||||
|
* - 在画布上拖动已有组件时:为被拖动的组件 id(多选拖动时为多个)
|
||||||
|
* - 从组件列表拖入新组件时:为空数组(此时尚无 id,可仅依据 targetId 判断)
|
||||||
|
* @param targetId 已通过 isContainer 命中的候选容器节点 id
|
||||||
|
* @returns
|
||||||
|
* - `false`:阻止该容器被视为合法拖入目标(不会被高亮命中)
|
||||||
|
* - `Id`(string | number):将拖入目标重定向到该 id 对应的节点
|
||||||
|
* - 其他(`true` / `void` / `undefined`):按命中的 targetId 正常拖入
|
||||||
|
*/
|
||||||
|
export type CanDropIn = (sourceIds: Id[], targetId: Id) => Id | boolean | void;
|
||||||
export type CustomizeRender = (renderer: StageCore) => Promise<HTMLElement | void> | HTMLElement | void;
|
export type CustomizeRender = (renderer: StageCore) => Promise<HTMLElement | void> | HTMLElement | void;
|
||||||
|
|
||||||
export type CustomizeMoveableOptionsFunction = (config: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions;
|
export type CustomizeMoveableOptionsFunction = (config: CustomizeMoveableOptionsCallbackConfig) => MoveableOptions;
|
||||||
@ -54,6 +66,11 @@ export interface StageCoreConfig {
|
|||||||
zoom?: number;
|
zoom?: number;
|
||||||
canSelect?: CanSelect;
|
canSelect?: CanSelect;
|
||||||
isContainer?: IsContainer;
|
isContainer?: IsContainer;
|
||||||
|
/**
|
||||||
|
* 画布上拖动组件时,对已通过 isContainer 命中的候选容器进行二次过滤,
|
||||||
|
* 用于实现"某些源不允许拖入某些容器内部"的场景。返回 false 时阻止该容器被高亮命中
|
||||||
|
*/
|
||||||
|
canDropIn?: CanDropIn;
|
||||||
containerHighlightClassName?: string;
|
containerHighlightClassName?: string;
|
||||||
containerHighlightDuration?: number;
|
containerHighlightDuration?: number;
|
||||||
containerHighlightType?: ContainerHighlightType;
|
containerHighlightType?: ContainerHighlightType;
|
||||||
@ -80,6 +97,8 @@ export interface ActionManagerConfig {
|
|||||||
disabledMultiSelect?: boolean;
|
disabledMultiSelect?: boolean;
|
||||||
canSelect?: CanSelect;
|
canSelect?: CanSelect;
|
||||||
isContainer?: IsContainer;
|
isContainer?: IsContainer;
|
||||||
|
/** 见 StageCoreConfig.canDropIn */
|
||||||
|
canDropIn?: CanDropIn;
|
||||||
getRootContainer: GetRootContainer;
|
getRootContainer: GetRootContainer;
|
||||||
getRenderDocument: GetRenderDocument;
|
getRenderDocument: GetRenderDocument;
|
||||||
updateDragEl?: UpdateDragEl;
|
updateDragEl?: UpdateDragEl;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user