mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-02 06:24:04 +08:00
feat(editor): add unified change events
This commit is contained in:
parent
f7afed66aa
commit
7bbb1f24c0
@ -87,3 +87,19 @@
|
||||
- **详情:** 历史记录改变,[editorService.redo(),editorService.undo()](./editorServiceMethods.md#undo)后触发
|
||||
|
||||
- **事件回调函数:** `(data: MPage | MPageFragment) => void`
|
||||
|
||||
## change
|
||||
|
||||
- **详情:** DSL 发生变更后统一触发,免去分别监听 `add` / `remove` / `update` / `move-layer` / `drag-to`。在 [editorService.add()](./editorServiceMethods.md#add)、[editorService.remove()](./editorServiceMethods.md#remove)、[editorService.update()](./editorServiceMethods.md#update)、[editorService.moveLayer()](./editorServiceMethods.md#movelayer)、[editorService.dragTo()](./editorServiceMethods.md#dragto) 后触发。
|
||||
|
||||
回调参数 `event` 通过 `type` 区分操作类型,并携带本次变更的节点列表 `data`,每项包含变更的 `node` 及其所属的 `page`(可能为 `null`)。`move-layer` 额外携带层级偏移 `offset`,`drag-to` 额外携带目标位置 `targetIndex` / `targetParent`。
|
||||
|
||||
::: warning 注意
|
||||
撤销 / 重做(`undo` / `redo`)内部同样会经由 `add` / `remove` / `update` 触发本事件;如需区分「用户操作」与「撤销重做」,请配合 [history-change](#history-change) 事件判断。
|
||||
:::
|
||||
|
||||
- **事件回调函数:** `(event: EditorChangeEvent) => void`
|
||||
|
||||
::: details 查看 EditorChangeEvent 类型定义
|
||||
<<< @/../packages/editor/src/type.ts#EditorChangeEvent{ts}
|
||||
:::
|
||||
|
||||
@ -85,6 +85,7 @@ class CodeBlock extends BaseService {
|
||||
public async setCodeDsl(codeDsl: CodeBlockDSL): Promise<void> {
|
||||
this.state.codeDsl = codeDsl;
|
||||
this.emit('code-dsl-change', this.state.codeDsl);
|
||||
this.emit('change', this.state.codeDsl);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -199,6 +200,7 @@ class CodeBlock extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('addOrUpdate', id, codeDsl[id]);
|
||||
this.emit('change', this.getCodeDsl());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -318,6 +320,8 @@ class CodeBlock extends BaseService {
|
||||
|
||||
this.emit('remove', id);
|
||||
});
|
||||
|
||||
this.emit('change', this.getCodeDsl());
|
||||
}
|
||||
|
||||
// #region AndGetHistoryId
|
||||
|
||||
@ -147,6 +147,7 @@ class DataSource extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('add', newConfig);
|
||||
this.emit('change', this.get('dataSources'));
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
@ -192,6 +193,7 @@ class DataSource extends BaseService {
|
||||
oldConfig,
|
||||
changeRecords,
|
||||
});
|
||||
this.emit('change', this.get('dataSources'));
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
@ -220,6 +222,7 @@ class DataSource extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('remove', id);
|
||||
this.emit('change', this.get('dataSources'));
|
||||
}
|
||||
|
||||
// #region AndGetHistoryId
|
||||
|
||||
@ -535,6 +535,10 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('add', newNodes);
|
||||
this.emit('change', {
|
||||
type: 'add',
|
||||
data: newNodes.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
});
|
||||
|
||||
return Array.isArray(addNode) ? newNodes : newNodes[0];
|
||||
}
|
||||
@ -635,6 +639,9 @@ class Editor extends BaseService {
|
||||
|
||||
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
||||
|
||||
// 删除后节点已从树中移除,无法再反查所属 page,这里在删除前先逐个捕获用于 `change` 事件
|
||||
const changeItems = nodes.map((node) => ({ node, page: this.getPageOfNode(node.id) }));
|
||||
|
||||
const removedItems: StepDiffItem<MNode>[] = [];
|
||||
let pageForOp: { name: string; id: Id } | null = null;
|
||||
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
||||
@ -670,6 +677,7 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('remove', nodes);
|
||||
this.emit('change', { type: 'remove', data: changeItems });
|
||||
}
|
||||
|
||||
public async doUpdate(
|
||||
@ -805,6 +813,10 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('update', updateData);
|
||||
this.emit('change', {
|
||||
type: 'update',
|
||||
data: updateData.map((node) => ({ node, page: this.getPageOfNode(node.newNode.id) })),
|
||||
});
|
||||
return Array.isArray(config) ? updateData.map((item) => item.newNode) : updateData[0].newNode;
|
||||
}
|
||||
|
||||
@ -1046,6 +1058,11 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('move-layer', offset);
|
||||
this.emit('change', {
|
||||
type: 'move-layer',
|
||||
data: [{ node, page: this.getPageOfNode(node.id) }],
|
||||
offset,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1225,6 +1242,12 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
this.emit('drag-to', { targetIndex, configs, targetParent });
|
||||
this.emit('change', {
|
||||
type: 'drag-to',
|
||||
data: configs.map((node) => ({ node, page: this.getPageOfNode(node.id) })),
|
||||
targetIndex,
|
||||
targetParent,
|
||||
});
|
||||
}
|
||||
|
||||
// #region AndGetHistoryId
|
||||
@ -1558,6 +1581,20 @@ class Editor extends BaseService {
|
||||
this.get('modifiedNodeIds').set(id, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定节点所属的页面 / 页面片:
|
||||
* - 普通节点返回其所在的 page;
|
||||
* - 节点本身就是 page / pageFragment 时返回它自己;
|
||||
* - 找不到时返回 null。
|
||||
* 供 `change` 事件携带「变更节点对应的 page」(而非编辑器当前选中页)。
|
||||
*/
|
||||
private getPageOfNode(id: Id): MPage | MPageFragment | null {
|
||||
const { node, page } = this.getNodeInfo(id, false);
|
||||
if (page) return page;
|
||||
if (node && (isPage(node) || isPageFragment(node))) return node as MPage | MPageFragment;
|
||||
return null;
|
||||
}
|
||||
|
||||
private captureSelectionBeforeOp() {
|
||||
if (this.selectionBeforeOp) return;
|
||||
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
||||
|
||||
@ -1218,8 +1218,41 @@ export interface EditorEvents {
|
||||
'move-layer': [offset: number | LayerOffset];
|
||||
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
|
||||
'history-change': [data: MPage | MPageFragment];
|
||||
/**
|
||||
* DSL 发生变更后统一触发,免去分别监听 add / remove / update / move-layer / drag-to。
|
||||
* 回调参数为 {@link EditorChangeEvent},按 `type` 区分操作类型并携带各自的操作内容(payload)
|
||||
* 以及变更所在的当前 page(可能为 null)。撤销 / 重做内部同样会经由
|
||||
* add / remove / update 触发本事件;如需区分「用户操作」与「撤销重做」请配合 `history-change`。
|
||||
*/
|
||||
change: [event: EditorChangeEvent];
|
||||
}
|
||||
|
||||
// #region EditorChangeEvent
|
||||
/** `change` 事件中单个变更项:变更的 node 及其所属的 page(可能为 null)。 */
|
||||
export interface EditorChangeItem {
|
||||
node: MNode;
|
||||
page: StoreState['page'];
|
||||
}
|
||||
|
||||
/** `update` 类型变更项:node 为前后快照及 form 端变更记录,page 为其所属页面。 */
|
||||
export interface EditorUpdateChangeItem {
|
||||
node: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] };
|
||||
page: StoreState['page'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link EditorEvents.change} 的回调参数:以 `type` 区分操作类型,并携带对应的操作内容。
|
||||
* `data` 为本次变更的节点列表,每项包含 node 及其所属的 page(可能为 null);
|
||||
* `move-layer` 额外带层级偏移 `offset`,`drag-to` 额外带目标位置 `targetIndex` / `targetParent`。
|
||||
*/
|
||||
export type EditorChangeEvent =
|
||||
| { type: 'add'; data: EditorChangeItem[] }
|
||||
| { type: 'remove'; data: EditorChangeItem[] }
|
||||
| { type: 'update'; data: EditorUpdateChangeItem[] }
|
||||
| { type: 'move-layer'; data: EditorChangeItem[]; offset: number | LayerOffset }
|
||||
| { type: 'drag-to'; data: EditorChangeItem[]; targetIndex: number; targetParent: MContainer };
|
||||
// #endregion EditorChangeEvent
|
||||
|
||||
export interface HistoryEvents {
|
||||
change: [
|
||||
state: BaseStepValue | StepValue | CodeBlockStepValue | DataSourceStepValue,
|
||||
|
||||
@ -16,16 +16,17 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { beforeAll, describe, expect, test } from 'vitest';
|
||||
import { beforeAll, describe, expect, test, vi } from 'vitest';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { MApp, MNode } from '@tmagic/core';
|
||||
import type { MApp, MContainer, MNode } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import { getNodePath } from '@tmagic/utils';
|
||||
|
||||
import editorService from '@editor/services/editor';
|
||||
import historyService from '@editor/services/history';
|
||||
import storageService from '@editor/services/storage';
|
||||
import type { EditorChangeEvent } from '@editor/type';
|
||||
import { COPY_STORAGE_KEY, setEditorConfig } from '@editor/utils';
|
||||
|
||||
setEditorConfig({
|
||||
@ -759,6 +760,105 @@ describe('moveLayer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('change 事件', () => {
|
||||
// 取出最后一次 change 事件的回调参数
|
||||
const lastChangeEvent = (fn: ReturnType<typeof vi.fn>): EditorChangeEvent =>
|
||||
fn.mock.calls[fn.mock.calls.length - 1][0];
|
||||
|
||||
test('add 触发 change:type 为 add,data 携带新增 node 及其所属 page', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
const newNode = await editorService.add({ type: 'text' });
|
||||
editorService.off('change', fn);
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1);
|
||||
const event = lastChangeEvent(fn);
|
||||
expect(event.type).toBe('add');
|
||||
expect(event.data).toHaveLength(1);
|
||||
const addedId = Array.isArray(newNode) ? newNode[0].id : newNode.id;
|
||||
expect(event.data[0].node.id).toBe(addedId);
|
||||
// 新增节点归属于当前页面
|
||||
expect(event.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
});
|
||||
|
||||
test('remove 触发 change:type 为 remove,data 携带被删 node 及删除前所属 page', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
await editorService.remove({ id: NodeId.NODE_ID, type: 'text' });
|
||||
editorService.off('change', fn);
|
||||
|
||||
const event = lastChangeEvent(fn);
|
||||
expect(event.type).toBe('remove');
|
||||
expect(event.data).toHaveLength(1);
|
||||
expect(event.data[0].node.id).toBe(NodeId.NODE_ID);
|
||||
// 删除前捕获,page 仍能正确反查到
|
||||
expect(event.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
});
|
||||
|
||||
test('update 触发 change:type 为 update,data.node 携带 newNode / oldNode', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
await editorService.update({ id: NodeId.NODE_ID, type: 'text', text: 'changed' });
|
||||
editorService.off('change', fn);
|
||||
|
||||
const event = lastChangeEvent(fn);
|
||||
expect(event.type).toBe('update');
|
||||
if (event.type === 'update') {
|
||||
expect(event.data[0].node.newNode.id).toBe(NodeId.NODE_ID);
|
||||
expect(event.data[0].node.newNode.text).toBe('changed');
|
||||
expect(event.data[0].node.oldNode.id).toBe(NodeId.NODE_ID);
|
||||
expect(event.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
}
|
||||
});
|
||||
|
||||
test('moveLayer 触发 change:type 为 move-layer,并携带 offset', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.NODE_ID);
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
await editorService.moveLayer(1);
|
||||
editorService.off('change', fn);
|
||||
|
||||
const event = lastChangeEvent(fn);
|
||||
expect(event.type).toBe('move-layer');
|
||||
if (event.type === 'move-layer') {
|
||||
expect(event.offset).toBe(1);
|
||||
expect(event.data[0].node.id).toBe(NodeId.NODE_ID);
|
||||
expect(event.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
}
|
||||
});
|
||||
|
||||
test('dragTo 触发 change:type 为 drag-to,并携带 targetIndex / targetParent', async () => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
const page = editorService.get('page') as unknown as MContainer;
|
||||
|
||||
const fn = vi.fn();
|
||||
editorService.on('change', fn);
|
||||
await editorService.dragTo({ id: NodeId.NODE_ID2, type: 'text' }, page, 0);
|
||||
editorService.off('change', fn);
|
||||
|
||||
const event = lastChangeEvent(fn);
|
||||
expect(event.type).toBe('drag-to');
|
||||
if (event.type === 'drag-to') {
|
||||
expect(event.targetIndex).toBe(0);
|
||||
expect(event.targetParent.id).toBe(NodeId.PAGE_ID);
|
||||
expect(event.data[0].node.id).toBe(NodeId.NODE_ID2);
|
||||
expect(event.data[0].page?.id).toBe(NodeId.PAGE_ID);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('undo redo', () => {
|
||||
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user