mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-06-05 00:25:19 +08:00
feat(editor): 新增 DSL 修改方法的 doNotSelect 选项
- add/remove/sort/alignCenter/moveToContainer/paste 新增 doNotSelect 选项,控制操作后是否自动触发选中变化 - doUpdate/doRemove 改为始终同步当前选中列表中的节点引用,避免 state 持有已被替换/已删除的过期节点 - 顺手修复 doUpdate 在 splice(-1) 时误改最后一个选中项的 bug - 移除 update/doUpdate 的 selectedAfterUpdate 参数(语义已内化),move 不再暴露无意义的 doNotSelect - 新增 safeOptions / safeParent 辅助函数,兜底插件机制将 dispatch 注入到形参位置的场景 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1e69bc221d
commit
05e512b1fe
@ -332,6 +332,9 @@ editorService.highlight("text_123");
|
|||||||
|
|
||||||
- {`MContainer`} parent 指定的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
- {`MContainer`} parent 指定的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
||||||
|
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- {Promise<`MNode` | `MNode`[]>} 新增的组件或组件集合
|
- {Promise<`MNode` | `MNode`[]>} 新增的组件或组件集合
|
||||||
|
|
||||||
@ -352,6 +355,8 @@ editorService.highlight("text_123");
|
|||||||
- **[扩展支持](../../guide/editor-expand#行为扩展):** 是
|
- **[扩展支持](../../guide/editor-expand#行为扩展):** 是
|
||||||
- **参数:**
|
- **参数:**
|
||||||
- {`MNode`} node 要删除的节点
|
- {`MNode`} node 要删除的节点
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- `{Promise<void>}`
|
- `{Promise<void>}`
|
||||||
@ -366,6 +371,8 @@ editorService.highlight("text_123");
|
|||||||
|
|
||||||
- **参数:**
|
- **参数:**
|
||||||
- {`MNode` | `MNode`[])} node 要删除的节点或节点集合
|
- {`MNode` | `MNode`[])} node 要删除的节点或节点集合
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- `{Promise<void>}`
|
- `{Promise<void>}`
|
||||||
@ -390,7 +397,6 @@ editorService.highlight("text_123");
|
|||||||
- {`MNode`} config 新的节点
|
- {`MNode`} config 新的节点
|
||||||
- `{Object}` data 可选配置
|
- `{Object}` data 可选配置
|
||||||
- {`ChangeRecord`[]} changeRecords 变更记录
|
- {`ChangeRecord`[]} changeRecords 变更记录
|
||||||
- `{boolean}` selectedAfterUpdate 更新后是否将新节点同步到当前选中节点列表
|
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- `{Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }>}` 更新前后的节点信息
|
- `{Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }>}` 更新前后的节点信息
|
||||||
@ -405,6 +411,8 @@ editorService.highlight("text_123");
|
|||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
节点中应该要有id,不然不知道要更新哪个节点
|
节点中应该要有id,不然不知道要更新哪个节点
|
||||||
|
|
||||||
|
当被更新节点正好在当前选中列表中时,state 会自动同步到新的节点引用,无需调用方处理
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## update
|
## update
|
||||||
@ -415,7 +423,6 @@ editorService.highlight("text_123");
|
|||||||
- {`MNode` | `MNode`[]} config 新的节点或节点集合
|
- {`MNode` | `MNode`[]} config 新的节点或节点集合
|
||||||
- `{Object}` data 可选配置
|
- `{Object}` data 可选配置
|
||||||
- {`ChangeRecord`[]} changeRecords 变更记录
|
- {`ChangeRecord`[]} changeRecords 变更记录
|
||||||
- `{boolean}` selectedAfterUpdate 更新后是否同步到当前选中节点列表
|
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- {Promise<`MNode` | `MNode`[]>} 新的节点或节点集合
|
- {Promise<`MNode` | `MNode`[]>} 新的节点或节点集合
|
||||||
@ -439,6 +446,8 @@ editorService.highlight("text_123");
|
|||||||
- **参数:**
|
- **参数:**
|
||||||
- `{ string | number }` id1
|
- `{ string | number }` id1
|
||||||
- `{ string | number }` id2
|
- `{ string | number }` id2
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- `{Promise<void>}`
|
- `{Promise<void>}`
|
||||||
@ -502,6 +511,10 @@ editorService.highlight("text_123");
|
|||||||
<<< @/../packages/editor/src/type.ts#PastePosition{ts}
|
<<< @/../packages/editor/src/type.ts#PastePosition{ts}
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
- `{TargetOptions}` collectorOptions 可选的依赖收集器配置
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- {Promise<`MNode` | `MNode`[]>} 添加后的组件节点配置
|
- {Promise<`MNode` | `MNode`[]>} 添加后的组件节点配置
|
||||||
|
|
||||||
@ -535,6 +548,8 @@ editorService.highlight("text_123");
|
|||||||
|
|
||||||
- **参数:**
|
- **参数:**
|
||||||
- {`MNode` | `MNode`[]} config 需要居中的组件或者组件集合
|
- {`MNode` | `MNode`[]} config 需要居中的组件或者组件集合
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- {Promise<`MNode` | `MNode`[]>}
|
- {Promise<`MNode` | `MNode`[]>}
|
||||||
@ -572,6 +587,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
|||||||
- **参数:**
|
- **参数:**
|
||||||
- {`MNode`} config 需要移动的节点
|
- {`MNode`} config 需要移动的节点
|
||||||
- `{string | number}` targetId 容器ID
|
- `{string | number}` targetId 容器ID
|
||||||
|
- `{Object}` options 可选配置
|
||||||
|
- `{boolean}` doNotSelect 移动后是否不更新当前选中节点(默认 false)
|
||||||
|
|
||||||
- **返回:**
|
- **返回:**
|
||||||
- Promise<`MNode` | undefined>
|
- Promise<`MNode` | undefined>
|
||||||
|
|||||||
@ -65,6 +65,25 @@ import type { HistoryOpContext } from '@editor/utils/editor-history';
|
|||||||
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
|
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
|
||||||
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经过 BaseService 的插件 / 中间件包装后,源方法的最后一个形参可能被注入为 dispatch 函数
|
||||||
|
* 当 options 形参位置被注入为函数(或为 null)时,将其归一为空对象,避免后续逻辑误读
|
||||||
|
*/
|
||||||
|
const safeOptions = <T extends object>(options: unknown): T => {
|
||||||
|
const empty = {};
|
||||||
|
if (!options || typeof options === 'function') return empty as T;
|
||||||
|
return options as T;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 经过 BaseService 的插件 / 中间件包装后,源方法的形参可能被注入为 dispatch 函数
|
||||||
|
* 当 parent 形参位置被注入为函数(或为空值)时,归一为 null,由调用方继续走默认 parent 逻辑
|
||||||
|
*/
|
||||||
|
const safeParent = (parent: unknown): MContainer | null => {
|
||||||
|
if (!parent || typeof parent === 'function') return null;
|
||||||
|
return parent as MContainer;
|
||||||
|
};
|
||||||
|
|
||||||
class Editor extends BaseService {
|
class Editor extends BaseService {
|
||||||
public state: StoreState = reactive({
|
public state: StoreState = reactive({
|
||||||
root: null,
|
root: null,
|
||||||
@ -349,9 +368,18 @@ class Editor extends BaseService {
|
|||||||
* 向指点容器添加组件节点
|
* 向指点容器添加组件节点
|
||||||
* @param addConfig 将要添加的组件节点配置
|
* @param addConfig 将要添加的组件节点配置
|
||||||
* @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
* @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
||||||
|
* @param options 可选配置
|
||||||
|
* @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
||||||
* @returns 添加后的节点
|
* @returns 添加后的节点
|
||||||
*/
|
*/
|
||||||
public async add(addNode: AddMNode | MNode[], parent?: MContainer | null): Promise<MNode | MNode[]> {
|
public async add(
|
||||||
|
addNode: AddMNode | MNode[],
|
||||||
|
parent?: MContainer | null,
|
||||||
|
options?: { doNotSelect?: boolean },
|
||||||
|
): Promise<MNode | MNode[]> {
|
||||||
|
const safeParentNode = safeParent(parent);
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
this.captureSelectionBeforeOp();
|
this.captureSelectionBeforeOp();
|
||||||
|
|
||||||
const stage = this.get('stage');
|
const stage = this.get('stage');
|
||||||
@ -374,25 +402,29 @@ class Editor extends BaseService {
|
|||||||
if ((isPage(node) || isPageFragment(node)) && root) {
|
if ((isPage(node) || isPageFragment(node)) && root) {
|
||||||
return this.doAdd(node, root);
|
return this.doAdd(node, root);
|
||||||
}
|
}
|
||||||
const parentNode = parent && typeof parent !== 'function' ? parent : getAddParent(node);
|
const parentNode = safeParentNode ?? getAddParent(node);
|
||||||
if (!parentNode) throw new Error('未找到父元素');
|
if (!parentNode) throw new Error('未找到父元素');
|
||||||
return this.doAdd(node, parentNode);
|
return this.doAdd(node, parentNode);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (newNodes.length > 1) {
|
if (newNodes.length > 1) {
|
||||||
const newNodeIds = newNodes.map((node) => node.id);
|
if (!doNotSelect) {
|
||||||
// 触发选中样式
|
const newNodeIds = newNodes.map((node) => node.id);
|
||||||
stage?.multiSelect(newNodeIds);
|
// 触发选中样式
|
||||||
await this.multiSelect(newNodeIds);
|
stage?.multiSelect(newNodeIds);
|
||||||
|
await this.multiSelect(newNodeIds);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.select(newNodes[0]);
|
if (!doNotSelect) {
|
||||||
|
await this.select(newNodes[0]);
|
||||||
|
}
|
||||||
|
|
||||||
if (isPage(newNodes[0])) {
|
if (isPage(newNodes[0])) {
|
||||||
this.state.pageLength += 1;
|
this.state.pageLength += 1;
|
||||||
} else if (isPageFragment(newNodes[0])) {
|
} else if (isPageFragment(newNodes[0])) {
|
||||||
this.state.pageFragmentLength += 1;
|
this.state.pageFragmentLength += 1;
|
||||||
} else {
|
} else if (!doNotSelect) {
|
||||||
// 新增页面,这个时候页面还有渲染出来,此时select会出错,在runtime-ready的时候回去select
|
// 新增页面,这个时候页面还有渲染出来,此时select会出错,在runtime-ready的时候回去select
|
||||||
stage?.select(newNodes[0].id);
|
stage?.select(newNodes[0].id);
|
||||||
}
|
}
|
||||||
@ -421,7 +453,9 @@ class Editor extends BaseService {
|
|||||||
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
public async doRemove(node: MNode): Promise<void> {
|
public async doRemove(node: MNode, options?: { doNotSelect?: boolean }): Promise<void> {
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
const root = this.get('root');
|
const root = this.get('root');
|
||||||
if (!root) throw new Error('root不能为空');
|
if (!root) throw new Error('root不能为空');
|
||||||
|
|
||||||
@ -437,6 +471,24 @@ class Editor extends BaseService {
|
|||||||
const stage = this.get('stage');
|
const stage = this.get('stage');
|
||||||
stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
|
stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
|
||||||
|
|
||||||
|
if (doNotSelect) {
|
||||||
|
// 当被删除节点正好在当前选中列表中时,必须从 state 中移除引用,避免 state 持有已删除节点(与 doNotSelect 无关)
|
||||||
|
const selectedNodes = this.get('nodes');
|
||||||
|
const removedSelectedIndex = selectedNodes.findIndex((n: MNode) => `${n.id}` === `${node.id}`);
|
||||||
|
if (removedSelectedIndex !== -1) {
|
||||||
|
const nextSelected = [...selectedNodes];
|
||||||
|
nextSelected.splice(removedSelectedIndex, 1);
|
||||||
|
this.set('nodes', nextSelected);
|
||||||
|
}
|
||||||
|
// 同理,如果被删除的是当前 page,也清空 state.page,避免持有已删除页面
|
||||||
|
if (isPage(node) || isPageFragment(node)) {
|
||||||
|
const currentPage = this.get('page');
|
||||||
|
if (currentPage && `${currentPage.id}` === `${node.id}`) {
|
||||||
|
this.set('page', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selectDefault = async (pages: MNode[]) => {
|
const selectDefault = async (pages: MNode[]) => {
|
||||||
if (pages[0]) {
|
if (pages[0]) {
|
||||||
await this.select(pages[0]);
|
await this.select(pages[0]);
|
||||||
@ -453,14 +505,20 @@ class Editor extends BaseService {
|
|||||||
if (isPage(node)) {
|
if (isPage(node)) {
|
||||||
this.state.pageLength -= 1;
|
this.state.pageLength -= 1;
|
||||||
|
|
||||||
await selectDefault(rootItems);
|
if (!doNotSelect) {
|
||||||
|
await selectDefault(rootItems);
|
||||||
|
}
|
||||||
} else if (isPageFragment(node)) {
|
} else if (isPageFragment(node)) {
|
||||||
this.state.pageFragmentLength -= 1;
|
this.state.pageFragmentLength -= 1;
|
||||||
|
|
||||||
await selectDefault(rootItems);
|
if (!doNotSelect) {
|
||||||
|
await selectDefault(rootItems);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await this.select(parent);
|
if (!doNotSelect) {
|
||||||
stage?.select(parent.id);
|
await this.select(parent);
|
||||||
|
stage?.select(parent.id);
|
||||||
|
}
|
||||||
|
|
||||||
this.addModifiedNodeId(parent.id);
|
this.addModifiedNodeId(parent.id);
|
||||||
}
|
}
|
||||||
@ -473,9 +531,13 @@ class Editor extends BaseService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 删除组件
|
* 删除组件
|
||||||
* @param {Object} node
|
* @param {Object} node 要删除的节点或节点集合
|
||||||
|
* @param options 可选配置
|
||||||
|
* @param options.doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
|
||||||
*/
|
*/
|
||||||
public async remove(nodeOrNodeList: MNode | MNode[]): Promise<void> {
|
public async remove(nodeOrNodeList: MNode | MNode[], options?: { doNotSelect?: boolean }): Promise<void> {
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
this.captureSelectionBeforeOp();
|
this.captureSelectionBeforeOp();
|
||||||
|
|
||||||
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
||||||
@ -499,7 +561,7 @@ class Editor extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(nodes.map((node) => this.doRemove(node)));
|
await Promise.all(nodes.map((node) => this.doRemove(node, { doNotSelect })));
|
||||||
|
|
||||||
if (removedItems.length > 0 && pageForOp) {
|
if (removedItems.length > 0 && pageForOp) {
|
||||||
this.pushOpHistory('remove', { removedItems }, pageForOp);
|
this.pushOpHistory('remove', { removedItems }, pageForOp);
|
||||||
@ -510,10 +572,7 @@ class Editor extends BaseService {
|
|||||||
|
|
||||||
public async doUpdate(
|
public async doUpdate(
|
||||||
config: MNode,
|
config: MNode,
|
||||||
{
|
{ changeRecords = [] }: { changeRecords?: ChangeRecord[] } = {},
|
||||||
changeRecords = [],
|
|
||||||
selectedAfterUpdate = true,
|
|
||||||
}: { changeRecords?: ChangeRecord[]; selectedAfterUpdate?: boolean } = {},
|
|
||||||
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
||||||
const root = this.get('root');
|
const root = this.get('root');
|
||||||
if (!root) throw new Error('root为空');
|
if (!root) throw new Error('root为空');
|
||||||
@ -557,12 +616,12 @@ class Editor extends BaseService {
|
|||||||
|
|
||||||
parentNodeItems[index] = newConfig;
|
parentNodeItems[index] = newConfig;
|
||||||
|
|
||||||
// 将update后的配置更新到nodes中
|
// 当被更新节点正好在当前选中列表中时,必须同步引用,否则 state 会持有已被替换的旧节点
|
||||||
if (selectedAfterUpdate) {
|
const selectedNodes = this.get('nodes');
|
||||||
const nodes = this.get('nodes');
|
const targetIndex = selectedNodes.findIndex((nodeItem: MNode) => `${nodeItem.id}` === `${newConfig.id}`);
|
||||||
const targetIndex = nodes.findIndex((nodeItem: MNode) => `${nodeItem.id}` === `${newConfig.id}`);
|
if (targetIndex !== -1) {
|
||||||
nodes.splice(targetIndex, 1, newConfig);
|
selectedNodes.splice(targetIndex, 1, newConfig);
|
||||||
this.set('nodes', [...nodes]);
|
this.set('nodes', [...selectedNodes]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isPage(newConfig) || isPageFragment(newConfig)) {
|
if (isPage(newConfig) || isPageFragment(newConfig)) {
|
||||||
@ -586,7 +645,7 @@ class Editor extends BaseService {
|
|||||||
*/
|
*/
|
||||||
public async update(
|
public async update(
|
||||||
config: MNode | MNode[],
|
config: MNode | MNode[],
|
||||||
data: { changeRecords?: ChangeRecord[]; selectedAfterUpdate?: boolean } = {},
|
data: { changeRecords?: ChangeRecord[] } = {},
|
||||||
): Promise<MNode | MNode[]> {
|
): Promise<MNode | MNode[]> {
|
||||||
this.captureSelectionBeforeOp();
|
this.captureSelectionBeforeOp();
|
||||||
|
|
||||||
@ -620,9 +679,13 @@ class Editor extends BaseService {
|
|||||||
* 将id为id1的组件移动到id为id2的组件位置上,例如:[1,2,3,4] -> sort(1,3) -> [2,1,3,4]
|
* 将id为id1的组件移动到id为id2的组件位置上,例如:[1,2,3,4] -> sort(1,3) -> [2,1,3,4]
|
||||||
* @param id1 组件ID
|
* @param id1 组件ID
|
||||||
* @param id2 组件ID
|
* @param id2 组件ID
|
||||||
|
* @param options 可选配置
|
||||||
|
* @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
||||||
* @returns void
|
* @returns void
|
||||||
*/
|
*/
|
||||||
public async sort(id1: Id, id2: Id): Promise<void> {
|
public async sort(id1: Id, id2: Id, options?: { doNotSelect?: boolean }): Promise<void> {
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
this.captureSelectionBeforeOp();
|
this.captureSelectionBeforeOp();
|
||||||
|
|
||||||
const root = this.get('root');
|
const root = this.get('root');
|
||||||
@ -642,7 +705,9 @@ class Editor extends BaseService {
|
|||||||
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
||||||
|
|
||||||
await this.update(parent);
|
await this.update(parent);
|
||||||
await this.select(node);
|
if (!doNotSelect) {
|
||||||
|
await this.select(node);
|
||||||
|
}
|
||||||
|
|
||||||
this.get('stage')?.update({
|
this.get('stage')?.update({
|
||||||
config: cloneDeep(node),
|
config: cloneDeep(node),
|
||||||
@ -682,9 +747,18 @@ class Editor extends BaseService {
|
|||||||
/**
|
/**
|
||||||
* 从localStorage中获取节点,然后添加到当前容器中
|
* 从localStorage中获取节点,然后添加到当前容器中
|
||||||
* @param position 粘贴的坐标
|
* @param position 粘贴的坐标
|
||||||
|
* @param collectorOptions 可选的依赖收集器配置
|
||||||
|
* @param options 可选配置
|
||||||
|
* @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
||||||
* @returns 添加后的组件节点配置
|
* @returns 添加后的组件节点配置
|
||||||
*/
|
*/
|
||||||
public async paste(position: PastePosition = {}, collectorOptions?: TargetOptions): Promise<MNode | MNode[] | void> {
|
public async paste(
|
||||||
|
position: PastePosition = {},
|
||||||
|
collectorOptions?: TargetOptions,
|
||||||
|
options?: { doNotSelect?: boolean },
|
||||||
|
): Promise<MNode | MNode[] | void> {
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
const config: MNode[] = storageService.getItem(COPY_STORAGE_KEY);
|
||||||
if (!Array.isArray(config)) return;
|
if (!Array.isArray(config)) return;
|
||||||
|
|
||||||
@ -704,7 +778,7 @@ class Editor extends BaseService {
|
|||||||
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
propsService.replaceRelateId(config, pasteConfigs, collectorOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.add(pasteConfigs, parent);
|
return this.add(pasteConfigs, parent, { doNotSelect });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
public async doPaste(config: MNode[], position: PastePosition = {}): Promise<MNode[]> {
|
||||||
@ -732,9 +806,13 @@ class Editor extends BaseService {
|
|||||||
/**
|
/**
|
||||||
* 将指点节点设置居中
|
* 将指点节点设置居中
|
||||||
* @param config 组件节点配置
|
* @param config 组件节点配置
|
||||||
|
* @param options 可选配置
|
||||||
|
* @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
||||||
* @returns 当前组件节点配置
|
* @returns 当前组件节点配置
|
||||||
*/
|
*/
|
||||||
public async alignCenter(config: MNode | MNode[]): Promise<MNode | MNode[]> {
|
public async alignCenter(config: MNode | MNode[], options?: { doNotSelect?: boolean }): Promise<MNode | MNode[]> {
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
const nodes = Array.isArray(config) ? config : [config];
|
const nodes = Array.isArray(config) ? config : [config];
|
||||||
const stage = this.get('stage');
|
const stage = this.get('stage');
|
||||||
|
|
||||||
@ -742,10 +820,12 @@ class Editor extends BaseService {
|
|||||||
|
|
||||||
const newNode = await this.update(newNodes);
|
const newNode = await this.update(newNodes);
|
||||||
|
|
||||||
if (newNodes.length > 1) {
|
if (!doNotSelect) {
|
||||||
await stage?.multiSelect(newNodes.map((node) => node.id));
|
if (newNodes.length > 1) {
|
||||||
} else {
|
await stage?.multiSelect(newNodes.map((node) => node.id));
|
||||||
await stage?.select(newNodes[0].id);
|
} else {
|
||||||
|
await stage?.select(newNodes[0].id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return newNode;
|
return newNode;
|
||||||
@ -808,8 +888,16 @@ class Editor extends BaseService {
|
|||||||
* 移动到指定容器中
|
* 移动到指定容器中
|
||||||
* @param config 需要移动的节点
|
* @param config 需要移动的节点
|
||||||
* @param targetId 容器ID
|
* @param targetId 容器ID
|
||||||
|
* @param options 可选配置
|
||||||
|
* @param options.doNotSelect 移动后是否不更新当前选中节点(默认 false)
|
||||||
*/
|
*/
|
||||||
public async moveToContainer(config: MNode, targetId: Id): Promise<MNode | undefined> {
|
public async moveToContainer(
|
||||||
|
config: MNode,
|
||||||
|
targetId: Id,
|
||||||
|
options?: { doNotSelect?: boolean },
|
||||||
|
): Promise<MNode | undefined> {
|
||||||
|
const { doNotSelect = false } = safeOptions<{ doNotSelect?: boolean }>(options);
|
||||||
|
|
||||||
this.captureSelectionBeforeOp();
|
this.captureSelectionBeforeOp();
|
||||||
|
|
||||||
const root = this.get('root');
|
const root = this.get('root');
|
||||||
@ -837,7 +925,9 @@ class Editor extends BaseService {
|
|||||||
|
|
||||||
target.items.push(newConfig);
|
target.items.push(newConfig);
|
||||||
|
|
||||||
await stage.select(targetId);
|
if (!doNotSelect) {
|
||||||
|
await stage.select(targetId);
|
||||||
|
}
|
||||||
|
|
||||||
const targetParent = this.getParentById(target.id);
|
const targetParent = this.getParentById(target.id);
|
||||||
await stage.update({
|
await stage.update({
|
||||||
@ -846,8 +936,10 @@ class Editor extends BaseService {
|
|||||||
root: cloneDeep(root),
|
root: cloneDeep(root),
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.select(newConfig);
|
if (!doNotSelect) {
|
||||||
stage.select(newConfig.id);
|
await this.select(newConfig);
|
||||||
|
stage.select(newConfig.id);
|
||||||
|
}
|
||||||
|
|
||||||
this.addModifiedNodeId(target.id);
|
this.addModifiedNodeId(target.id);
|
||||||
this.addModifiedNodeId(parent.id);
|
this.addModifiedNodeId(parent.id);
|
||||||
|
|||||||
@ -286,6 +286,23 @@ describe('add', () => {
|
|||||||
),
|
),
|
||||||
).rejects.toThrowError('app下不能添加组件');
|
).rejects.toThrowError('app下不能添加组件');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('doNotSelect: true 不更新选中节点', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
const beforeNodeId = editorService.get('node')?.id;
|
||||||
|
expect(beforeNodeId).toBe(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
const newNode = await editorService.add({ type: 'text' }, null, { doNotSelect: true });
|
||||||
|
|
||||||
|
// 节点已被添加到 dsl
|
||||||
|
const addedId = Array.isArray(newNode) ? newNode[0].id : newNode.id;
|
||||||
|
const parentInfo = editorService.getParentById(addedId);
|
||||||
|
expect(parentInfo?.items).toHaveLength(3);
|
||||||
|
|
||||||
|
// 但当前选中节点保持原状(未自动选中新增节点)
|
||||||
|
expect(editorService.get('node')?.id).toBe(beforeNodeId);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('remove', () => {
|
describe('remove', () => {
|
||||||
@ -316,6 +333,36 @@ describe('remove', () => {
|
|||||||
test('undefine', async () => {
|
test('undefine', async () => {
|
||||||
expect(() => editorService.remove({ id: NodeId.ERROR_NODE_ID, type: 'text' })).rejects.toThrow();
|
expect(() => editorService.remove({ id: NodeId.ERROR_NODE_ID, type: 'text' })).rejects.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('doNotSelect: true 不更新选中节点', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
// 选中 NODE_ID,删除另外一个 NODE_ID2
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
const beforeNodeId = editorService.get('node')?.id;
|
||||||
|
expect(beforeNodeId).toBe(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
await editorService.remove({ id: NodeId.NODE_ID2, type: 'text' }, { doNotSelect: true });
|
||||||
|
|
||||||
|
// 节点已被删除
|
||||||
|
expect(editorService.getNodeById(NodeId.NODE_ID2)).toBeNull();
|
||||||
|
// 当前选中节点保持原状(未自动选中父节点)
|
||||||
|
expect(editorService.get('node')?.id).toBe(beforeNodeId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('被删除节点正好是当前选中节点时,state 强制移除引用', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
// 选中 NODE_ID 后再删除 NODE_ID 自身
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
expect(editorService.get('node')?.id).toBe(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
// 即使 doNotSelect: true,被删除节点正好是当前选中节点时,state 也必须移除引用
|
||||||
|
await editorService.remove({ id: NodeId.NODE_ID, type: 'text' }, { doNotSelect: true });
|
||||||
|
|
||||||
|
// 节点已删除
|
||||||
|
expect(editorService.getNodeById(NodeId.NODE_ID)).toBeNull();
|
||||||
|
// state.nodes 中不再包含被删除的节点
|
||||||
|
expect(editorService.get('nodes').some((n) => n.id === NodeId.NODE_ID)).toBe(false);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('update', () => {
|
describe('update', () => {
|
||||||
@ -365,6 +412,33 @@ describe('update', () => {
|
|||||||
const node2 = editorService.getNodeById(NodeId.NODE_ID);
|
const node2 = editorService.getNodeById(NodeId.NODE_ID);
|
||||||
expect(node2?.style?.position).toBe('absolute');
|
expect(node2?.style?.position).toBe('absolute');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('被更新节点正好是当前选中节点时,state.node 始终与 dsl 同步', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'updated-text' });
|
||||||
|
|
||||||
|
// dsl 已更新
|
||||||
|
expect(editorService.getNodeById(NodeId.NODE_ID)?.text).toBe('updated-text');
|
||||||
|
// state.node 引用同步到新节点,不会持有过期数据
|
||||||
|
expect(editorService.get('node')?.text).toBe('updated-text');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('更新非选中节点时,不影响当前选中列表', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
const beforeSelected = editorService.get('node');
|
||||||
|
|
||||||
|
// 更新另一个非选中节点
|
||||||
|
await editorService.update({ id: NodeId.NODE_ID2, type: 'text', text: 'other-text' });
|
||||||
|
|
||||||
|
// dsl 已更新
|
||||||
|
expect(editorService.getNodeById(NodeId.NODE_ID2)?.text).toBe('other-text');
|
||||||
|
// 原选中节点引用不被错误替换(修复 splice(-1) 误改最后一个选中项的旧 bug)
|
||||||
|
expect(editorService.get('node')?.id).toBe(NodeId.NODE_ID);
|
||||||
|
expect(editorService.get('node')).toBe(beforeSelected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sort', () => {
|
describe('sort', () => {
|
||||||
@ -378,6 +452,19 @@ describe('sort', () => {
|
|||||||
parent = editorService.get('parent');
|
parent = editorService.get('parent');
|
||||||
expect(parent?.items[0].id).toBe(NodeId.NODE_ID2);
|
expect(parent?.items[0].id).toBe(NodeId.NODE_ID2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('doNotSelect: true 完成排序且不触发额外 select', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID2);
|
||||||
|
const parentBefore = editorService.get('parent');
|
||||||
|
expect(parentBefore?.items[0].id).toBe(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
await editorService.sort(NodeId.NODE_ID2, NodeId.NODE_ID, { doNotSelect: true });
|
||||||
|
|
||||||
|
// dsl 顺序已更新
|
||||||
|
const parentAfter = editorService.getParentById(NodeId.NODE_ID2);
|
||||||
|
expect(parentAfter?.items[0].id).toBe(NodeId.NODE_ID2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('copy', () => {
|
describe('copy', () => {
|
||||||
@ -390,6 +477,26 @@ describe('copy', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('paste', () => {
|
||||||
|
test('doNotSelect: true 不更新选中节点', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
const sourceNode = editorService.getNodeById(NodeId.NODE_ID2);
|
||||||
|
await editorService.copy(sourceNode!);
|
||||||
|
|
||||||
|
const beforeNodeId = editorService.get('node')?.id;
|
||||||
|
expect(beforeNodeId).toBe(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
const pasted = await editorService.paste({}, undefined, { doNotSelect: true });
|
||||||
|
|
||||||
|
// 粘贴成功
|
||||||
|
expect(pasted).toBeTruthy();
|
||||||
|
// 当前选中节点保持原状
|
||||||
|
expect(editorService.get('node')?.id).toBe(beforeNodeId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('moveLayer', () => {
|
describe('moveLayer', () => {
|
||||||
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
||||||
|
|
||||||
@ -402,6 +509,35 @@ describe('moveLayer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('插件参数兜底', () => {
|
||||||
|
test('add 的 parent 形参传入函数时不抛错,仍走默认父节点逻辑', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
// 模拟 BaseService 中间件机制在 parent 位置注入 dispatch 函数
|
||||||
|
const dispatchFn = () => {};
|
||||||
|
const newNode = await editorService.add({ type: 'text' }, dispatchFn as any);
|
||||||
|
|
||||||
|
// 默认行为:被加到了当前选中节点的父节点 (PAGE)
|
||||||
|
const addedId = Array.isArray(newNode) ? newNode[0].id : newNode.id;
|
||||||
|
const parentInfo = editorService.getParentById(addedId);
|
||||||
|
expect(parentInfo?.id).toBe(NodeId.PAGE_ID);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add 的 options 形参传入函数时不抛错,doNotSelect 回落为默认值', async () => {
|
||||||
|
editorService.set('root', cloneDeep(root));
|
||||||
|
await editorService.select(NodeId.NODE_ID);
|
||||||
|
|
||||||
|
// 模拟 BaseService 中间件机制在 options 位置注入 dispatch 函数
|
||||||
|
const dispatchFn = () => {};
|
||||||
|
const newNode = await editorService.add({ type: 'text' }, null, dispatchFn as any);
|
||||||
|
|
||||||
|
// 默认行为:当前选中节点变成了新增节点
|
||||||
|
const addedId = Array.isArray(newNode) ? newNode[0].id : newNode.id;
|
||||||
|
expect(editorService.get('node')?.id).toBe(addedId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('undo redo', () => {
|
describe('undo redo', () => {
|
||||||
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user