diff --git a/packages/editor/src/layouts/workspace/Workspace.vue b/packages/editor/src/layouts/workspace/Workspace.vue index bb151225..7537da8d 100644 --- a/packages/editor/src/layouts/workspace/Workspace.vue +++ b/packages/editor/src/layouts/workspace/Workspace.vue @@ -72,7 +72,7 @@ export default defineComponent({ }) .keydown([ctrl, 'v'], (e) => { e.inputEvent.preventDefault(); - nodes.value && services?.editorService.paste(); + nodes.value && services?.editorService.paste({ offsetX: 10, offsetY: 10 }); }) .keydown([ctrl, 'x'], (e) => { e.inputEvent.preventDefault(); diff --git a/packages/editor/src/services/editor.ts b/packages/editor/src/services/editor.ts index 1251a5a8..c3bcd845 100644 --- a/packages/editor/src/services/editor.ts +++ b/packages/editor/src/services/editor.ts @@ -32,12 +32,14 @@ import { change2Fixed, COPY_STORAGE_KEY, Fixed2Other, + fixNodePosition, getInitPositionStyle, getNodeIndex, isFixed, setLayout, } from '../utils/editor'; -import { beforeAdd, beforePaste, beforeRemove, notifyAddToStage } from '../utils/operator'; +import { beforePaste, beforeRemove, getAddParent } from '../utils/operator'; +import { propsService } from '..'; import BaseService from './BaseService'; @@ -60,6 +62,7 @@ class Editor extends BaseService { [ 'getLayout', 'select', + 'doAdd', 'add', 'remove', 'update', @@ -281,34 +284,30 @@ class Editor extends BaseService { this.set('nodes', nodes); } - /** - * 批量向容器添加节点 - * @param configs 将要添加的节点数组 - * @returns 添加后的节点 - */ - public async multiAdd(configs: MNode[]): Promise { + public async doAdd(node: MNode, parent: MContainer): Promise { + const root = this.get('root'); + const curNode = this.get('node'); const stage = this.get('stage'); - const newNodes: MNode[] = await Promise.all( - configs.map(async (configItem: MNode): Promise => { - // 新增元素到配置 - const { parentNode, newNode, layout } = await beforeAdd(configItem as AddMNode); - // 将新增元素事件通知到stage以更新渲染 - await notifyAddToStage(parentNode, newNode, layout); - return newNode; - }), - ); - const newNodeIds: Id[] = newNodes.map((node) => node.id); - // 增加历史记录 多选不可能选中page - this.addModifiedNodeId(newNodeIds.join('-')); - this.pushHistoryState(); + if ((parent?.type === NodeType.ROOT || curNode.type === NodeType.ROOT) && node.type !== NodeType.PAGE) { + throw new Error('app下不能添加组件'); + } - // 触发选中样式 - stage?.multiSelect(newNodeIds); + const layout = await this.getLayout(toRaw(parent), node as MNode); + node.style = getInitPositionStyle(node.style, layout); - this.emit('multiAdd', newNodes); + await stage?.add({ config: cloneDeep(node), parent: cloneDeep(parent), root: cloneDeep(root) }); - return newNodes; + node.style = fixNodePosition(node, parent, stage); + + await stage?.update({ config: cloneDeep(node), root: cloneDeep(root) }); + + // 新增节点添加到配置中 + parent?.items?.push(node); + + this.addModifiedNodeId(node.id); + + return node; } /** @@ -317,30 +316,47 @@ class Editor extends BaseService { * @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点 * @returns 添加后的节点 */ - public async add(addNode: AddMNode, parent?: MContainer | null): Promise { + public async add(addNode: AddMNode | MNode[], parent?: MContainer | null): Promise { const stage = this.get('stage'); - // 新增元素到配置 - const { parentNode, newNode, layout, isPage } = await beforeAdd(addNode, parent); - // 将新增元素事件通知到stage以更新渲染 - await notifyAddToStage(parentNode, newNode, layout); - // 更新编辑器选中元素 - await this.select(newNode); - // 增加历史记录 - this.addModifiedNodeId(newNode.id); - if (!isPage) { - this.pushHistoryState(); - } - if (isPage) { - this.state.pageLength += 1; + const parentNode = parent && typeof parent !== 'function' ? parent : getAddParent(addNode); + if (!parentNode) throw new Error('未找到父元素'); + + // 新增多个组件只存在于粘贴多个组件,粘贴的是一个完整的config,所以不再需要getPropsValue + const addNodes = []; + if (!Array.isArray(addNode)) { + const { type, inputEvent, ...config } = addNode; + + if (!type) throw new Error('组件类型不能为空'); + + addNodes.push({ ...toRaw(await propsService.getPropsValue(type, config)) }); } else { - // 新增页面,这个时候页面还有渲染出来,此时select会出错,在runtime-ready的时候回去select - stage?.select(newNode.id); + addNodes.push(...addNode); } - this.emit('add', newNode); + const newNodes = await Promise.all(addNodes.map((node) => this.doAdd(node, parentNode))); - return newNode; + if (newNodes.length > 1) { + const newNodeIds = newNodes.map((node) => node.id); + // 触发选中样式 + stage?.multiSelect(newNodeIds); + await this.multiSelect(newNodeIds); + } else { + await this.select(newNodes[0]); + + if (isPage(newNodes[0])) { + this.state.pageLength += 1; + } else { + // 新增页面,这个时候页面还有渲染出来,此时select会出错,在runtime-ready的时候回去select + stage?.select(newNodes[0].id); + } + } + + this.pushHistoryState(); + + this.emit('add', newNodes); + + return newNodes.length > 1 ? newNodes[0] : newNodes; } /** @@ -492,14 +508,14 @@ class Editor extends BaseService { * @param position 粘贴的坐标 * @returns 添加后的组件节点配置 */ - public async paste(position: PastePosition = {}): Promise { - const config = await storageService.getItem(COPY_STORAGE_KEY); + public async paste(position: PastePosition = {}): Promise { + const config: MNode[] = await storageService.getItem(COPY_STORAGE_KEY); - if (!config) return; + if (!Array.isArray(config)) return []; const pasteConfigs = await beforePaste(position, config); - return await this.multiAdd(pasteConfigs); + return this.add(pasteConfigs); } /** @@ -590,7 +606,7 @@ class Editor extends BaseService { return srcValue; } }); - newConfig.style = getInitPositionStyle(newConfig.style, layout, target, stage); + newConfig.style = getInitPositionStyle(newConfig.style, layout); target.items.push(newConfig); @@ -633,7 +649,7 @@ class Editor extends BaseService { const node = toRaw(this.get('node')); if (!node || isPage(node)) return; - const { style, id } = node; + const { style, id, type } = node; if (!style || style.position !== 'absolute') return; if (top && !isNumber(style.top)) return; @@ -641,6 +657,7 @@ class Editor extends BaseService { this.update({ id, + type, style: { ...style, left: Number(style.left) + left, diff --git a/packages/editor/src/utils/editor.ts b/packages/editor/src/utils/editor.ts index e297010e..85848fe7 100644 --- a/packages/editor/src/utils/editor.ts +++ b/packages/editor/src/utils/editor.ts @@ -88,10 +88,10 @@ export const getRelativeStyle = (style: Record = {}): Record = {}, parentNode: MNode, stage: StageCore) => { - let height = style.height || 0; +const getMiddleTop = (node: MNode, parentNode: MNode, stage: StageCore | null) => { + let height = node.style?.height || 0; - if (!stage || typeof style.top !== 'undefined' || !parentNode.style) return style.top; + if (!stage || typeof node.style?.top !== 'undefined' || !parentNode.style) return node.style?.top; if (!isNumber(height)) { height = 0; @@ -103,20 +103,15 @@ const getMiddleTop = (style: Record = {}, parentNode: MNode, stage: const { scrollTop = 0, wrapperHeight } = stage.mask; return (wrapperHeight - height) / 2 + scrollTop; } + return (parentHeight - height) / 2; }; -export const getInitPositionStyle = ( - style: Record = {}, - layout: Layout, - parentNode: MNode, - stage: StageCore, -) => { +export const getInitPositionStyle = (style: Record = {}, layout: Layout) => { if (layout === Layout.ABSOLUTE) { return { ...style, position: 'absolute', - top: getMiddleTop(style, parentNode, stage), }; } @@ -230,3 +225,15 @@ export const fixNodeLeft = (config: MNode, parent: MContainer, doc?: Document) = return config.style.left; }; + +export const fixNodePosition = (config: MNode, parent: MContainer, stage: StageCore | null) => { + if (config.style?.position !== 'absolute') { + return config.style; + } + + return { + ...(config.style || {}), + top: getMiddleTop(config, parent, stage), + left: fixNodeLeft(config, parent, stage?.renderer.contentWindow?.document), + }; +}; diff --git a/packages/editor/src/utils/operator.ts b/packages/editor/src/utils/operator.ts index e424f29e..efde3712 100644 --- a/packages/editor/src/utils/operator.ts +++ b/packages/editor/src/utils/operator.ts @@ -8,8 +8,8 @@ import { isPage } from '@tmagic/utils'; import editorService from '../services/editor'; import historyService from '../services/history'; import propsService from '../services/props'; -import { AddMNode, Layout, PastePosition } from '../type'; -import { fixNodeLeft, generatePageNameByApp, getInitPositionStyle, getNodeIndex } from '../utils/editor'; +import { AddMNode, PastePosition } from '../type'; +import { generatePageNameByApp, getInitPositionStyle, getNodeIndex } from '../utils/editor'; /** * 粘贴前置操作:返回分配了新id以及校准了坐标的配置 @@ -17,7 +17,7 @@ import { fixNodeLeft, generatePageNameByApp, getInitPositionStyle, getNodeIndex * @param config 待粘贴的元素配置(复制时保存的那份配置) * @returns */ -export const beforePaste = async (position: PastePosition, config: MNode[]) => { +export const beforePaste = async (position: PastePosition, config: MNode[]): Promise => { if (!config[0]?.style) return config; const curNode = editorService.get('node'); // 将数组中第一个元素的坐标作为参照点 @@ -69,54 +69,6 @@ export const beforePaste = async (position: PastePosition, config: MNode[]) => { return pasteConfigs; }; -/** - * 新增元素前置操作,实现了在编辑器中新增元素节点,并返回新增元素信息以供stage更新 - * @param addNode 待添加元素的配置 - * @param parent 父级容器(可选) - * @returns 新增的元素,父级元素,布局方式,是否为根页面 - */ -export const beforeAdd = async ( - addNode: AddMNode, - parent?: MContainer | null, -): Promise<{ parentNode: MContainer; newNode: MNode; layout: Layout; isPage: boolean }> => { - // 加入inputEvent是为给业务扩展时可以获取到更多的信息,只有在使用拖拽添加组件时才有改对象 - const { type, inputEvent, ...config } = addNode; - const curNode = editorService.get('node'); - - let parentNode: MContainer | undefined; - const isPage = type === NodeType.PAGE; - - if (isPage) { - parentNode = editorService.get('root'); - // 由于支持中间件扩展,在parent参数为undefined时,parent会变成next函数 - } else if (parent && typeof parent !== 'function') { - parentNode = parent; - } else if (curNode.items) { - parentNode = curNode; - } else { - parentNode = editorService.getParentById(curNode.id, false); - } - - if (!parentNode) throw new Error('未找到父元素'); - - const layout = await editorService.getLayout(toRaw(parentNode), addNode as MNode); - const newNode = { ...toRaw(await propsService.getPropsValue(type, config)) }; - newNode.style = getInitPositionStyle(newNode.style, layout, parentNode, editorService.get('stage')); - - if ((parentNode?.type === NodeType.ROOT || curNode.type === NodeType.ROOT) && newNode.type !== NodeType.PAGE) { - throw new Error('app下不能添加组件'); - } - // 新增节点添加到配置中 - parentNode?.items?.push(newNode); - // 返回新增信息以供stage更新 - return { - parentNode, - newNode, - layout, - isPage, - }; -}; - /** * 将元素粘贴到容器内时,将相对于画布坐标转换为相对于容器的坐标 * @param position PastePosition 粘贴时相对于画布的坐标 @@ -135,27 +87,6 @@ export const getPositionInContainer = (position: PastePosition = {}, id: Id) => }; }; -/** - * 将新增元素事件通知到stage以更新渲染 - * @param parentNode 父元素 - * @param newNode 当前新增元素 - * @param layout 布局方式 - */ -export const notifyAddToStage = async (parentNode: MContainer, newNode: MNode, layout: Layout) => { - const stage = editorService.get('stage'); - const root = editorService.get('root'); - - await stage?.add({ config: cloneDeep(newNode), parent: cloneDeep(parentNode), root: cloneDeep(root) }); - - if (layout === Layout.ABSOLUTE) { - const fixedLeft = fixNodeLeft(newNode, parentNode, stage?.renderer.contentWindow?.document); - if (typeof fixedLeft !== 'undefined' && newNode.style) { - newNode.style.left = fixedLeft; - await stage?.update({ config: cloneDeep(newNode), root: cloneDeep(root) }); - } - } -}; - /** * 删除前置操作:实现了在编辑器中删除元素节点,并返回父级元素信息以供stage更新 * @param node 待删除的节点 @@ -208,3 +139,25 @@ export const beforeRemove = async (node: MNode): Promise => { } return parent; }; + +export const getAddParent = (addNode: AddMNode | MNode[]) => { + const curNode = editorService.get('node'); + + let parentNode; + if (!Array.isArray(addNode) && isPage(addNode as MNode)) { + parentNode = editorService.get('root'); + } else if (curNode.items) { + parentNode = curNode; + } else { + parentNode = editorService.getParentById(curNode.id, false); + } + return parentNode; +}; + +export const getDefaultConfig = async (addNode: AddMNode, parentNode: MContainer) => { + const { type, inputEvent, ...config } = addNode; + const layout = await editorService.getLayout(toRaw(parentNode), addNode as MNode); + const newNode = { ...toRaw(await propsService.getPropsValue(type, config)) }; + newNode.style = getInitPositionStyle(newNode.style, layout); + return newNode; +}; diff --git a/packages/editor/tests/unit/services/editor.spec.ts b/packages/editor/tests/unit/services/editor.spec.ts index 0c234d21..3957e69b 100644 --- a/packages/editor/tests/unit/services/editor.spec.ts +++ b/packages/editor/tests/unit/services/editor.spec.ts @@ -208,7 +208,9 @@ describe('add', () => { // 添加后会选中这个节点 const node = editorService.get('node'); const parent = editorService.get('parent'); - expect(node.id).toBe(newNode.id); + if (!Array.isArray(newNode)) { + expect(node.id).toBe(newNode.id); + } expect(parent.items).toHaveLength(3); }); @@ -222,7 +224,9 @@ describe('add', () => { }); const node = editorService.get('node'); const parent = editorService.get('parent'); - expect(node.id).toBe(newNode.id); + if (!Array.isArray(newNode)) { + expect(node.id).toBe(newNode.id); + } expect(parent.items).toHaveLength(3); }); @@ -237,7 +241,9 @@ describe('add', () => { rootNode, ); const node = editorService.get('node'); - expect(node.id).toBe(newNode.id); + if (!Array.isArray(newNode)) { + expect(node.id).toBe(newNode.id); + } expect(rootNode.items.length).toBe(2); }); diff --git a/packages/stage/src/StageCore.ts b/packages/stage/src/StageCore.ts index b70f6155..8227385c 100644 --- a/packages/stage/src/StageCore.ts +++ b/packages/stage/src/StageCore.ts @@ -337,7 +337,7 @@ export default class StageCore extends EventEmitter { } } - public getAddContainerHighlightClassNameTimeout(event: MouseEvent, exclude: Element[] = []) { + public getAddContainerHighlightClassNameTimeout(event: MouseEvent, exclude: Element[] = []): NodeJS.Timeout { return globalThis.setTimeout(() => { this.addContainerHighlightClassName(event, exclude); }, this.containerHighlightDuration);