diff --git a/docs/api/editor/props.md b/docs/api/editor/props.md index 8fc734d7..eb625989 100644 --- a/docs/api/editor/props.md +++ b/docs/api/editor/props.md @@ -923,4 +923,22 @@ const updateDragEl = (el, target) => { ``` + +## disabledMultiSelect + +- **详情:** + +禁止多选 + +- **类型:** `boolean` + +- **默认值:** `false` + +- **示例:** + +```html + +``` \ No newline at end of file diff --git a/packages/editor/src/Editor.vue b/packages/editor/src/Editor.vue index 828ff5b4..8115f1c4 100644 --- a/packages/editor/src/Editor.vue +++ b/packages/editor/src/Editor.vue @@ -177,6 +177,7 @@ provide( disabledDragStart: props.disabledDragStart, renderType: props.renderType, guidesOptions: props.guidesOptions, + disabledMultiSelect: props.disabledMultiSelect, }), ); diff --git a/packages/editor/src/components/TreeNode.vue b/packages/editor/src/components/TreeNode.vue index 95e50152..2b7843a2 100644 --- a/packages/editor/src/components/TreeNode.vue +++ b/packages/editor/src/components/TreeNode.vue @@ -18,7 +18,7 @@ > @@ -35,7 +35,7 @@ -
+
nodeStatus.value.selected); const visible = computed(() => nodeStatus.value.visible); const draggable = computed(() => nodeStatus.value.draggable); -const hasChilren = computed(() => props.data.items?.some((item) => props.nodeStatusMap.get(item.id)?.visible)); +const hasChildren = computed(() => props.data.items?.some((item) => props.nodeStatusMap.get(item.id)?.visible)); const handleDragStart = (event: DragEvent) => { treeEmit?.('node-dragstart', event, props.data); diff --git a/packages/editor/src/editorProps.ts b/packages/editor/src/editorProps.ts index ecbda680..41dd6832 100644 --- a/packages/editor/src/editorProps.ts +++ b/packages/editor/src/editorProps.ts @@ -73,6 +73,7 @@ export interface EditorProps { /** 自定义依赖收集器,复制组件时会将关联依赖一并复制 */ collectorOptions?: CustomTargetOptions; guidesOptions?: Partial; + disabledMultiSelect?: boolean; } export const defaultEditorProps = { @@ -93,4 +94,5 @@ export const defaultEditorProps = { containerHighlightType: ContainerHighlightType.DEFAULT, codeOptions: () => ({}), renderType: RenderType.IFRAME, + disabledMultiSelect: false, }; diff --git a/packages/editor/src/hooks/use-stage.ts b/packages/editor/src/hooks/use-stage.ts index bed9f94e..f0cf7ab8 100644 --- a/packages/editor/src/hooks/use-stage.ts +++ b/packages/editor/src/hooks/use-stage.ts @@ -1,4 +1,4 @@ -import { computed } from 'vue'; +import { computed, watch } from 'vue'; import type { MNode } from '@tmagic/schema'; import StageCore, { GuidesType, RemoveEventData, SortEventData, UpdateEventData } from '@tmagic/stage'; @@ -45,8 +45,20 @@ export const useStage = (stageOptions: StageOptions) => { moveableOptions: stageOptions.moveableOptions, updateDragEl: stageOptions.updateDragEl, guidesOptions: stageOptions.guidesOptions, + disabledMultiSelect: stageOptions.disabledMultiSelect, }); + watch( + () => editorService.get('disabledMultiSelect'), + (disabledMultiSelect) => { + if (disabledMultiSelect) { + stage.disableMultiSelect(); + } else { + stage.enableMultiSelect(); + } + }, + ); + stage.mask.setGuides([ getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY)), getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY)), diff --git a/packages/editor/src/initService.ts b/packages/editor/src/initService.ts index ba7fe69e..4463649a 100644 --- a/packages/editor/src/initService.ts +++ b/packages/editor/src/initService.ts @@ -47,6 +47,16 @@ export const initServiceState = ( }, ); + watch( + () => props.disabledMultiSelect, + (disabledMultiSelect) => { + editorService.set('disabledMultiSelect', disabledMultiSelect || false); + }, + { + immediate: true, + }, + ); + watch( () => props.componentGroupList, (componentGroupList) => componentGroupList && componentListService.setList(componentGroupList), diff --git a/packages/editor/src/layouts/sidebar/layer/use-click.ts b/packages/editor/src/layouts/sidebar/layer/use-click.ts index b0d028cd..8315f64d 100644 --- a/packages/editor/src/layouts/sidebar/layer/use-click.ts +++ b/packages/editor/src/layouts/sidebar/layer/use-click.ts @@ -1,4 +1,4 @@ -import { type ComputedRef, nextTick, type Ref, ref } from 'vue'; +import { computed, type ComputedRef, nextTick, type Ref, ref } from 'vue'; import { throttle } from 'lodash-es'; import { Id, MNode } from '@tmagic/schema'; @@ -13,13 +13,15 @@ export const useClick = ( isCtrlKeyDown: Ref, nodeStatusMap: ComputedRef | undefined>, ) => { + const isMultiSelect = computed(() => isCtrlKeyDown.value && !services?.editorService.get('disabledMultiSelect')); + // 触发画布选中 const select = async (data: MNode) => { if (!data.id) { throw new Error('没有id'); } - if (isCtrlKeyDown.value) { + if (isMultiSelect.value) { multiSelect(data); } else { await services?.editorService.select(data); @@ -70,7 +72,7 @@ export const useClick = ( return; } - if (data.items && data.items.length > 0 && !isCtrlKeyDown.value) { + if (data.items && data.items.length > 0 && !isMultiSelect.value) { updateStatus(nodeStatusMap.value, data.id, { expand: true, }); diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index dfc057ad..22322f21 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -58,6 +58,7 @@ class Editor extends BaseService { highlightNode: null, modifiedNodeIds: new Map(), pageLength: 0, + disabledMultiSelect: false, }); private isHistoryStateChange = false; diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index 261319f1..d1ae40b9 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -133,6 +133,7 @@ export interface StageOptions { updateDragEl: UpdateDragEl; renderType: RenderType; guidesOptions: Partial; + disabledMultiSelect?: boolean; } export interface StoreState { @@ -146,6 +147,7 @@ export interface StoreState { stageLoading: boolean; modifiedNodeIds: Map; pageLength: number; + disabledMultiSelect: boolean; } export type StoreStateKey = keyof StoreState; diff --git a/packages/stage/src/ActionManager.ts b/packages/stage/src/ActionManager.ts index 0bda6269..9a1b940c 100644 --- a/packages/stage/src/ActionManager.ts +++ b/packages/stage/src/ActionManager.ts @@ -57,7 +57,7 @@ const defaultContainerHighlightDuration = 800; */ export default class ActionManager extends EventEmitter { private dr: StageDragResize; - private multiDr: StageMultiDragResize; + private multiDr?: StageMultiDragResize; private highlightLayer: StageHighlight; /** 单选、多选、高亮的容器(蒙层的content) */ private container: HTMLElement; @@ -81,6 +81,8 @@ export default class ActionManager extends EventEmitter { private canSelect: CanSelect; private isContainer: IsContainer; private getRenderDocument: GetRenderDocument; + private disabledMultiSelect = false; + private config: ActionManagerConfig; private mouseMoveHandler = throttle(async (event: MouseEvent): Promise => { if ((event.target as HTMLDivElement)?.classList?.contains('moveable-direction')) { @@ -99,41 +101,24 @@ export default class ActionManager extends EventEmitter { constructor(config: ActionManagerConfig) { super(); + this.config = config; this.container = config.container; this.containerHighlightClassName = config.containerHighlightClassName || CONTAINER_HIGHLIGHT_CLASS_NAME; this.containerHighlightDuration = config.containerHighlightDuration || defaultContainerHighlightDuration; this.containerHighlightType = config.containerHighlightType; + this.disabledMultiSelect = config.disabledMultiSelect ?? false; this.getTargetElement = config.getTargetElement; this.getElementsFromPoint = config.getElementsFromPoint; this.canSelect = config.canSelect || ((el: HTMLElement) => !!el.id); this.getRenderDocument = config.getRenderDocument; this.isContainer = config.isContainer; - const createDrHelper = () => - new DragResizeHelper({ - container: config.container, - updateDragEl: config.updateDragEl, - }); + this.dr = this.createDr(config); + + if (!this.disabledMultiSelect) { + this.multiDr = this.createMultiDr(config); + } - this.dr = new StageDragResize({ - container: config.container, - disabledDragStart: config.disabledDragStart, - moveableOptions: this.changeCallback(config.moveableOptions, false), - dragResizeHelper: createDrHelper(), - getRootContainer: config.getRootContainer, - getRenderDocument: config.getRenderDocument, - markContainerEnd: this.markContainerEnd.bind(this), - delayedMarkContainer: this.delayedMarkContainer.bind(this), - }); - this.multiDr = new StageMultiDragResize({ - container: config.container, - moveableOptions: this.changeCallback(config.moveableOptions, true), - dragResizeHelper: createDrHelper(), - getRootContainer: config.getRootContainer, - getRenderDocument: config.getRenderDocument, - markContainerEnd: this.markContainerEnd.bind(this), - delayedMarkContainer: this.delayedMarkContainer.bind(this), - }); this.highlightLayer = new StageHighlight({ container: config.container, updateDragEl: config.updateDragEl, @@ -142,7 +127,22 @@ export default class ActionManager extends EventEmitter { this.initMouseEvent(); this.initKeyEvent(); - this.initActionEvent(); + } + + public disableMultiSelect() { + this.disabledMultiSelect = true; + if (this.multiDr) { + this.multiDr.destroy(); + this.multiDr = undefined; + } + } + + public enableMultiSelect() { + this.disabledMultiSelect = false; + + if (!this.multiDr) { + this.multiDr = this.createMultiDr(this.config); + } } /** @@ -152,7 +152,7 @@ export default class ActionManager extends EventEmitter { */ public setGuidelines(type: GuidesType, guidelines: number[]): void { this.dr.setGuidelines(type, guidelines); - this.multiDr.setGuidelines(type, guidelines); + this.multiDr?.setGuidelines(type, guidelines); } /** @@ -160,7 +160,7 @@ export default class ActionManager extends EventEmitter { */ public clearGuides(): void { this.dr.clearGuides(); - this.multiDr.clearGuides(); + this.multiDr?.clearGuides(); } /** @@ -170,7 +170,7 @@ export default class ActionManager extends EventEmitter { public updateMoveable(el?: HTMLElement): void { this.dr.updateMoveable(el); // 多选时不可配置元素,因此不存在多选元素变更,不需要传el - this.multiDr.updateMoveable(); + this.multiDr?.updateMoveable(); } /** @@ -197,7 +197,7 @@ export default class ActionManager extends EventEmitter { if (this.dr.getTarget()) { return this.dr.getOption(key); } - if (this.multiDr.targetList.length) { + if (this.multiDr?.targetList.length) { return this.multiDr.getOption(key); } } @@ -254,7 +254,7 @@ export default class ActionManager extends EventEmitter { if (selectedEl?.className.includes(PAGE_CLASS)) { return true; } - return this.multiDr.canSelect(el, selectedEl); + return this.multiDr?.canSelect(el, selectedEl) || false; } public select(el: HTMLElement, event: MouseEvent | undefined): void { @@ -266,7 +266,7 @@ export default class ActionManager extends EventEmitter { public multiSelect(idOrElList: HTMLElement[] | Id[]): void { this.selectedElList = idOrElList.map((idOrEl) => this.getTargetElement(idOrEl)); this.clearSelectStatus(SelectStatus.SELECT); - this.multiDr.multiSelect(this.selectedElList); + this.multiDr?.multiSelect(this.selectedElList); } public getHighlightEl(): HTMLElement | undefined { @@ -287,7 +287,7 @@ export default class ActionManager extends EventEmitter { } // 选中组件不高亮、多选拖拽状态不高亮 - if (el === this.getSelectedEl() || this.multiDr.dragStatus === StageDragStatus.ING) { + if (el === this.getSelectedEl() || this.multiDr?.dragStatus === StageDragStatus.ING) { this.clearHighlight(); return; } @@ -309,7 +309,7 @@ export default class ActionManager extends EventEmitter { */ public clearSelectStatus(selectType: SelectStatus): void { if (selectType === SelectStatus.MULTI_SELECT) { - this.multiDr.clearSelectStatus(); + this.multiDr?.clearSelectStatus(); this.selectedElList = []; } else { this.dr.clearSelectStatus(); @@ -361,10 +361,84 @@ export default class ActionManager extends EventEmitter { this.container.removeEventListener('mouseleave', this.mouseLeaveHandler); this.container.removeEventListener('wheel', this.mouseWheelHandler); this.dr.destroy(); - this.multiDr.destroy(); + this.multiDr?.destroy(); this.highlightLayer.destroy(); } + private createDr(config: ActionManagerConfig) { + const createDrHelper = () => + new DragResizeHelper({ + container: config.container, + updateDragEl: config.updateDragEl, + }); + + const dr = new StageDragResize({ + container: config.container, + disabledDragStart: config.disabledDragStart, + moveableOptions: this.changeCallback(config.moveableOptions, false), + dragResizeHelper: createDrHelper(), + getRootContainer: config.getRootContainer, + getRenderDocument: config.getRenderDocument, + markContainerEnd: this.markContainerEnd.bind(this), + delayedMarkContainer: this.delayedMarkContainer.bind(this), + }); + + dr.on('update', (data: UpdateEventData) => { + // 点击组件并立即拖动的场景,要保证select先被触发,延迟update通知 + setTimeout(() => this.emit('update', data)); + }) + .on('sort', (data: UpdateEventData) => { + // 点击组件并立即拖动的场景,要保证select先被触发,延迟update通知 + setTimeout(() => this.emit('sort', data)); + }) + .on('select-parent', () => { + this.emit('select-parent'); + }) + .on('remove', () => { + const drTarget = this.dr.getTarget(); + if (!drTarget) return; + const data: RemoveEventData = { + data: [{ el: drTarget }], + }; + this.emit('remove', data); + }) + .on('drag-start', (e: OnDragStart) => { + this.emit('drag-start', e); + }); + + return dr; + } + + private createMultiDr(config: ActionManagerConfig) { + const createDrHelper = () => + new DragResizeHelper({ + container: config.container, + updateDragEl: config.updateDragEl, + }); + const multiDr = new StageMultiDragResize({ + container: config.container, + moveableOptions: this.changeCallback(config.moveableOptions, true), + dragResizeHelper: createDrHelper(), + getRootContainer: config.getRootContainer, + getRenderDocument: config.getRenderDocument, + markContainerEnd: this.markContainerEnd.bind(this), + delayedMarkContainer: this.delayedMarkContainer.bind(this), + }); + + multiDr + ?.on('update', (data: UpdateEventData) => { + this.emit('multi-update', data); + }) + .on('change-to-select', async (id: Id) => { + // 如果还在多选状态,不触发切换到单选 + if (this.isMultiSelectStatus) return false; + const el = this.getTargetElement(id); + this.emit('change-to-select', el); + }); + + return multiDr; + } + private changeCallback(options: CustomizeMoveableOptions, isMulti: boolean): CustomizeMoveableOptions { // 在actionManager才能获取到各种参数,在这里传好参数有比较好的扩展性 if (typeof options === 'function') { @@ -450,15 +524,21 @@ export default class ActionManager extends EventEmitter { // 多选启用状态监听 KeyController.global.keydown(ctrl, (e) => { e.inputEvent.preventDefault(); - this.isMultiSelectStatus = true; + if (!this.disabledMultiSelect) { + this.isMultiSelectStatus = true; + } }); // ctrl+tab切到其他窗口,需要将多选状态置为false KeyController.global.on('blur', () => { - this.isMultiSelectStatus = false; + if (!this.disabledMultiSelect) { + this.isMultiSelectStatus = false; + } }); KeyController.global.keyup(ctrl, (e) => { e.inputEvent.preventDefault(); - this.isMultiSelectStatus = false; + if (!this.disabledMultiSelect) { + this.isMultiSelectStatus = false; + } }); // alt健监听,用于启用拖拽组件加入容器状态 @@ -474,46 +554,6 @@ export default class ActionManager extends EventEmitter { }); } - /** - * 处理单选、多选抛出来的事件 - */ - private initActionEvent(): void { - this.dr - .on('update', (data: UpdateEventData) => { - // 点击组件并立即拖动的场景,要保证select先被触发,延迟update通知 - setTimeout(() => this.emit('update', data)); - }) - .on('sort', (data: UpdateEventData) => { - // 点击组件并立即拖动的场景,要保证select先被触发,延迟update通知 - setTimeout(() => this.emit('sort', data)); - }) - .on('select-parent', () => { - this.emit('select-parent'); - }) - .on('remove', () => { - const drTarget = this.dr.getTarget(); - if (!drTarget) return; - const data: RemoveEventData = { - data: [{ el: drTarget }], - }; - this.emit('remove', data); - }) - .on('drag-start', (e: OnDragStart) => { - this.emit('drag-start', e); - }); - - this.multiDr - .on('update', (data: UpdateEventData) => { - this.emit('multi-update', data); - }) - .on('change-to-select', async (id: Id) => { - // 如果还在多选状态,不触发切换到单选 - if (this.isMultiSelectStatus) return false; - const el = this.getTargetElement(id); - this.emit('change-to-select', el); - }); - } - /** * 在down事件中集中cpu处理画布中选中操作渲染,在up事件中再通知外面的编辑器更新 */ diff --git a/packages/stage/src/StageCore.ts b/packages/stage/src/StageCore.ts index 4c799503..ebae458f 100644 --- a/packages/stage/src/StageCore.ts +++ b/packages/stage/src/StageCore.ts @@ -224,6 +224,14 @@ export default class StageCore extends EventEmitter { return this.actionManager.getDragStatus(); } + public disableMultiSelect() { + this.actionManager.disableMultiSelect(); + } + + public enableMultiSelect() { + this.actionManager.enableMultiSelect(); + } + /** * 销毁实例 */ @@ -262,6 +270,7 @@ export default class StageCore extends EventEmitter { moveableOptions: config.moveableOptions, container: this.mask.content, disabledDragStart: config.disabledDragStart, + disabledMultiSelect: config.disabledMultiSelect, canSelect: config.canSelect, isContainer: config.isContainer, updateDragEl: config.updateDragEl, diff --git a/packages/stage/src/types.ts b/packages/stage/src/types.ts index e93ac382..85942e5a 100644 --- a/packages/stage/src/types.ts +++ b/packages/stage/src/types.ts @@ -79,6 +79,7 @@ export interface StageCoreConfig { disabledDragStart?: boolean; renderType?: RenderType; guidesOptions?: Partial; + disabledMultiSelect?: boolean; } export interface ActionManagerConfig { @@ -88,6 +89,7 @@ export interface ActionManagerConfig { containerHighlightType?: ContainerHighlightType; moveableOptions?: CustomizeMoveableOptions; disabledDragStart?: boolean; + disabledMultiSelect?: boolean; canSelect?: CanSelect; isContainer: IsContainer; getRootContainer: GetRootContainer;