mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-27 21:10:36 +08:00
feat(editor,stage): 新增 containerHighlightAddOnly 配置
开启后仅新增组件(从组件列表拖入)时才识别容器,画布中拖动已有组件不再被加入其他容器,避免误操作
This commit is contained in:
parent
be7b38f97e
commit
e6cf694a03
@ -1019,6 +1019,18 @@ alt: 按住alt键启动识别
|
||||
|
||||
|
||||
- **类型:** `'default' | 'alt' | ''`
|
||||
|
||||
## containerHighlightAddOnly
|
||||
|
||||
- **详情:**
|
||||
|
||||
是否只有新增组件(从组件列表拖入新组件)时才启用识别容器
|
||||
|
||||
开启后,在画布中拖动已有组件时不再识别容器,即已有组件不能通过拖动加入其他容器;新增组件仍然按 [containerHighlightType](#containerHighlightType) 配置的方式识别
|
||||
|
||||
- **默认值:** `false`
|
||||
|
||||
- **类型:** `boolean`
|
||||
|
||||
## stageRect
|
||||
|
||||
|
||||
@ -69,10 +69,11 @@
|
||||
|
||||
## delayedMarkContainer
|
||||
|
||||
- **类型**:`(event: MouseEvent, excludeElList?: Element[]) => NodeJS.Timeout | undefined`
|
||||
- **类型**:`(event: MouseEvent, excludeElList?: Element[], isAdd?: boolean) => NodeJS.Timeout | undefined`
|
||||
- **参数**:
|
||||
- `event`:鼠标事件
|
||||
- `excludeElList`:计算鼠标所在容器时要排除的元素列表
|
||||
- `isAdd`:当前操作是否为新增组件(从组件列表拖入新组件),`containerHighlightType` 为 `addOnly` 时只有新增才会标记容器
|
||||
- **详情**:
|
||||
|
||||
鼠标拖拽着元素,在容器上方悬停,延迟一段时间后,对容器进行标记,如果悬停时间够长将标记成功,悬停时间短,调用方通过返回的timeoutId取消标记
|
||||
|
||||
@ -218,6 +218,7 @@ const stageOptions: StageOptions = {
|
||||
containerHighlightClassName: props.containerHighlightClassName,
|
||||
containerHighlightDuration: props.containerHighlightDuration,
|
||||
containerHighlightType: props.containerHighlightType,
|
||||
containerHighlightAddOnly: props.containerHighlightAddOnly,
|
||||
disabledDragStart: props.disabledDragStart,
|
||||
renderType: props.renderType,
|
||||
guidesOptions: props.guidesOptions,
|
||||
|
||||
@ -82,6 +82,11 @@ export interface EditorProps {
|
||||
containerHighlightDuration?: number;
|
||||
/** 拖入画布中容器时,识别容器的操作类型 */
|
||||
containerHighlightType?: ContainerHighlightType;
|
||||
/**
|
||||
* 是否仅在新增组件(从组件列表拖入新组件)时才启用识别容器,
|
||||
* 开启后在画布中拖动已有组件不会识别容器,默认 false
|
||||
*/
|
||||
containerHighlightAddOnly?: boolean;
|
||||
/** 画布大小 */
|
||||
stageRect?: StageRect;
|
||||
/** monaco editor 的配置 */
|
||||
@ -163,6 +168,7 @@ export const defaultEditorProps = {
|
||||
containerHighlightClassName: CONTAINER_HIGHLIGHT_CLASS_NAME,
|
||||
containerHighlightDuration: 800,
|
||||
containerHighlightType: ContainerHighlightType.DEFAULT,
|
||||
containerHighlightAddOnly: false,
|
||||
disabledShowSrc: false,
|
||||
disabledDataSource: false,
|
||||
disabledCodeBlock: false,
|
||||
|
||||
@ -28,6 +28,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
containerHighlightClassName: stageOptions.containerHighlightClassName,
|
||||
containerHighlightDuration: stageOptions.containerHighlightDuration,
|
||||
containerHighlightType: stageOptions.containerHighlightType,
|
||||
containerHighlightAddOnly: stageOptions.containerHighlightAddOnly,
|
||||
disabledDragStart: stageOptions.disabledDragStart,
|
||||
renderType: stageOptions.renderType,
|
||||
canSelect: (el, event, stop) => {
|
||||
|
||||
@ -145,6 +145,6 @@ const dragHandler = (e: DragEvent) => {
|
||||
|
||||
if (timeout || !stage.value) return;
|
||||
|
||||
timeout = stage.value.delayedMarkContainer(e);
|
||||
timeout = stage.value.delayedMarkContainer(e, [], true);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -178,6 +178,11 @@ export interface StageOptions {
|
||||
containerHighlightClassName?: string;
|
||||
containerHighlightDuration?: number;
|
||||
containerHighlightType?: ContainerHighlightType;
|
||||
/**
|
||||
* 是否仅在新增组件(从组件列表拖入新组件)时才启用识别容器,
|
||||
* 开启后在画布中拖动已有组件不会识别容器,默认 false
|
||||
*/
|
||||
containerHighlightAddOnly?: boolean;
|
||||
disabledDragStart?: boolean;
|
||||
render?: (stage: StageCore) => HTMLDivElement | void | Promise<HTMLDivElement | void>;
|
||||
moveableOptions?: CustomizeMoveableOptions;
|
||||
|
||||
@ -21,6 +21,7 @@ describe('defaultEditorProps', () => {
|
||||
test('containerHighlight 默认值', () => {
|
||||
expect(defaultEditorProps.containerHighlightDuration).toBe(800);
|
||||
expect(typeof defaultEditorProps.containerHighlightClassName).toBe('string');
|
||||
expect(defaultEditorProps.containerHighlightAddOnly).toBe(false);
|
||||
});
|
||||
|
||||
test('数组/对象工厂函数返回空值', () => {
|
||||
|
||||
@ -135,6 +135,6 @@ describe('ComponentListPanel', () => {
|
||||
const item = wrapper.find('.component-item');
|
||||
await item.trigger('drag', { clientX: 0, clientY: 0 });
|
||||
await item.trigger('drag', { clientX: 0, clientY: 0 });
|
||||
expect(stage.delayedMarkContainer).toHaveBeenCalled();
|
||||
expect(stage.delayedMarkContainer).toHaveBeenCalledWith(expect.anything(), [], true);
|
||||
});
|
||||
});
|
||||
|
||||
@ -84,6 +84,8 @@ export default class ActionManager extends EventEmitter {
|
||||
private containerHighlightDuration: number;
|
||||
/** 将组件加入容器的操作方式 */
|
||||
private containerHighlightType?: ContainerHighlightType;
|
||||
/** 是否仅在新增组件时才启用将组件加入容器 */
|
||||
private containerHighlightAddOnly = false;
|
||||
private isAltKeydown = false;
|
||||
private getTargetElement: GetTargetElement;
|
||||
private getElementsFromPoint: GetElementsFromPoint;
|
||||
@ -124,6 +126,7 @@ export default class ActionManager extends EventEmitter {
|
||||
this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS_NAME;
|
||||
this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration;
|
||||
this.containerHighlightType = config.containerHighlightType;
|
||||
this.containerHighlightAddOnly = config.containerHighlightAddOnly ?? false;
|
||||
this.disabledMultiSelect = config.disabledMultiSelect ?? false;
|
||||
this.alwaysMultiSelect = config.alwaysMultiSelect ?? false;
|
||||
this.getTargetElement = config.getTargetElement;
|
||||
@ -428,10 +431,15 @@ export default class ActionManager extends EventEmitter {
|
||||
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
||||
* @param event 鼠标事件
|
||||
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
||||
* @param isAdd 当前操作是否为新增组件(从组件列表拖入),开启 containerHighlightAddOnly 时只有新增才会标记
|
||||
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
||||
*/
|
||||
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined {
|
||||
if (this.canAddToContainer()) {
|
||||
public delayedMarkContainer(
|
||||
event: MouseEvent,
|
||||
excludeElList: Element[] = [],
|
||||
isAdd = false,
|
||||
): NodeJS.Timeout | undefined {
|
||||
if (this.canAddToContainer(isAdd)) {
|
||||
return globalThis.setTimeout(() => {
|
||||
this.addContainerHighlightClassName(event, excludeElList);
|
||||
}, this.containerHighlightDuration);
|
||||
@ -609,9 +617,13 @@ export default class ActionManager extends EventEmitter {
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前状态下能否将组件加入容器,默认是鼠标悬停一段时间加入,alt模式则是按住alt+鼠标悬停一段时间加入
|
||||
* 当前状态下能否将组件加入容器,默认是鼠标悬停一段时间加入,alt模式则是按住alt+鼠标悬停一段时间加入;
|
||||
* 开启containerHighlightAddOnly时,画布中拖动已有组件不能加入容器
|
||||
* @param isAdd 当前操作是否为新增组件
|
||||
*/
|
||||
private canAddToContainer(): boolean {
|
||||
private canAddToContainer(isAdd = false): boolean {
|
||||
if (this.containerHighlightAddOnly && !isAdd) return false;
|
||||
|
||||
return (
|
||||
this.containerHighlightType === ContainerHighlightType.DEFAULT ||
|
||||
(this.containerHighlightType === ContainerHighlightType.ALT && this.isAltKeydown)
|
||||
@ -624,10 +636,10 @@ export default class ActionManager extends EventEmitter {
|
||||
*/
|
||||
private markContainerEnd(): HTMLElement | null {
|
||||
const doc = this.getRenderDocument();
|
||||
if (doc && this.canAddToContainer()) {
|
||||
return removeClassNameByClassName(doc, this.containerHighlightClassName);
|
||||
}
|
||||
return null;
|
||||
if (!doc) return null;
|
||||
|
||||
const markedContainer = removeClassNameByClassName(doc, this.containerHighlightClassName);
|
||||
return this.canAddToContainer() ? markedContainer : null;
|
||||
}
|
||||
|
||||
private initMouseEvent(): void {
|
||||
|
||||
@ -263,10 +263,15 @@ export default class StageCore extends EventEmitter {
|
||||
* 标记的作用:1、高亮容器,给用户一个加入容器的交互感知;2、释放鼠标后,通过标记的标志找到要加入的容器
|
||||
* @param event 鼠标事件
|
||||
* @param excludeElList 计算鼠标所在容器时要排除的元素列表
|
||||
* @param isAdd 当前操作是否为新增组件(从组件列表拖入),开启 containerHighlightAddOnly 时只有新增才会标记
|
||||
* @returns timeoutId,调用方在鼠标移走时要取消该timeout,阻止标记
|
||||
*/
|
||||
public delayedMarkContainer(event: MouseEvent, excludeElList: Element[] = []): NodeJS.Timeout | undefined {
|
||||
return this.actionManager?.delayedMarkContainer(event, excludeElList);
|
||||
public delayedMarkContainer(
|
||||
event: MouseEvent,
|
||||
excludeElList: Element[] = [],
|
||||
isAdd = false,
|
||||
): NodeJS.Timeout | undefined {
|
||||
return this.actionManager?.delayedMarkContainer(event, excludeElList, isAdd);
|
||||
}
|
||||
|
||||
public getMoveableOption<K extends keyof MoveableOptions>(key: K): MoveableOptions[K] | undefined {
|
||||
@ -368,6 +373,7 @@ export default class StageCore extends EventEmitter {
|
||||
containerHighlightClassName: config.containerHighlightClassName,
|
||||
containerHighlightDuration: config.containerHighlightDuration,
|
||||
containerHighlightType: config.containerHighlightType,
|
||||
containerHighlightAddOnly: config.containerHighlightAddOnly,
|
||||
moveableOptions: config.moveableOptions,
|
||||
container: this.mask!.content,
|
||||
disabledDragStart: config.disabledDragStart,
|
||||
|
||||
@ -53,7 +53,11 @@ export type GetTargetElement = (id: Id) => HTMLElement | null;
|
||||
/** render提供的接口,通过坐标获得坐标下所有HTML元素数组 */
|
||||
export type GetElementsFromPoint = (point: Point) => HTMLElement[];
|
||||
export type GetRenderDocument = () => Document | undefined;
|
||||
export type DelayedMarkContainer = (event: MouseEvent, exclude: Element[]) => NodeJS.Timeout | undefined;
|
||||
export type DelayedMarkContainer = (
|
||||
event: MouseEvent,
|
||||
exclude?: Element[],
|
||||
isAdd?: boolean,
|
||||
) => NodeJS.Timeout | undefined;
|
||||
export type MarkContainerEnd = () => HTMLElement | null;
|
||||
export type GetRootContainer = () => HTMLDivElement | undefined;
|
||||
|
||||
@ -74,6 +78,11 @@ export interface StageCoreConfig {
|
||||
containerHighlightClassName?: string;
|
||||
containerHighlightDuration?: number;
|
||||
containerHighlightType?: ContainerHighlightType;
|
||||
/**
|
||||
* 是否仅在新增组件(从组件列表拖入新组件)时才启用将组件拖入容器,
|
||||
* 开启后在画布中拖动已有组件不会识别容器,默认 false
|
||||
*/
|
||||
containerHighlightAddOnly?: boolean;
|
||||
moveableOptions?: CustomizeMoveableOptions;
|
||||
/** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
|
||||
runtimeUrl?: string;
|
||||
@ -102,6 +111,8 @@ export interface ActionManagerConfig {
|
||||
containerHighlightClassName?: string;
|
||||
containerHighlightDuration?: number;
|
||||
containerHighlightType?: ContainerHighlightType;
|
||||
/** 见 StageCoreConfig.containerHighlightAddOnly */
|
||||
containerHighlightAddOnly?: boolean;
|
||||
moveableOptions?: CustomizeMoveableOptions;
|
||||
disabledDragStart?: boolean;
|
||||
disabledMultiSelect?: boolean;
|
||||
|
||||
@ -275,6 +275,84 @@ describe('ActionManager - 交互与事件', () => {
|
||||
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(true);
|
||||
});
|
||||
|
||||
test('delayedMarkContainer 未配置 containerHighlightType 时不标记容器', () => {
|
||||
am = new ActionManager(createConfig({ isContainer: async () => true }));
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin())).toBeUndefined();
|
||||
});
|
||||
|
||||
test('delayedMarkContainer alt 模式下需按住 alt 才标记容器', () => {
|
||||
am = new ActionManager(
|
||||
createConfig({
|
||||
containerHighlightType: ContainerHighlightType.ALT,
|
||||
isContainer: async () => true,
|
||||
}),
|
||||
);
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin())).toBeUndefined();
|
||||
(am as any).isAltKeydown = true;
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin())).toBeDefined();
|
||||
});
|
||||
|
||||
test('containerHighlightAddOnly 开启时拖动已有组件不标记容器', () => {
|
||||
const containerEl = globalThis.document.createElement('div');
|
||||
containerEl.dataset.tmagicId = 'container_1';
|
||||
am = new ActionManager(
|
||||
createConfig({
|
||||
containerHighlightType: ContainerHighlightType.DEFAULT,
|
||||
containerHighlightAddOnly: true,
|
||||
getElementsFromPoint: () => [containerEl],
|
||||
isContainer: async () => true,
|
||||
}),
|
||||
);
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin(), [globalThis.document.createElement('div')])).toBeUndefined();
|
||||
vi.advanceTimersByTime(900);
|
||||
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(false);
|
||||
});
|
||||
|
||||
test('containerHighlightAddOnly 开启时新增组件仍按 containerHighlightType 标记容器', async () => {
|
||||
const containerEl = globalThis.document.createElement('div');
|
||||
containerEl.dataset.tmagicId = 'container_1';
|
||||
am = new ActionManager(
|
||||
createConfig({
|
||||
containerHighlightType: ContainerHighlightType.DEFAULT,
|
||||
containerHighlightAddOnly: true,
|
||||
getElementsFromPoint: () => [containerEl],
|
||||
isContainer: async () => true,
|
||||
}),
|
||||
);
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin(), [], true)).toBeDefined();
|
||||
vi.advanceTimersByTime(900);
|
||||
await Promise.resolve();
|
||||
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(true);
|
||||
});
|
||||
|
||||
test('containerHighlightAddOnly 开启且 containerHighlightType 为 alt 时新增组件也需按住 alt', () => {
|
||||
am = new ActionManager(
|
||||
createConfig({
|
||||
containerHighlightType: ContainerHighlightType.ALT,
|
||||
containerHighlightAddOnly: true,
|
||||
isContainer: async () => true,
|
||||
}),
|
||||
);
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin(), [], true)).toBeUndefined();
|
||||
(am as any).isAltKeydown = true;
|
||||
expect(am.delayedMarkContainer(mouseAtOrigin(), [], true)).toBeDefined();
|
||||
});
|
||||
|
||||
test('containerHighlightAddOnly 开启时结束标记仍清理高亮但不返回容器', () => {
|
||||
const containerEl = globalThis.document.createElement('div');
|
||||
containerEl.classList.add('tmagic-stage-container-highlight');
|
||||
globalThis.document.body.appendChild(containerEl);
|
||||
am = new ActionManager(
|
||||
createConfig({
|
||||
containerHighlightType: ContainerHighlightType.DEFAULT,
|
||||
containerHighlightAddOnly: true,
|
||||
}),
|
||||
);
|
||||
|
||||
expect((am as any).markContainerEnd()).toBeNull();
|
||||
expect(containerEl.classList.contains('tmagic-stage-container-highlight')).toBe(false);
|
||||
});
|
||||
|
||||
test('updateMoveableOptions / getDragStatus 可调用', () => {
|
||||
am = new ActionManager(createConfig());
|
||||
expect(() => am.updateMoveableOptions()).not.toThrow();
|
||||
|
||||
@ -181,6 +181,16 @@ describe('StageCore', () => {
|
||||
stage.destroy();
|
||||
});
|
||||
|
||||
test('delayedMarkContainer 将新增标识透传给 actionManager', async () => {
|
||||
const { host, stage } = createStage();
|
||||
await stage.mount(host);
|
||||
const spy = vi.spyOn(stage.actionManager!, 'delayedMarkContainer');
|
||||
const event = mouseAtOrigin();
|
||||
stage.delayedMarkContainer(event, [], true);
|
||||
expect(spy).toHaveBeenCalledWith(event, [], true);
|
||||
stage.destroy();
|
||||
});
|
||||
|
||||
test('autoScrollIntoView 选中时调用 mask.observerIntersection', async () => {
|
||||
const host = globalThis.document.createElement('div');
|
||||
globalThis.document.body.appendChild(host);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user