mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-05-20 07:23:38 +00: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 指定的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
||||
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>} 新增的组件或组件集合
|
||||
|
||||
@ -352,6 +355,8 @@ editorService.highlight("text_123");
|
||||
- **[扩展支持](../../guide/editor-expand#行为扩展):** 是
|
||||
- **参数:**
|
||||
- {`MNode`} node 要删除的节点
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -366,6 +371,8 @@ editorService.highlight("text_123");
|
||||
|
||||
- **参数:**
|
||||
- {`MNode` | `MNode`[])} node 要删除的节点或节点集合
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 删除后是否不更新当前选中节点(默认 false,删除后会选中父节点或首个页面)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -390,7 +397,6 @@ editorService.highlight("text_123");
|
||||
- {`MNode`} config 新的节点
|
||||
- `{Object}` data 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords 变更记录
|
||||
- `{boolean}` selectedAfterUpdate 更新后是否将新节点同步到当前选中节点列表
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }>}` 更新前后的节点信息
|
||||
@ -405,6 +411,8 @@ editorService.highlight("text_123");
|
||||
|
||||
:::tip
|
||||
节点中应该要有id,不然不知道要更新哪个节点
|
||||
|
||||
当被更新节点正好在当前选中列表中时,state 会自动同步到新的节点引用,无需调用方处理
|
||||
:::
|
||||
|
||||
## update
|
||||
@ -415,7 +423,6 @@ editorService.highlight("text_123");
|
||||
- {`MNode` | `MNode`[]} config 新的节点或节点集合
|
||||
- `{Object}` data 可选配置
|
||||
- {`ChangeRecord`[]} changeRecords 变更记录
|
||||
- `{boolean}` selectedAfterUpdate 更新后是否同步到当前选中节点列表
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>} 新的节点或节点集合
|
||||
@ -439,6 +446,8 @@ editorService.highlight("text_123");
|
||||
- **参数:**
|
||||
- `{ string | number }` id1
|
||||
- `{ string | number }` id2
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
||||
|
||||
- **返回:**
|
||||
- `{Promise<void>}`
|
||||
@ -502,6 +511,10 @@ editorService.highlight("text_123");
|
||||
<<< @/../packages/editor/src/type.ts#PastePosition{ts}
|
||||
:::
|
||||
|
||||
- `{TargetOptions}` collectorOptions 可选的依赖收集器配置
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>} 添加后的组件节点配置
|
||||
|
||||
@ -535,6 +548,8 @@ editorService.highlight("text_123");
|
||||
|
||||
- **参数:**
|
||||
- {`MNode` | `MNode`[]} config 需要居中的组件或者组件集合
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
||||
|
||||
- **返回:**
|
||||
- {Promise<`MNode` | `MNode`[]>}
|
||||
@ -572,6 +587,8 @@ alignCenter可以支持一次水平居中多个组件,alignCenter是通过调
|
||||
- **参数:**
|
||||
- {`MNode`} config 需要移动的节点
|
||||
- `{string | number}` targetId 容器ID
|
||||
- `{Object}` options 可选配置
|
||||
- `{boolean}` doNotSelect 移动后是否不更新当前选中节点(默认 false)
|
||||
|
||||
- **返回:**
|
||||
- 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 { 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 {
|
||||
public state: StoreState = reactive({
|
||||
root: null,
|
||||
@ -349,9 +368,18 @@ class Editor extends BaseService {
|
||||
* 向指点容器添加组件节点
|
||||
* @param addConfig 将要添加的组件节点配置
|
||||
* @param parent 要添加到的容器组件节点配置,如果不设置,默认为当前选中的组件的父节点
|
||||
* @param options 可选配置
|
||||
* @param options.doNotSelect 添加后是否不更新当前选中节点(默认 false,添加后会选中新增的节点)
|
||||
* @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();
|
||||
|
||||
const stage = this.get('stage');
|
||||
@ -374,25 +402,29 @@ class Editor extends BaseService {
|
||||
if ((isPage(node) || isPageFragment(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('未找到父元素');
|
||||
return this.doAdd(node, parentNode);
|
||||
}),
|
||||
);
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
const newNodeIds = newNodes.map((node) => node.id);
|
||||
// 触发选中样式
|
||||
stage?.multiSelect(newNodeIds);
|
||||
await this.multiSelect(newNodeIds);
|
||||
if (!doNotSelect) {
|
||||
const newNodeIds = newNodes.map((node) => node.id);
|
||||
// 触发选中样式
|
||||
stage?.multiSelect(newNodeIds);
|
||||
await this.multiSelect(newNodeIds);
|
||||
}
|
||||
} else {
|
||||
await this.select(newNodes[0]);
|
||||
if (!doNotSelect) {
|
||||
await this.select(newNodes[0]);
|
||||
}
|
||||
|
||||
if (isPage(newNodes[0])) {
|
||||
this.state.pageLength += 1;
|
||||
} else if (isPageFragment(newNodes[0])) {
|
||||
this.state.pageFragmentLength += 1;
|
||||
} else {
|
||||
} else if (!doNotSelect) {
|
||||
// 新增页面,这个时候页面还有渲染出来,此时select会出错,在runtime-ready的时候回去select
|
||||
stage?.select(newNodes[0].id);
|
||||
}
|
||||
@ -421,7 +453,9 @@ class Editor extends BaseService {
|
||||
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');
|
||||
if (!root) throw new Error('root不能为空');
|
||||
|
||||
@ -437,6 +471,24 @@ class Editor extends BaseService {
|
||||
const stage = this.get('stage');
|
||||
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[]) => {
|
||||
if (pages[0]) {
|
||||
await this.select(pages[0]);
|
||||
@ -453,14 +505,20 @@ class Editor extends BaseService {
|
||||
if (isPage(node)) {
|
||||
this.state.pageLength -= 1;
|
||||
|
||||
await selectDefault(rootItems);
|
||||
if (!doNotSelect) {
|
||||
await selectDefault(rootItems);
|
||||
}
|
||||
} else if (isPageFragment(node)) {
|
||||
this.state.pageFragmentLength -= 1;
|
||||
|
||||
await selectDefault(rootItems);
|
||||
if (!doNotSelect) {
|
||||
await selectDefault(rootItems);
|
||||
}
|
||||
} else {
|
||||
await this.select(parent);
|
||||
stage?.select(parent.id);
|
||||
if (!doNotSelect) {
|
||||
await this.select(parent);
|
||||
stage?.select(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();
|
||||
|
||||
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) {
|
||||
this.pushOpHistory('remove', { removedItems }, pageForOp);
|
||||
@ -510,10 +572,7 @@ class Editor extends BaseService {
|
||||
|
||||
public async doUpdate(
|
||||
config: MNode,
|
||||
{
|
||||
changeRecords = [],
|
||||
selectedAfterUpdate = true,
|
||||
}: { changeRecords?: ChangeRecord[]; selectedAfterUpdate?: boolean } = {},
|
||||
{ changeRecords = [] }: { changeRecords?: ChangeRecord[] } = {},
|
||||
): Promise<{ newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }> {
|
||||
const root = this.get('root');
|
||||
if (!root) throw new Error('root为空');
|
||||
@ -557,12 +616,12 @@ class Editor extends BaseService {
|
||||
|
||||
parentNodeItems[index] = newConfig;
|
||||
|
||||
// 将update后的配置更新到nodes中
|
||||
if (selectedAfterUpdate) {
|
||||
const nodes = this.get('nodes');
|
||||
const targetIndex = nodes.findIndex((nodeItem: MNode) => `${nodeItem.id}` === `${newConfig.id}`);
|
||||
nodes.splice(targetIndex, 1, newConfig);
|
||||
this.set('nodes', [...nodes]);
|
||||
// 当被更新节点正好在当前选中列表中时,必须同步引用,否则 state 会持有已被替换的旧节点
|
||||
const selectedNodes = this.get('nodes');
|
||||
const targetIndex = selectedNodes.findIndex((nodeItem: MNode) => `${nodeItem.id}` === `${newConfig.id}`);
|
||||
if (targetIndex !== -1) {
|
||||
selectedNodes.splice(targetIndex, 1, newConfig);
|
||||
this.set('nodes', [...selectedNodes]);
|
||||
}
|
||||
|
||||
if (isPage(newConfig) || isPageFragment(newConfig)) {
|
||||
@ -586,7 +645,7 @@ class Editor extends BaseService {
|
||||
*/
|
||||
public async update(
|
||||
config: MNode | MNode[],
|
||||
data: { changeRecords?: ChangeRecord[]; selectedAfterUpdate?: boolean } = {},
|
||||
data: { changeRecords?: ChangeRecord[] } = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
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]
|
||||
* @param id1 组件ID
|
||||
* @param id2 组件ID
|
||||
* @param options 可选配置
|
||||
* @param options.doNotSelect 排序后是否不更新当前选中节点(默认 false)
|
||||
* @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();
|
||||
|
||||
const root = this.get('root');
|
||||
@ -642,7 +705,9 @@ class Editor extends BaseService {
|
||||
parent.items.splice(index2, 0, ...parent.items.splice(index1, 1));
|
||||
|
||||
await this.update(parent);
|
||||
await this.select(node);
|
||||
if (!doNotSelect) {
|
||||
await this.select(node);
|
||||
}
|
||||
|
||||
this.get('stage')?.update({
|
||||
config: cloneDeep(node),
|
||||
@ -682,9 +747,18 @@ class Editor extends BaseService {
|
||||
/**
|
||||
* 从localStorage中获取节点,然后添加到当前容器中
|
||||
* @param position 粘贴的坐标
|
||||
* @param collectorOptions 可选的依赖收集器配置
|
||||
* @param options 可选配置
|
||||
* @param options.doNotSelect 粘贴后是否不更新当前选中节点(默认 false)
|
||||
* @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);
|
||||
if (!Array.isArray(config)) return;
|
||||
|
||||
@ -704,7 +778,7 @@ class Editor extends BaseService {
|
||||
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[]> {
|
||||
@ -732,9 +806,13 @@ class Editor extends BaseService {
|
||||
/**
|
||||
* 将指点节点设置居中
|
||||
* @param config 组件节点配置
|
||||
* @param options 可选配置
|
||||
* @param options.doNotSelect 居中后是否不更新当前选中节点(默认 false)
|
||||
* @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 stage = this.get('stage');
|
||||
|
||||
@ -742,10 +820,12 @@ class Editor extends BaseService {
|
||||
|
||||
const newNode = await this.update(newNodes);
|
||||
|
||||
if (newNodes.length > 1) {
|
||||
await stage?.multiSelect(newNodes.map((node) => node.id));
|
||||
} else {
|
||||
await stage?.select(newNodes[0].id);
|
||||
if (!doNotSelect) {
|
||||
if (newNodes.length > 1) {
|
||||
await stage?.multiSelect(newNodes.map((node) => node.id));
|
||||
} else {
|
||||
await stage?.select(newNodes[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
return newNode;
|
||||
@ -808,8 +888,16 @@ class Editor extends BaseService {
|
||||
* 移动到指定容器中
|
||||
* @param config 需要移动的节点
|
||||
* @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();
|
||||
|
||||
const root = this.get('root');
|
||||
@ -837,7 +925,9 @@ class Editor extends BaseService {
|
||||
|
||||
target.items.push(newConfig);
|
||||
|
||||
await stage.select(targetId);
|
||||
if (!doNotSelect) {
|
||||
await stage.select(targetId);
|
||||
}
|
||||
|
||||
const targetParent = this.getParentById(target.id);
|
||||
await stage.update({
|
||||
@ -846,8 +936,10 @@ class Editor extends BaseService {
|
||||
root: cloneDeep(root),
|
||||
});
|
||||
|
||||
await this.select(newConfig);
|
||||
stage.select(newConfig.id);
|
||||
if (!doNotSelect) {
|
||||
await this.select(newConfig);
|
||||
stage.select(newConfig.id);
|
||||
}
|
||||
|
||||
this.addModifiedNodeId(target.id);
|
||||
this.addModifiedNodeId(parent.id);
|
||||
|
||||
@ -286,6 +286,23 @@ describe('add', () => {
|
||||
),
|
||||
).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', () => {
|
||||
@ -316,6 +333,36 @@ describe('remove', () => {
|
||||
test('undefine', async () => {
|
||||
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', () => {
|
||||
@ -365,6 +412,33 @@ describe('update', () => {
|
||||
const node2 = editorService.getNodeById(NodeId.NODE_ID);
|
||||
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', () => {
|
||||
@ -378,6 +452,19 @@ describe('sort', () => {
|
||||
parent = editorService.get('parent');
|
||||
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', () => {
|
||||
@ -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', () => {
|
||||
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', () => {
|
||||
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user