mirror of
https://github.com/idrawjs/idraw
synced 2026-05-24 10:08:34 +00:00
feat: rename event name and add modify method
This commit is contained in:
parent
99c002d543
commit
cab61b4c76
14 changed files with 837 additions and 28 deletions
|
|
@ -565,9 +565,9 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
|
|||
}
|
||||
|
||||
if (data && (['drag', 'drag-list', 'drag-list-end', 'resize'] as ActionType[]).includes(actionType)) {
|
||||
let type = 'drag-element';
|
||||
let type = 'dragElement';
|
||||
if (type === 'resize') {
|
||||
type = 'resize-element';
|
||||
type = 'resizeElement';
|
||||
}
|
||||
eventHub.trigger('change', { data, type });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@
|
|||
<div id="mount-refer"></div>
|
||||
</div>
|
||||
<div>
|
||||
<button id="btn">Export</button>
|
||||
<!-- <button id="btn">Export</button> -->
|
||||
</div>
|
||||
<div id="preview"></div>
|
||||
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export interface IDrawEventKeys {
|
|||
export type IDrawEvent = CoreEvent & {
|
||||
change: {
|
||||
data: Data;
|
||||
type: 'update-element' | 'delete-element' | 'move-element' | 'add-element' | 'drag-element' | 'resize-element' | 'set-data' | 'other';
|
||||
type: 'updateElement' | 'deleteElement' | 'moveElement' | 'addElement' | 'dragElement' | 'resizeElement' | 'setData' | 'undo' | 'redo' | 'other';
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export class iDraw {
|
|||
setData(data: Data) {
|
||||
const core = this.#core;
|
||||
core.setData(data);
|
||||
core.trigger('change', { data, type: 'set-data' });
|
||||
core.trigger('change', { data, type: 'setData' });
|
||||
}
|
||||
|
||||
getData(opts?: { compact?: boolean }): Data | null {
|
||||
|
|
@ -212,7 +212,7 @@ export class iDraw {
|
|||
updateElementInList(element.uuid, element, data.elements);
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger('change', { data, type: 'update-element' });
|
||||
core.trigger('change', { data, type: 'updateElement' });
|
||||
}
|
||||
|
||||
addElement(
|
||||
|
|
@ -231,7 +231,7 @@ export class iDraw {
|
|||
}
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger('change', { data, type: 'add-element' });
|
||||
core.trigger('change', { data, type: 'addElement' });
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -241,18 +241,18 @@ export class iDraw {
|
|||
deleteElementInList(uuid, data.elements);
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger('change', { data, type: 'delete-element' });
|
||||
core.trigger('change', { data, type: 'deleteElement' });
|
||||
}
|
||||
|
||||
moveElement(uuid: string, to: ElementPosition) {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
const from = getElementPositionFromList(uuid, data.elements);
|
||||
const list = moveElementPosition(data.elements, { from, to });
|
||||
const { elements: list } = moveElementPosition(data.elements, { from, to });
|
||||
data.elements = list;
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger('change', { data, type: 'move-element' });
|
||||
core.trigger('change', { data, type: 'moveElement' });
|
||||
}
|
||||
|
||||
async getImageBlobURL(opts: ExportImageFileBaseOptions): Promise<ExportImageFileResult> {
|
||||
|
|
|
|||
|
|
@ -117,7 +117,8 @@ export {
|
|||
deepResizeGroupElement,
|
||||
deepCloneElement,
|
||||
calcViewCenterContent,
|
||||
calcViewCenter
|
||||
calcViewCenter,
|
||||
modifyElement
|
||||
} from '@idraw/util';
|
||||
export { iDraw } from './idraw';
|
||||
export type { IDrawEvent, IDrawEventKeys } from './event';
|
||||
|
|
|
|||
|
|
@ -16,3 +16,4 @@ export * from './lib/controller';
|
|||
export * from './lib/html';
|
||||
export * from './lib/svg-path';
|
||||
export * from './lib/config';
|
||||
export * from './lib/modify';
|
||||
|
|
|
|||
|
|
@ -1,19 +1,43 @@
|
|||
import type { Element, ElementPosition } from './element';
|
||||
import type { RecursivePartial } from './util';
|
||||
|
||||
export type ModifyType = 'update-element' | 'add-element' | 'delete-element' | 'move-element';
|
||||
export type ModifyType = 'updateElement' | 'addElement' | 'deleteElement' | 'moveElement';
|
||||
|
||||
export type ModifyElement = Omit<RecursivePartial<Element>, 'uuid'> & { uuid: string };
|
||||
export type ModifiedElement = Omit<RecursivePartial<Element>, 'uuid'>;
|
||||
|
||||
export interface ModifyDataMap {
|
||||
'update-element': { position: ElementPosition; modifyElement: ModifyElement };
|
||||
'add-element': { position: ElementPosition; element: Element };
|
||||
'delete-element': { position: ElementPosition; element: Element };
|
||||
'move-element': { from: ElementPosition; to: ElementPosition };
|
||||
export type ModifiedTargetElement = ModifiedElement & { uuid: string };
|
||||
|
||||
export interface ModifyContentMap {
|
||||
updateElement: { position: ElementPosition; beforeModifiedElement: ModifiedElement; afterModifiedElement: ModifiedElement };
|
||||
addElement: { position: ElementPosition; element: Element };
|
||||
deleteElement: { position: ElementPosition; element: Element };
|
||||
moveElement: { from: ElementPosition; to: ElementPosition };
|
||||
}
|
||||
|
||||
export interface ModifyItem<T extends ModifyType> {
|
||||
export interface ModifyOptions<T extends ModifyType> {
|
||||
type: T;
|
||||
data: ModifyDataMap[T];
|
||||
time: number;
|
||||
content: ModifyContentMap[T];
|
||||
}
|
||||
|
||||
export interface ModifyRecordMap {
|
||||
updateElement: {
|
||||
type: 'updateElement';
|
||||
time: number;
|
||||
} & Required<ModifyContentMap['updateElement']>;
|
||||
addElement: {
|
||||
type: 'addElement';
|
||||
time: number;
|
||||
} & Required<ModifyContentMap['addElement']>;
|
||||
deleteElement: {
|
||||
type: 'deleteElement';
|
||||
time: number;
|
||||
} & Required<ModifyContentMap['deleteElement']>;
|
||||
moveElement: {
|
||||
type: 'moveElement';
|
||||
time: number;
|
||||
afterModifiedFrom: ElementPosition;
|
||||
afterModifiedTo: ElementPosition;
|
||||
} & Required<ModifyContentMap['moveElement']>;
|
||||
}
|
||||
|
||||
export type ModifyRecord<T extends ModifyType = ModifyType> = ModifyRecordMap[T];
|
||||
|
|
|
|||
441
packages/util/__tests__/lib/modify-recorder.test.ts
Normal file
441
packages/util/__tests__/lib/modify-recorder.test.ts
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
import type { Data, Element, ModifiedElement } from '@idraw/types';
|
||||
import { deepClone } from '@idraw/util';
|
||||
import { ModifyRecorder } from '../../src/lib/modify-recorder';
|
||||
import { imageBase64, html, svg } from '../_assets/base';
|
||||
|
||||
const originData: Data = {
|
||||
elements: [
|
||||
{
|
||||
uuid: 'b37213ce-d711-cbb3-51ac-d8081c19f127',
|
||||
type: 'rect',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#A0A0A0'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '39308517-e10f-76df-43a9-50ed7295e61e',
|
||||
type: 'circle',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#B0B0B0'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'ef934ab7-a32e-040c-9ac0-ed193405e6e4',
|
||||
type: 'text',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
text: 'Hello World'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '063e3a80-1ede-7912-f919-975e34a9bd01',
|
||||
type: 'group',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
children: [
|
||||
{
|
||||
uuid: '76a9bc55-6400-5aff-3147-d6c9b0d68753',
|
||||
type: 'rect',
|
||||
x: 10,
|
||||
y: 20,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#C0C0C0'
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'e0889472-1f16-d6cd-3c7a-4b827d52279d',
|
||||
type: 'image',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
src: imageBase64
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'b60e64e8-833e-e112-d7eb-1ab6e7d6870c',
|
||||
type: 'svg',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
svg: svg
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '61f2a61e-cdd5-ae36-983f-686ba8e35973',
|
||||
type: 'html',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
html: html
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
describe('ModifyRecorder', () => {
|
||||
let dateSpy: any;
|
||||
const mockTime = Date.parse('2024-01-01');
|
||||
|
||||
beforeAll(() => {
|
||||
dateSpy = jest.spyOn(global.Date, 'now').mockImplementation(() => mockTime);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
dateSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('do-undo-redo addElement', () => {
|
||||
const data = deepClone(originData);
|
||||
const recorder = new ModifyRecorder({ recordable: true });
|
||||
const elem: Element<'rect'> = {
|
||||
uuid: '2d2c5333-352d-7734-9ad6-53faa0ba36fc',
|
||||
type: 'rect',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 10,
|
||||
h: 10,
|
||||
detail: {
|
||||
background: 'red'
|
||||
}
|
||||
};
|
||||
let resultData: Data | null = null;
|
||||
// do
|
||||
{
|
||||
resultData = recorder.do(data, {
|
||||
type: 'addElement',
|
||||
content: {
|
||||
position: [1],
|
||||
element: deepClone(elem)
|
||||
}
|
||||
});
|
||||
const expectData = deepClone(originData);
|
||||
expectData.elements.splice(1, 0, deepClone(elem));
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
position: [1],
|
||||
element: deepClone(elem),
|
||||
type: 'addElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
|
||||
// undo
|
||||
{
|
||||
resultData = recorder.undo(resultData as unknown as Data);
|
||||
expect(resultData).toStrictEqual(originData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([]);
|
||||
const unRecords = recorder.$getUndoStack();
|
||||
expect(unRecords).toStrictEqual([
|
||||
{
|
||||
position: [1],
|
||||
element: deepClone(elem),
|
||||
type: 'addElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// redo
|
||||
{
|
||||
resultData = recorder.redo(resultData as unknown as Data);
|
||||
const expectData = deepClone(originData);
|
||||
expectData.elements.splice(1, 0, deepClone(elem));
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
position: [1],
|
||||
element: deepClone(elem),
|
||||
type: 'addElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('do-undo-redo updateElement', () => {
|
||||
const data = deepClone(originData);
|
||||
const recorder = new ModifyRecorder({ recordable: true });
|
||||
const targetElem: Element<'rect'> = (data.elements?.[3] as Element<'group'>).detail.children[0] as Element<'rect'>;
|
||||
|
||||
const beforeElem: ModifiedElement = {
|
||||
x: targetElem.x,
|
||||
y: targetElem.y,
|
||||
w: targetElem.w,
|
||||
h: targetElem.h,
|
||||
detail: {
|
||||
background: targetElem.detail.background
|
||||
}
|
||||
};
|
||||
|
||||
const afterElem: ModifiedElement = {
|
||||
x: 5,
|
||||
y: 15,
|
||||
w: 25,
|
||||
h: 35,
|
||||
detail: {
|
||||
background: 'red'
|
||||
}
|
||||
};
|
||||
|
||||
let resultData: Data | null = null;
|
||||
// do
|
||||
{
|
||||
resultData = recorder.do(data, {
|
||||
type: 'updateElement',
|
||||
content: {
|
||||
position: [3, 0],
|
||||
beforeModifiedElement: deepClone(beforeElem),
|
||||
afterModifiedElement: deepClone(afterElem)
|
||||
}
|
||||
});
|
||||
const expectData = deepClone(originData);
|
||||
const expectTargetElem: Element<'rect'> = (expectData.elements?.[3] as Element<'group'>).detail.children[0] as Element<'rect'>;
|
||||
expectTargetElem.x = afterElem.x as number;
|
||||
expectTargetElem.y = afterElem.y as number;
|
||||
expectTargetElem.w = afterElem.w as number;
|
||||
expectTargetElem.h = afterElem.h as number;
|
||||
expectTargetElem.detail.background = (afterElem as Element<'rect'>).detail.background as string;
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
position: [3, 0],
|
||||
beforeModifiedElement: deepClone(beforeElem),
|
||||
afterModifiedElement: deepClone(afterElem),
|
||||
type: 'updateElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
|
||||
// undo
|
||||
{
|
||||
resultData = recorder.undo(resultData as unknown as Data);
|
||||
const expectData = deepClone(originData);
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([]);
|
||||
|
||||
const unRecords = recorder.$getUndoStack();
|
||||
expect(unRecords).toStrictEqual([
|
||||
{
|
||||
position: [3, 0],
|
||||
beforeModifiedElement: deepClone(beforeElem),
|
||||
afterModifiedElement: deepClone(afterElem),
|
||||
type: 'updateElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// redo
|
||||
{
|
||||
resultData = recorder.redo(resultData as unknown as Data);
|
||||
const expectData = deepClone(originData);
|
||||
const expectTargetElem: Element<'rect'> = (expectData.elements?.[3] as Element<'group'>).detail.children[0] as Element<'rect'>;
|
||||
expectTargetElem.x = afterElem.x as number;
|
||||
expectTargetElem.y = afterElem.y as number;
|
||||
expectTargetElem.w = afterElem.w as number;
|
||||
expectTargetElem.h = afterElem.h as number;
|
||||
expectTargetElem.detail.background = (afterElem as Element<'rect'>).detail.background as string;
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
position: [3, 0],
|
||||
beforeModifiedElement: deepClone(beforeElem),
|
||||
afterModifiedElement: deepClone(afterElem),
|
||||
type: 'updateElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('do-undo-redo deleteElement', () => {
|
||||
const data = deepClone(originData);
|
||||
const recorder = new ModifyRecorder({ recordable: true });
|
||||
const targetElem: Element<'rect'> = (data.elements?.[3] as Element<'group'>).detail.children[0] as Element<'rect'>;
|
||||
let resultData: Data | null = null;
|
||||
// do
|
||||
{
|
||||
resultData = recorder.do(data, {
|
||||
type: 'deleteElement',
|
||||
content: {
|
||||
position: [3, 0],
|
||||
element: deepClone(targetElem)
|
||||
}
|
||||
});
|
||||
const expectData = deepClone(originData);
|
||||
(expectData.elements?.[3] as Element<'group'>).detail.children.splice(0, 1);
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
position: [3, 0],
|
||||
element: deepClone(targetElem),
|
||||
type: 'deleteElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
|
||||
// undo
|
||||
{
|
||||
resultData = recorder.undo(resultData as unknown as Data);
|
||||
expect(resultData).toStrictEqual(originData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([]);
|
||||
const unRecords = recorder.$getUndoStack();
|
||||
expect(unRecords).toStrictEqual([
|
||||
{
|
||||
position: [3, 0],
|
||||
element: deepClone(targetElem),
|
||||
type: 'deleteElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// redo
|
||||
{
|
||||
resultData = recorder.redo(resultData as unknown as Data);
|
||||
const expectData = deepClone(originData);
|
||||
(expectData.elements?.[3] as Element<'group'>).detail.children.splice(0, 1);
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
position: [3, 0],
|
||||
element: deepClone(targetElem),
|
||||
type: 'deleteElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
});
|
||||
|
||||
test('do-undo-redo moveElement', () => {
|
||||
const data = deepClone(originData);
|
||||
const recorder = new ModifyRecorder({ recordable: true });
|
||||
let resultData: Data | null = null;
|
||||
// const targetElem: Element<'rect'> = (data.elements?.[3] as Element<'group'>).detail.children[0] as Element<'rect'>;
|
||||
|
||||
// do
|
||||
{
|
||||
resultData = recorder.do(data, {
|
||||
type: 'moveElement',
|
||||
content: {
|
||||
from: [3, 0],
|
||||
to: [1]
|
||||
}
|
||||
});
|
||||
const expectData = deepClone(originData);
|
||||
const [fromElem] = (expectData.elements?.[3] as Element<'group'>).detail.children.splice(0, 1);
|
||||
expectData.elements.splice(1, 0, fromElem);
|
||||
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
from: [3, 0],
|
||||
to: [1],
|
||||
afterModifiedFrom: [4, 0],
|
||||
afterModifiedTo: [1],
|
||||
type: 'moveElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
|
||||
// undo
|
||||
{
|
||||
resultData = recorder.undo(resultData as unknown as Data);
|
||||
expect(resultData).toStrictEqual(originData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([]);
|
||||
const unRecords = recorder.$getUndoStack();
|
||||
expect(unRecords).toStrictEqual([
|
||||
{
|
||||
from: [3, 0],
|
||||
to: [1],
|
||||
afterModifiedFrom: [4, 0],
|
||||
afterModifiedTo: [1],
|
||||
type: 'moveElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
// redo
|
||||
{
|
||||
resultData = recorder.redo(resultData as unknown as Data);
|
||||
const expectData = deepClone(originData);
|
||||
const [fromElem] = (expectData.elements?.[3] as Element<'group'>).detail.children.splice(0, 1);
|
||||
expectData.elements.splice(1, 0, fromElem);
|
||||
|
||||
expect(resultData).toStrictEqual(expectData);
|
||||
const doRecords = recorder.$getDoStack();
|
||||
expect(doRecords).toStrictEqual([
|
||||
{
|
||||
from: [3, 0],
|
||||
to: [1],
|
||||
afterModifiedFrom: [4, 0],
|
||||
afterModifiedTo: [1],
|
||||
type: 'moveElement',
|
||||
time: mockTime
|
||||
}
|
||||
]);
|
||||
const undoRecords = recorder.$getUndoStack();
|
||||
expect(undoRecords).toStrictEqual([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
35
packages/util/__tests__/lib/modify.test.ts
Normal file
35
packages/util/__tests__/lib/modify.test.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { getModifiedElement } from '@idraw/util';
|
||||
import type { ModifiedElement, Element } from '@idraw/types';
|
||||
|
||||
describe('modify', () => {
|
||||
const originElement: Element = {
|
||||
uuid: 'b37213ce-d711-cbb3-51ac-d8081c19f127',
|
||||
type: 'rect',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#A0A0A0'
|
||||
}
|
||||
};
|
||||
|
||||
test('getModifiedElement', () => {
|
||||
const elem: ModifiedElement = {
|
||||
uuid: 'xxxxxx',
|
||||
x: 5,
|
||||
y: 10,
|
||||
detail: {
|
||||
background: 'red'
|
||||
}
|
||||
} as ModifiedElement;
|
||||
const modifiedElem = getModifiedElement(elem, originElement);
|
||||
expect(modifiedElem).toStrictEqual({
|
||||
x: 0,
|
||||
y: 0,
|
||||
detail: {
|
||||
background: '#A0A0A0'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -72,7 +72,10 @@ export {
|
|||
deleteElementInListByPosition,
|
||||
deleteElementInList,
|
||||
moveElementPosition,
|
||||
updateElementInList
|
||||
updateElementInList,
|
||||
updateElementInListByPosition
|
||||
} from './lib/handle-element';
|
||||
export { deepResizeGroupElement } from './lib/resize-element';
|
||||
export { calcViewCenterContent, calcViewCenter } from './lib/view-content';
|
||||
export { modifyElement, getModifiedElement } from './lib/modify';
|
||||
// export { ModifyRecorder } from './lib/modify-recorder';
|
||||
|
|
|
|||
|
|
@ -160,12 +160,14 @@ export function moveElementPosition(
|
|||
from: ElementPosition;
|
||||
to: ElementPosition;
|
||||
}
|
||||
): Elements {
|
||||
const { from, to } = opts;
|
||||
): { elements: Elements; from: ElementPosition; to: ElementPosition } {
|
||||
// const { from, to } = opts;
|
||||
const from = [...opts.from];
|
||||
const to = [...opts.to];
|
||||
|
||||
// [] -> [1,2,3] or [1, 2 ,3] -> []
|
||||
if (from.length === 0 || to.length === 0) {
|
||||
return elements;
|
||||
return { elements, from, to };
|
||||
}
|
||||
|
||||
// [1] -> [1, 2, 3]
|
||||
|
|
@ -173,7 +175,7 @@ export function moveElementPosition(
|
|||
for (let i = 0; i < from.length; i++) {
|
||||
if (to[i] === from[i]) {
|
||||
if (i === from.length - 1) {
|
||||
return elements;
|
||||
return { elements, from, to };
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
@ -181,10 +183,11 @@ export function moveElementPosition(
|
|||
}
|
||||
|
||||
const target = findElementFromListByPosition(from, elements);
|
||||
|
||||
if (target) {
|
||||
const insterResult = insertElementToListByPosition(target, to, elements);
|
||||
if (!insterResult) {
|
||||
return elements;
|
||||
return { elements, from, to };
|
||||
}
|
||||
|
||||
let trimDeletePosIndex = -1;
|
||||
|
|
@ -210,7 +213,7 @@ export function moveElementPosition(
|
|||
|
||||
deleteElementInListByPosition(from, elements);
|
||||
}
|
||||
return elements;
|
||||
return { elements, from, to };
|
||||
}
|
||||
|
||||
function mergeElement<T extends Element<ElementType> = Element<ElementType>>(originElem: T, updateContent: RecursivePartial<T>): T {
|
||||
|
|
@ -276,3 +279,23 @@ export function updateElementInList(uuid: string, updateContent: RecursivePartia
|
|||
}
|
||||
return targetElement;
|
||||
}
|
||||
|
||||
export function updateElementInListByPosition(
|
||||
position: ElementPosition,
|
||||
updateContent: RecursivePartial<Element<ElementType>>,
|
||||
elements: Element[]
|
||||
): Element | null {
|
||||
const elem: Element | null = findElementFromListByPosition(position, elements);
|
||||
if (elem) {
|
||||
if (elem.type === 'group' && elem.operations?.deepResize === true) {
|
||||
if ((updateContent.w && updateContent.w > 0) || (updateContent.h && updateContent.h > 0)) {
|
||||
deepResizeGroupElement(elem as Element<'group'>, {
|
||||
w: updateContent.w,
|
||||
h: updateContent.h
|
||||
});
|
||||
}
|
||||
}
|
||||
mergeElement(elem, updateContent);
|
||||
}
|
||||
return elem;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,10 @@ export const istype = {
|
|||
return parsePrototype(data) === 'AsyncFunction';
|
||||
},
|
||||
|
||||
boolean(data: any): boolean {
|
||||
return parsePrototype(data) === 'Boolean';
|
||||
},
|
||||
|
||||
string(data: any): boolean {
|
||||
return parsePrototype(data) === 'String';
|
||||
},
|
||||
|
|
|
|||
179
packages/util/src/lib/modify-recorder.ts
Normal file
179
packages/util/src/lib/modify-recorder.ts
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
import type { Data, ModifyType, ModifyContentMap, ModifyOptions, ModifyRecord } from '@idraw/types';
|
||||
import { deepClone } from './data';
|
||||
import { modifyElement } from './modify';
|
||||
|
||||
export interface ModifyRecorderOptions {
|
||||
recordable: boolean;
|
||||
}
|
||||
|
||||
export class ModifyRecorder {
|
||||
#doStack: ModifyRecord[] = [];
|
||||
#undoStack: ModifyRecord[] = [];
|
||||
#opts: ModifyRecorderOptions;
|
||||
|
||||
constructor(opts: ModifyRecorderOptions) {
|
||||
this.#opts = opts;
|
||||
}
|
||||
|
||||
#wrapRecord<T extends ModifyType>(opts: ModifyOptions<T>, modifiedContent: ModifyContentMap[T]): ModifyRecord<T> {
|
||||
const content = opts.content as ModifyContentMap[T];
|
||||
const modifyRecord: ModifyRecord<T> = {
|
||||
...deepClone<ModifyContentMap[T]>(content),
|
||||
// ...deepClone<ModifyContentMap[T]>(modifiedContent),
|
||||
type: opts.type,
|
||||
time: Date.now()
|
||||
} as ModifyRecord<T>;
|
||||
const record = modifyRecord as ModifyRecord<T>;
|
||||
if (opts.type === 'moveElement') {
|
||||
(modifyRecord as ModifyRecord<'moveElement'>).afterModifiedFrom = [...(modifiedContent as ModifyContentMap['moveElement']).from];
|
||||
(modifyRecord as ModifyRecord<'moveElement'>).afterModifiedTo = [...(modifiedContent as ModifyContentMap['moveElement']).to];
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
$getDoStack() {
|
||||
return this.#doStack;
|
||||
}
|
||||
|
||||
$getUndoStack() {
|
||||
return this.#undoStack;
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.#doStack = [];
|
||||
this.#undoStack = [];
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.clear();
|
||||
this.#doStack = null as any;
|
||||
this.#undoStack = null as any;
|
||||
}
|
||||
|
||||
do<T extends ModifyType>(data: Data, opts: ModifyOptions<T>): Data {
|
||||
const { data: modifiedData, content } = modifyElement(data, opts);
|
||||
if (this.#opts.recordable === true) {
|
||||
const record = this.#wrapRecord(opts, content);
|
||||
this.#doStack.push(record);
|
||||
}
|
||||
return modifiedData;
|
||||
}
|
||||
|
||||
undo(data: Data): Data | null {
|
||||
if (this.#opts.recordable !== true) {
|
||||
return null;
|
||||
}
|
||||
let modifiedData: Data | null = null;
|
||||
if (this.#doStack.length === 0) {
|
||||
return data;
|
||||
}
|
||||
const item = this.#doStack.pop();
|
||||
if (!item) {
|
||||
return data;
|
||||
}
|
||||
if (item?.type === 'addElement') {
|
||||
const record = item as ModifyRecord<'addElement'>;
|
||||
const { position, element } = record;
|
||||
modifiedData = modifyElement<'deleteElement'>(data, {
|
||||
type: 'deleteElement',
|
||||
content: {
|
||||
position,
|
||||
element
|
||||
}
|
||||
}).data;
|
||||
} else if (item?.type === 'updateElement') {
|
||||
const record = item as ModifyRecord<'updateElement'>;
|
||||
const { position, beforeModifiedElement, afterModifiedElement } = record;
|
||||
modifiedData = modifyElement<'updateElement'>(data, {
|
||||
type: 'updateElement',
|
||||
content: {
|
||||
position,
|
||||
beforeModifiedElement: afterModifiedElement,
|
||||
afterModifiedElement: beforeModifiedElement
|
||||
}
|
||||
}).data;
|
||||
} else if (item?.type === 'deleteElement') {
|
||||
const record = item as ModifyRecord<'deleteElement'>;
|
||||
const { position, element } = record;
|
||||
modifiedData = modifyElement<'addElement'>(data, {
|
||||
type: 'addElement',
|
||||
content: {
|
||||
position,
|
||||
element
|
||||
}
|
||||
}).data;
|
||||
} else if (item?.type === 'moveElement') {
|
||||
const record = item as ModifyRecord<'moveElement'>;
|
||||
const { afterModifiedFrom, afterModifiedTo } = record;
|
||||
const modifiedResult = modifyElement<'moveElement'>(data, {
|
||||
type: 'moveElement',
|
||||
content: {
|
||||
from: afterModifiedTo,
|
||||
to: afterModifiedFrom
|
||||
}
|
||||
});
|
||||
modifiedData = modifiedResult.data;
|
||||
}
|
||||
this.#undoStack.push(deepClone(item as ModifyRecord));
|
||||
return modifiedData;
|
||||
}
|
||||
|
||||
redo(data: Data): Data | null {
|
||||
if (this.#opts.recordable !== true) {
|
||||
return null;
|
||||
}
|
||||
let modifiedData: Data | null = null;
|
||||
if (this.#undoStack.length === 0) {
|
||||
return modifiedData;
|
||||
}
|
||||
const item = this.#undoStack.pop();
|
||||
if (!item) {
|
||||
return modifiedData;
|
||||
}
|
||||
|
||||
if (item?.type === 'addElement') {
|
||||
const record = item as ModifyRecord<'addElement'>;
|
||||
const { position, element } = record;
|
||||
modifiedData = modifyElement<'addElement'>(data, {
|
||||
type: 'addElement',
|
||||
content: {
|
||||
position,
|
||||
element
|
||||
}
|
||||
}).data;
|
||||
} else if (item?.type === 'updateElement') {
|
||||
const record = item as ModifyRecord<'updateElement'>;
|
||||
const { position, beforeModifiedElement, afterModifiedElement } = record;
|
||||
modifiedData = modifyElement<'updateElement'>(data, {
|
||||
type: 'updateElement',
|
||||
content: {
|
||||
position,
|
||||
beforeModifiedElement,
|
||||
afterModifiedElement
|
||||
}
|
||||
}).data;
|
||||
} else if (item?.type === 'deleteElement') {
|
||||
const record = item as ModifyRecord<'deleteElement'>;
|
||||
const { position, element } = record;
|
||||
modifiedData = modifyElement<'deleteElement'>(data, {
|
||||
type: 'deleteElement',
|
||||
content: {
|
||||
position,
|
||||
element
|
||||
}
|
||||
}).data;
|
||||
} else if (item?.type === 'moveElement') {
|
||||
const record = item as ModifyRecord<'moveElement'>;
|
||||
const { from, to } = record;
|
||||
modifiedData = modifyElement<'moveElement'>(data, {
|
||||
type: 'moveElement',
|
||||
content: {
|
||||
from,
|
||||
to
|
||||
}
|
||||
}).data;
|
||||
}
|
||||
this.#doStack.push(deepClone(item as ModifyRecord));
|
||||
return modifiedData;
|
||||
}
|
||||
}
|
||||
98
packages/util/src/lib/modify.ts
Normal file
98
packages/util/src/lib/modify.ts
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
import { Data, Element, ModifyOptions, ModifyType, ModifyContentMap, ModifiedElement } from '@idraw/types';
|
||||
import { insertElementToListByPosition, deleteElementInListByPosition, moveElementPosition, updateElementInListByPosition } from './handle-element';
|
||||
import { istype } from './istype';
|
||||
|
||||
export function modifyElement<T extends ModifyType = ModifyType>(data: Data, options: ModifyOptions<T>): { data: Data; content: ModifyContentMap[T] } {
|
||||
const { type } = options;
|
||||
const content: ModifyContentMap[T] = { ...options.content } as ModifyContentMap[T];
|
||||
if (type === 'addElement') {
|
||||
const opts: ModifyOptions<'addElement'> = options as ModifyOptions<'addElement'>;
|
||||
const { element, position } = opts.content;
|
||||
if (position?.length > 0) {
|
||||
insertElementToListByPosition(element, [...position], data.elements);
|
||||
} else {
|
||||
data.elements.push(element);
|
||||
}
|
||||
} else if (type === 'deleteElement') {
|
||||
const opts: ModifyOptions<'deleteElement'> = options as ModifyOptions<'deleteElement'>;
|
||||
const { position } = opts.content;
|
||||
deleteElementInListByPosition(position, data.elements);
|
||||
} else if (type === 'moveElement') {
|
||||
const opts: ModifyOptions<'moveElement'> = options as ModifyOptions<'moveElement'>;
|
||||
const { from, to } = opts.content;
|
||||
const movedResult = moveElementPosition(data.elements, { from, to });
|
||||
(content as ModifyContentMap['moveElement']).from = movedResult.from;
|
||||
(content as ModifyContentMap['moveElement']).to = movedResult.to;
|
||||
data.elements = movedResult.elements;
|
||||
} else if (type === 'updateElement') {
|
||||
const opts: ModifyOptions<'updateElement'> = options as ModifyOptions<'updateElement'>;
|
||||
const { position, afterModifiedElement } = opts.content;
|
||||
updateElementInListByPosition(position, afterModifiedElement, data.elements);
|
||||
}
|
||||
return { data, content };
|
||||
}
|
||||
|
||||
function _get(source: any, path: string, defaultValue = undefined) {
|
||||
// a.0.b -> ['a', '0', 'b']
|
||||
const keyList = path.split('.');
|
||||
const result = keyList.reduce((obj, key) => {
|
||||
return Object(obj)[key];
|
||||
}, source);
|
||||
return result === undefined ? defaultValue : result;
|
||||
}
|
||||
|
||||
function _set(obj: any, path: string, value: any) {
|
||||
// a.0.b -> ['a', '0', 'b']
|
||||
const keys = path.split('.');
|
||||
|
||||
if (typeof obj !== 'object') return obj;
|
||||
keys.reduce((o, k, i, _) => {
|
||||
if (i === _.length - 1) {
|
||||
o[k] = value;
|
||||
return null;
|
||||
} else if (k in o) {
|
||||
return o[k];
|
||||
} else {
|
||||
o[k] = /^[0-9]{1,}$/.test(_[i + 1]) ? [] : {};
|
||||
return o[k];
|
||||
}
|
||||
}, obj);
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function getModifiedElement(target: ModifiedElement, originElement: Element): ModifiedElement {
|
||||
const modifiedElement: ModifiedElement = {};
|
||||
const pathList: Array<number | string> = [];
|
||||
const _walk = (t: any) => {
|
||||
if (istype.json(t)) {
|
||||
const keys = Object.keys(t);
|
||||
keys.forEach((key: string) => {
|
||||
pathList.push(key);
|
||||
if (istype.json(t[key]) || istype.array(t[key])) {
|
||||
_walk(t[key]);
|
||||
} else {
|
||||
const pathStr = pathList.join('.');
|
||||
if (pathStr !== 'uuid') {
|
||||
const value = _get(originElement, pathStr);
|
||||
_set(modifiedElement, pathList.join('.'), value);
|
||||
}
|
||||
}
|
||||
pathList.pop();
|
||||
});
|
||||
} else if (istype.array(t)) {
|
||||
t.forEach((index: number) => {
|
||||
pathList.push(index);
|
||||
if (istype.json(t[index]) || istype.array(t[index])) {
|
||||
_walk(t[index]);
|
||||
} else {
|
||||
const value = _get(originElement, pathList.join('.'));
|
||||
_set(modifiedElement, pathList.join('.'), value);
|
||||
}
|
||||
pathList.pop();
|
||||
});
|
||||
}
|
||||
};
|
||||
_walk(target);
|
||||
|
||||
return modifiedElement;
|
||||
}
|
||||
Loading…
Reference in a new issue