mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-24 17:20:21 +08:00
feat(editor): 多选粘贴后同步选中粘贴的多个元素,并支持拖拽,粘贴删除支持多个元素同时撤销到上一步
This commit is contained in:
parent
af2fa3eee4
commit
8c64ea798a
@ -42,9 +42,7 @@ import { beforeAdd, beforePaste, beforeRemove, notifyAddToStage } from '@editor/
|
|||||||
import BaseService from './BaseService';
|
import BaseService from './BaseService';
|
||||||
|
|
||||||
class Editor extends BaseService {
|
class Editor extends BaseService {
|
||||||
private isHistoryStateChange = false;
|
public state = reactive<StoreState>({
|
||||||
|
|
||||||
private state = reactive<StoreState>({
|
|
||||||
root: null,
|
root: null,
|
||||||
page: null,
|
page: null,
|
||||||
parent: null,
|
parent: null,
|
||||||
@ -55,6 +53,7 @@ class Editor extends BaseService {
|
|||||||
modifiedNodeIds: new Map(),
|
modifiedNodeIds: new Map(),
|
||||||
pageLength: 0,
|
pageLength: 0,
|
||||||
});
|
});
|
||||||
|
private isHistoryStateChange = false;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(
|
super(
|
||||||
@ -288,7 +287,28 @@ class Editor extends BaseService {
|
|||||||
* @returns 添加后的节点
|
* @returns 添加后的节点
|
||||||
*/
|
*/
|
||||||
public async multiAdd(configs: MNode[]): Promise<MNode[]> {
|
public async multiAdd(configs: MNode[]): Promise<MNode[]> {
|
||||||
return await Promise.all(configs.map((configItem) => this.add(configItem as AddMNode)));
|
const stage = this.get<StageCore | null>('stage');
|
||||||
|
const newNodes: MNode[] = await Promise.all(
|
||||||
|
configs.map(async (configItem: MNode): Promise<MNode> => {
|
||||||
|
// 新增元素到配置
|
||||||
|
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();
|
||||||
|
|
||||||
|
// 触发选中样式
|
||||||
|
stage?.multiSelect(newNodeIds);
|
||||||
|
|
||||||
|
this.emit('multiAdd', newNodes);
|
||||||
|
|
||||||
|
return newNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -299,10 +319,11 @@ class Editor extends BaseService {
|
|||||||
*/
|
*/
|
||||||
public async add(addNode: AddMNode, parent?: MContainer | null): Promise<MNode> {
|
public async add(addNode: AddMNode, parent?: MContainer | null): Promise<MNode> {
|
||||||
const stage = this.get<StageCore | null>('stage');
|
const stage = this.get<StageCore | null>('stage');
|
||||||
|
// 新增元素到配置
|
||||||
const { parentNode, newNode, layout, isPage } = await beforeAdd(addNode, parent);
|
const { parentNode, newNode, layout, isPage } = await beforeAdd(addNode, parent);
|
||||||
// 将新增元素事件通知到stage以更新渲染
|
// 将新增元素事件通知到stage以更新渲染
|
||||||
await notifyAddToStage(parentNode, newNode, layout);
|
await notifyAddToStage(parentNode, newNode, layout);
|
||||||
// 触发选中样式
|
// 更新编辑器选中元素
|
||||||
await this.select(newNode);
|
await this.select(newNode);
|
||||||
// 增加历史记录
|
// 增加历史记录
|
||||||
this.addModifiedNodeId(newNode.id);
|
this.addModifiedNodeId(newNode.id);
|
||||||
@ -327,9 +348,42 @@ class Editor extends BaseService {
|
|||||||
* @param {Object} node
|
* @param {Object} node
|
||||||
* @return {Object} 删除的组件配置
|
* @return {Object} 删除的组件配置
|
||||||
*/
|
*/
|
||||||
public async remove(nodes: MNode | MNode[]): Promise<(MNode | void)[]> {
|
public async remove(nodeOrNodeList: MNode | MNode[]): Promise<MNode | MNode[]> {
|
||||||
const removeNodes = Array.isArray(nodes) ? nodes : [nodes];
|
if (Array.isArray(nodeOrNodeList)) {
|
||||||
return await Promise.all(removeNodes.map(async (node) => await this.doRemove(node)));
|
// 多选批量删除
|
||||||
|
const nodes = nodeOrNodeList;
|
||||||
|
return this.multiRemove(nodes);
|
||||||
|
}
|
||||||
|
const node = nodeOrNodeList;
|
||||||
|
const removeParent = await beforeRemove(node);
|
||||||
|
// 删除的是页面
|
||||||
|
if (!removeParent) return node;
|
||||||
|
// 更新历史记录
|
||||||
|
this.addModifiedNodeId(removeParent.id);
|
||||||
|
this.pushHistoryState();
|
||||||
|
|
||||||
|
this.emit('remove', node);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量删除
|
||||||
|
* @param nodes 批量删除的节点
|
||||||
|
* @returns 批量删除的节点
|
||||||
|
*/
|
||||||
|
public async multiRemove(nodes: MNode[]): Promise<MNode[]> {
|
||||||
|
await Promise.all(
|
||||||
|
nodes.map(async (removeNode) => {
|
||||||
|
await beforeRemove(removeNode);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const nodeIds = nodes.map((node) => node.id);
|
||||||
|
this.addModifiedNodeId(nodeIds.join('-'));
|
||||||
|
this.pushHistoryState();
|
||||||
|
|
||||||
|
this.emit('multiRemove', nodes);
|
||||||
|
return nodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -683,47 +737,6 @@ class Editor extends BaseService {
|
|||||||
page,
|
page,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doRemove(node: MNode): Promise<MNode | void> {
|
|
||||||
const beforeRemoveRes = beforeRemove(node);
|
|
||||||
if (!beforeRemoveRes) return;
|
|
||||||
const { parent, root } = beforeRemoveRes;
|
|
||||||
|
|
||||||
const stage = this.get<StageCore | null>('stage');
|
|
||||||
stage?.remove({ id: node.id, root: cloneDeep(this.get('root')) });
|
|
||||||
|
|
||||||
if (node.type === NodeType.PAGE) {
|
|
||||||
this.state.pageLength -= 1;
|
|
||||||
|
|
||||||
if (root.items[0]) {
|
|
||||||
await this.select(root.items[0]);
|
|
||||||
stage?.select(root.items[0].id);
|
|
||||||
} else {
|
|
||||||
this.set('node', null);
|
|
||||||
this.set('nodes', []);
|
|
||||||
this.set('parent', null);
|
|
||||||
this.set('page', null);
|
|
||||||
this.set('stage', null);
|
|
||||||
this.set('highlightNode', null);
|
|
||||||
this.resetModifiedNodeId();
|
|
||||||
historyService.reset();
|
|
||||||
|
|
||||||
this.emit('remove', node);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await this.select(parent);
|
|
||||||
stage?.select(parent.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addModifiedNodeId(parent.id);
|
|
||||||
this.pushHistoryState();
|
|
||||||
|
|
||||||
this.emit('remove', node);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditorService = Editor;
|
export type EditorService = Editor;
|
||||||
|
@ -1,18 +1,19 @@
|
|||||||
import { toRaw } from 'vue';
|
import { toRaw } from 'vue';
|
||||||
import { cloneDeep } from 'lodash-es';
|
import { cloneDeep, isEmpty } from 'lodash-es';
|
||||||
|
|
||||||
import { Id, MApp, MContainer, MNode, NodeType } from '@tmagic/schema';
|
import { Id, MApp, MContainer, MNode, NodeType } from '@tmagic/schema';
|
||||||
import StageCore from '@tmagic/stage';
|
import StageCore from '@tmagic/stage';
|
||||||
import { isPage } from '@tmagic/utils';
|
import { isPage } from '@tmagic/utils';
|
||||||
|
|
||||||
import editorService from '@editor/services/editor';
|
import editorService from '@editor/services/editor';
|
||||||
|
import historyService from '@editor/services/history';
|
||||||
import propsService from '@editor/services/props';
|
import propsService from '@editor/services/props';
|
||||||
import { AddMNode, Layout, PastePosition } from '@editor/type';
|
import { AddMNode, Layout, PastePosition } from '@editor/type';
|
||||||
import { fixNodeLeft, generatePageNameByApp, getInitPositionStyle, getNodeIndex } from '@editor/utils/editor';
|
import { fixNodeLeft, generatePageNameByApp, getInitPositionStyle, getNodeIndex } from '@editor/utils/editor';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 粘贴前置操作:返回分配了新id以及校准了坐标的配置
|
* 粘贴前置操作:返回分配了新id以及校准了坐标的配置
|
||||||
* @param position 粘贴的坐标,如果为空则默认在元素坐标基础上偏移10px
|
* @param position 粘贴的坐标
|
||||||
* @param config 待粘贴的元素配置(复制时保存的那份配置)
|
* @param config 待粘贴的元素配置(复制时保存的那份配置)
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
@ -25,7 +26,8 @@ export const beforePaste = async (position: PastePosition, config: MNode[]) => {
|
|||||||
const pasteConfigs: MNode[] = await Promise.all(
|
const pasteConfigs: MNode[] = await Promise.all(
|
||||||
config.map(async (configItem: MNode): Promise<MNode> => {
|
config.map(async (configItem: MNode): Promise<MNode> => {
|
||||||
let pastePosition = position;
|
let pastePosition = position;
|
||||||
if (curNode.items) {
|
if (!isEmpty(pastePosition) && curNode.items) {
|
||||||
|
// 如果没有传入粘贴坐标则可能为键盘操作,不再转换
|
||||||
// 如果粘贴时选中了容器,则将元素粘贴到容器内,坐标需要转换为相对于容器的坐标
|
// 如果粘贴时选中了容器,则将元素粘贴到容器内,坐标需要转换为相对于容器的坐标
|
||||||
pastePosition = getPositionInContainer(pastePosition, curNode.id);
|
pastePosition = getPositionInContainer(pastePosition, curNode.id);
|
||||||
}
|
}
|
||||||
@ -146,9 +148,10 @@ export const notifyAddToStage = async (parentNode: MContainer, newNode: MNode, l
|
|||||||
* @param node 待删除的节点
|
* @param node 待删除的节点
|
||||||
* @returns 父级元素,root根元素
|
* @returns 父级元素,root根元素
|
||||||
*/
|
*/
|
||||||
export const beforeRemove = (node: MNode): { parent: MContainer; root: MApp } | void => {
|
export const beforeRemove = async (node: MNode): Promise<MContainer | void> => {
|
||||||
if (!node?.id) return;
|
if (!node?.id) return;
|
||||||
|
|
||||||
|
const stage = editorService.get<StageCore | null>('stage');
|
||||||
const root = editorService.get<MApp | null>('root');
|
const root = editorService.get<MApp | null>('root');
|
||||||
|
|
||||||
if (!root) throw new Error('没有root');
|
if (!root) throw new Error('没有root');
|
||||||
@ -162,8 +165,33 @@ export const beforeRemove = (node: MNode): { parent: MContainer; root: MApp } |
|
|||||||
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
|
if (typeof index !== 'number' || index === -1) throw new Error('找不要删除的节点');
|
||||||
// 从配置中删除元素
|
// 从配置中删除元素
|
||||||
parent.items?.splice(index, 1);
|
parent.items?.splice(index, 1);
|
||||||
return {
|
|
||||||
parent,
|
// 通知stage更新
|
||||||
root,
|
stage?.remove({ id: node.id, root: cloneDeep(root) });
|
||||||
};
|
|
||||||
|
if (node.type === NodeType.PAGE) {
|
||||||
|
editorService.state.pageLength -= 1;
|
||||||
|
|
||||||
|
if (root.items[0]) {
|
||||||
|
await editorService.select(root.items[0]);
|
||||||
|
stage?.select(root.items[0].id);
|
||||||
|
} else {
|
||||||
|
editorService.set('node', null);
|
||||||
|
editorService.set('nodes', []);
|
||||||
|
editorService.set('parent', null);
|
||||||
|
editorService.set('page', null);
|
||||||
|
editorService.set('stage', null);
|
||||||
|
editorService.set('highlightNode', null);
|
||||||
|
editorService.resetModifiedNodeId();
|
||||||
|
historyService.reset();
|
||||||
|
|
||||||
|
editorService.emit('remove', node);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await editorService.select(parent);
|
||||||
|
stage?.select(parent.id);
|
||||||
|
}
|
||||||
|
return parent;
|
||||||
};
|
};
|
||||||
|
@ -128,8 +128,7 @@ export default class StageCore extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
this.selectedDomList.push(el);
|
this.selectedDomList.push(el);
|
||||||
}
|
}
|
||||||
this.multiDr.multiSelect(this.selectedDomList);
|
this.multiSelect(this.selectedDomList);
|
||||||
this.emit('multiSelect', this.selectedDomList);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 要先触发select,在触发update
|
// 要先触发select,在触发update
|
||||||
@ -222,6 +221,16 @@ export default class StageCore extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 多选
|
||||||
|
* @param domList 多选节点
|
||||||
|
*/
|
||||||
|
public async multiSelect(idOrElList: HTMLElement[] | Id[]): Promise<void> {
|
||||||
|
const elList = await Promise.all(idOrElList.map(async (idOrEl) => await this.getTargetElement(idOrEl)));
|
||||||
|
this.multiDr.multiSelect(elList);
|
||||||
|
this.emit('multiSelect', elList);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新选中的节点
|
* 更新选中的节点
|
||||||
* @param data 更新的数据
|
* @param data 更新的数据
|
||||||
|
@ -70,7 +70,6 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
});
|
});
|
||||||
this.moveableForMulti?.destroy();
|
this.moveableForMulti?.destroy();
|
||||||
this.multiMoveableHelper?.clear();
|
this.multiMoveableHelper?.clear();
|
||||||
|
|
||||||
this.moveableForMulti = new Moveable(
|
this.moveableForMulti = new Moveable(
|
||||||
this.container,
|
this.container,
|
||||||
this.getOptions({
|
this.getOptions({
|
||||||
@ -208,11 +207,12 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
* @return {MoveableOptions} moveable options参数
|
* @return {MoveableOptions} moveable options参数
|
||||||
*/
|
*/
|
||||||
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
private getOptions(options: MoveableOptions = {}): MoveableOptions {
|
||||||
let { moveableOptions = {} } = this.core.config;
|
let { multiMoveableOptions = {} } = this.core.config;
|
||||||
|
|
||||||
if (typeof moveableOptions === 'function') {
|
if (typeof multiMoveableOptions === 'function') {
|
||||||
moveableOptions = moveableOptions(this.core);
|
multiMoveableOptions = multiMoveableOptions(this.core);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
defaultGroupRotate: 0,
|
defaultGroupRotate: 0,
|
||||||
defaultGroupOrigin: '50% 50%',
|
defaultGroupOrigin: '50% 50%',
|
||||||
@ -225,7 +225,7 @@ export default class StageMultiDragResize extends EventEmitter {
|
|||||||
origin: true,
|
origin: true,
|
||||||
padding: { left: 0, top: 0, right: 0, bottom: 0 },
|
padding: { left: 0, top: 0, right: 0, bottom: 0 },
|
||||||
...options,
|
...options,
|
||||||
...moveableOptions,
|
...multiMoveableOptions,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,6 +39,7 @@ export type StageCoreConfig = {
|
|||||||
containerHighlightClassName: string;
|
containerHighlightClassName: string;
|
||||||
containerHighlightDuration: number;
|
containerHighlightDuration: number;
|
||||||
moveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions;
|
moveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions;
|
||||||
|
multiMoveableOptions?: ((core?: StageCore) => MoveableOptions) | MoveableOptions;
|
||||||
/** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
|
/** runtime 的HTML地址,可以是一个HTTP地址,如果和编辑器不同域,需要设置跨域,也可以是一个相对或绝对路径 */
|
||||||
runtimeUrl?: string;
|
runtimeUrl?: string;
|
||||||
render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
|
render?: (renderer: StageCore) => Promise<HTMLElement> | HTMLElement;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user