feat: rename event name and add modify method

This commit is contained in:
chenshenhai 2024-02-17 15:47:24 +08:00
parent 99c002d543
commit cab61b4c76
14 changed files with 837 additions and 28 deletions

View file

@ -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 });
}

View file

@ -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>

View file

@ -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';
};
};

View file

@ -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> {

View file

@ -117,7 +117,8 @@ export {
deepResizeGroupElement,
deepCloneElement,
calcViewCenterContent,
calcViewCenter
calcViewCenter,
modifyElement
} from '@idraw/util';
export { iDraw } from './idraw';
export type { IDrawEvent, IDrawEventKeys } from './event';

View file

@ -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';

View file

@ -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];

View 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([]);
}
});
});

View 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'
}
});
});
});

View file

@ -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';

View file

@ -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;
}

View file

@ -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';
},

View 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;
}
}

View 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;
}