diff --git a/packages/core/src/middleware/selector/index.ts b/packages/core/src/middleware/selector/index.ts index 1924499..3f20b95 100644 --- a/packages/core/src/middleware/selector/index.ts +++ b/packages/core/src/middleware/selector/index.ts @@ -565,9 +565,9 @@ export const MiddlewareSelector: BoardMiddleware
- +
diff --git a/packages/idraw/src/event.ts b/packages/idraw/src/event.ts index 2cc9695..d613d71 100644 --- a/packages/idraw/src/event.ts +++ b/packages/idraw/src/event.ts @@ -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'; }; }; diff --git a/packages/idraw/src/idraw.ts b/packages/idraw/src/idraw.ts index efb56b1..52ed790 100644 --- a/packages/idraw/src/idraw.ts +++ b/packages/idraw/src/idraw.ts @@ -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 { diff --git a/packages/idraw/src/index.ts b/packages/idraw/src/index.ts index 848efa2..1175862 100644 --- a/packages/idraw/src/index.ts +++ b/packages/idraw/src/index.ts @@ -117,7 +117,8 @@ export { deepResizeGroupElement, deepCloneElement, calcViewCenterContent, - calcViewCenter + calcViewCenter, + modifyElement } from '@idraw/util'; export { iDraw } from './idraw'; export type { IDrawEvent, IDrawEventKeys } from './event'; diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index a8e458c..a91c616 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -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'; diff --git a/packages/types/src/lib/modify.ts b/packages/types/src/lib/modify.ts index 6e43942..e8705bb 100644 --- a/packages/types/src/lib/modify.ts +++ b/packages/types/src/lib/modify.ts @@ -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, 'uuid'> & { uuid: string }; +export type ModifiedElement = Omit, '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 { +export interface ModifyOptions { type: T; - data: ModifyDataMap[T]; - time: number; + content: ModifyContentMap[T]; } + +export interface ModifyRecordMap { + updateElement: { + type: 'updateElement'; + time: number; + } & Required; + addElement: { + type: 'addElement'; + time: number; + } & Required; + deleteElement: { + type: 'deleteElement'; + time: number; + } & Required; + moveElement: { + type: 'moveElement'; + time: number; + afterModifiedFrom: ElementPosition; + afterModifiedTo: ElementPosition; + } & Required; +} + +export type ModifyRecord = ModifyRecordMap[T]; diff --git a/packages/util/__tests__/lib/modify-recorder.test.ts b/packages/util/__tests__/lib/modify-recorder.test.ts new file mode 100644 index 0000000..858b954 --- /dev/null +++ b/packages/util/__tests__/lib/modify-recorder.test.ts @@ -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([]); + } + }); +}); diff --git a/packages/util/__tests__/lib/modify.test.ts b/packages/util/__tests__/lib/modify.test.ts new file mode 100644 index 0000000..5bb859a --- /dev/null +++ b/packages/util/__tests__/lib/modify.test.ts @@ -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' + } + }); + }); +}); diff --git a/packages/util/src/index.ts b/packages/util/src/index.ts index 7fa1e8c..039183b 100644 --- a/packages/util/src/index.ts +++ b/packages/util/src/index.ts @@ -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'; diff --git a/packages/util/src/lib/handle-element.ts b/packages/util/src/lib/handle-element.ts index e9e2cc1..fe655be 100644 --- a/packages/util/src/lib/handle-element.ts +++ b/packages/util/src/lib/handle-element.ts @@ -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 = Element>(originElem: T, updateContent: RecursivePartial): T { @@ -276,3 +279,23 @@ export function updateElementInList(uuid: string, updateContent: RecursivePartia } return targetElement; } + +export function updateElementInListByPosition( + position: ElementPosition, + updateContent: RecursivePartial>, + 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; +} diff --git a/packages/util/src/lib/istype.ts b/packages/util/src/lib/istype.ts index 3123697..735effd 100644 --- a/packages/util/src/lib/istype.ts +++ b/packages/util/src/lib/istype.ts @@ -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'; }, diff --git a/packages/util/src/lib/modify-recorder.ts b/packages/util/src/lib/modify-recorder.ts new file mode 100644 index 0000000..89c8c63 --- /dev/null +++ b/packages/util/src/lib/modify-recorder.ts @@ -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(opts: ModifyOptions, modifiedContent: ModifyContentMap[T]): ModifyRecord { + const content = opts.content as ModifyContentMap[T]; + const modifyRecord: ModifyRecord = { + ...deepClone(content), + // ...deepClone(modifiedContent), + type: opts.type, + time: Date.now() + } as ModifyRecord; + const record = modifyRecord as ModifyRecord; + 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(data: Data, opts: ModifyOptions): 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; + } +} diff --git a/packages/util/src/lib/modify.ts b/packages/util/src/lib/modify.ts new file mode 100644 index 0000000..4fd1911 --- /dev/null +++ b/packages/util/src/lib/modify.ts @@ -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(data: Data, options: ModifyOptions): { 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 = []; + 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; +}