mirror of
https://github.com/idrawjs/idraw
synced 2026-05-23 09:38:22 +00:00
feat: add history and fix text-editor middleware
This commit is contained in:
parent
a901ab205c
commit
8a04ea0106
133 changed files with 4489 additions and 2904 deletions
|
|
@ -20,15 +20,19 @@ module.exports = {
|
|||
modulePaths: ['<rootDir>'],
|
||||
testRegex: '(/packages/([^/]{1,})/__tests__/.*)\\.test.ts$',
|
||||
testPathIgnorePatterns: [
|
||||
'(/packages/board/__tests__/.*)\\.test.ts$',
|
||||
// TODO
|
||||
'(/packages/core/__tests__/.*)\\.test.ts$',
|
||||
'(/packages/idraw/__tests__/.*)\\.test.ts$',
|
||||
// '(/packages/idraw/__tests__/.*)\\.test.ts$',
|
||||
'(/packages/renderer/__tests__/.*)\\.test.ts$',
|
||||
'(/packages/types/__tests__/.*)\\.test.ts$'
|
||||
],
|
||||
moduleNameMapper: {
|
||||
'@idraw/util': '<rootDir>/packages/util/src/index.ts'
|
||||
'@idraw/types': '<rootDir>/packages/types/src/index.ts',
|
||||
'@idraw/util': '<rootDir>/packages/util/src/index.ts',
|
||||
'@idraw/renderer': '<rootDir>/packages/renderer/src/index.ts',
|
||||
'@idraw/core': '<rootDir>/packages/core/src/index.ts',
|
||||
'^idraw$': '<rootDir>/packages/idraw/src/index.ts'
|
||||
},
|
||||
// "testRegex": "(/packages/idraw/__tests__/.*)\\.test.ts$",
|
||||
setupFiles: ['jest-canvas-mock']
|
||||
setupFiles: ['jest-canvas-mock', './jest.setup.js']
|
||||
};
|
||||
|
|
|
|||
29
jest.setup.js
Normal file
29
jest.setup.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/* eslint-disable no-undef */
|
||||
// mock OffscreenCanvas
|
||||
global.OffscreenCanvas = class OffscreenCanvas {
|
||||
constructor(width, height) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
this.__canvas = canvas;
|
||||
}
|
||||
getContext(type) {
|
||||
return this.__canvas.getContext(type);
|
||||
}
|
||||
get width() {
|
||||
return this.__canvas.width;
|
||||
}
|
||||
set width(value) {
|
||||
this.__canvas.width = value;
|
||||
}
|
||||
get height() {
|
||||
return this.__canvas.height;
|
||||
}
|
||||
set height(value) {
|
||||
this.__canvas.height = value;
|
||||
}
|
||||
|
||||
get canvas() {
|
||||
return this.__canvas;
|
||||
}
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"private": false,
|
||||
"version": "0.4.0-beta.40",
|
||||
"version": "0.4.0-beta.41",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ import type {
|
|||
ViewSizeInfo,
|
||||
PointSize,
|
||||
BoardExtendEventMap,
|
||||
UtilEventEmitter,
|
||||
ModifyOptions
|
||||
UtilEventEmitter
|
||||
} from '@idraw/types';
|
||||
import { BoardWatcher } from './watcher';
|
||||
import { Sharer } from './sharer';
|
||||
|
|
@ -30,8 +29,8 @@ interface BoardMiddlewareMapItem {
|
|||
|
||||
export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
||||
#opts: BoardOptions;
|
||||
#middlewareMap: WeakMap<BoardMiddleware, BoardMiddlewareMapItem> = new WeakMap();
|
||||
#middlewares: BoardMiddleware[] = [];
|
||||
#middlewareMap: Map<BoardMiddleware, BoardMiddlewareMapItem> = new Map();
|
||||
// #middlewares: BoardMiddleware[] = [];
|
||||
#activeMiddlewareObjs: BoardMiddlewareObject[] = [];
|
||||
#watcher: BoardWatcher;
|
||||
#renderer: Renderer;
|
||||
|
|
@ -291,13 +290,12 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
|||
|
||||
#resetActiveMiddlewareObjs() {
|
||||
const activeMiddlewareObjs: BoardMiddlewareObject[] = [];
|
||||
const middlewareMap = this.#middlewareMap;
|
||||
this.#middlewares.forEach((middleware: BoardMiddleware) => {
|
||||
const item = middlewareMap.get(middleware);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
for (const [_, item] of this.#middlewareMap) {
|
||||
if (item?.status === 'enable' && item?.middlewareObject) {
|
||||
activeMiddlewareObjs.push(item.middlewareObject);
|
||||
}
|
||||
});
|
||||
}
|
||||
this.#activeMiddlewareObjs = activeMiddlewareObjs;
|
||||
}
|
||||
|
||||
|
|
@ -313,13 +311,7 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
|||
return this.#renderer;
|
||||
}
|
||||
|
||||
setData(
|
||||
data: Data,
|
||||
opts?: {
|
||||
modifiedOptions?: ModifyOptions; // TODO
|
||||
}
|
||||
): { viewSizeInfo: ViewSizeInfo } {
|
||||
const { modifiedOptions } = opts || {};
|
||||
setData(data: Data): { viewSizeInfo: ViewSizeInfo } {
|
||||
const sharer = this.#sharer;
|
||||
this.#sharer.setActiveStorage('data', data);
|
||||
const viewSizeInfo = sharer.getActiveViewSizeInfo();
|
||||
|
|
@ -330,17 +322,11 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
|||
viewHeight: viewSizeInfo.height,
|
||||
extend: true
|
||||
});
|
||||
if (modifiedOptions) {
|
||||
this.#viewer.resetVirtualFlatItemMap(data, {
|
||||
viewSizeInfo,
|
||||
viewScaleInfo
|
||||
});
|
||||
} else {
|
||||
this.#viewer.resetVirtualFlatItemMap(data, {
|
||||
viewSizeInfo,
|
||||
viewScaleInfo
|
||||
});
|
||||
}
|
||||
|
||||
this.#viewer.resetVirtualFlatItemMap(data, {
|
||||
viewSizeInfo,
|
||||
viewScaleInfo
|
||||
});
|
||||
|
||||
this.#viewer.drawFrame();
|
||||
const newViewSizeInfo = {
|
||||
|
|
@ -360,14 +346,14 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
|||
use<C extends any = any>(middleware: BoardMiddleware<any, any, any>, config?: Partial<C>) {
|
||||
if (this.#middlewareMap.has(middleware)) {
|
||||
const item = this.#middlewareMap.get(middleware);
|
||||
if (item) {
|
||||
item.middlewareObject.use?.();
|
||||
if (item && item.status !== 'enable') {
|
||||
item.status = 'enable';
|
||||
this.#middlewareMap.set(middleware, item);
|
||||
item.middlewareObject.use?.();
|
||||
this.#resetActiveMiddlewareObjs();
|
||||
return;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const { boardContent, container } = this.#opts;
|
||||
const sharer = this.#sharer;
|
||||
const viewer = this.#viewer;
|
||||
|
|
@ -379,8 +365,6 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
|||
config
|
||||
);
|
||||
obj.use?.();
|
||||
this.#middlewares.push(middleware);
|
||||
this.#activeMiddlewareObjs.push(obj);
|
||||
|
||||
this.#middlewareMap.set(middleware, {
|
||||
status: 'enable',
|
||||
|
|
@ -391,11 +375,13 @@ export class Board<T extends BoardExtendEventMap = BoardExtendEventMap> {
|
|||
}
|
||||
|
||||
disuse(middleware: BoardMiddleware<any, any>) {
|
||||
const item = this.#middlewareMap.get(middleware);
|
||||
if (item) {
|
||||
item.middlewareObject.disuse?.();
|
||||
item.status = 'disable';
|
||||
this.#middlewareMap.set(middleware, item);
|
||||
if (this.#middlewareMap.has(middleware)) {
|
||||
const item = this.#middlewareMap.get(middleware);
|
||||
if (item) {
|
||||
item.middlewareObject.disuse?.();
|
||||
item.status = 'disable';
|
||||
}
|
||||
this.#middlewareMap.delete(middleware);
|
||||
this.#resetActiveMiddlewareObjs();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,41 @@ import type {
|
|||
Data,
|
||||
PointSize,
|
||||
CoreOptions,
|
||||
BoardMiddleware,
|
||||
Middleware,
|
||||
ViewSizeInfo,
|
||||
CoreEventMap,
|
||||
ViewScaleInfo,
|
||||
LoadItemMap,
|
||||
ModifyOptions
|
||||
ElementType,
|
||||
RecursivePartial,
|
||||
Element,
|
||||
ModifyRecord,
|
||||
ElementPosition,
|
||||
DataLayout,
|
||||
FlattenLayout,
|
||||
DataGlobal
|
||||
} from '@idraw/types';
|
||||
import {
|
||||
deepClone,
|
||||
createElement,
|
||||
getElementPositionFromList,
|
||||
toFlattenElement,
|
||||
deleteElementInList,
|
||||
findElementFromListByPosition,
|
||||
updateElementInListByPosition,
|
||||
insertElementToListByPosition,
|
||||
moveElementPosition,
|
||||
toFlattenLayout,
|
||||
toFlattenGlobal,
|
||||
get,
|
||||
mergeLayout,
|
||||
mergeGlobal
|
||||
} from '@idraw/util';
|
||||
import { Board, Sharer, Calculator } from './board';
|
||||
import { createBoardContent, validateElements } from '@idraw/util';
|
||||
import { Cursor } from './lib/cursor';
|
||||
import { getModifyElementRecord } from './record';
|
||||
|
||||
export { coreEventKeys } from './config';
|
||||
export type { CoreEventKeys } from './config';
|
||||
|
||||
|
|
@ -77,26 +102,21 @@ export class Core<E extends CoreEventMap = CoreEventMap> {
|
|||
container.style.position = 'relative';
|
||||
}
|
||||
|
||||
use<C extends any = any>(middleware: BoardMiddleware<any, any, any>, config?: C) {
|
||||
use<C extends any = any>(middleware: Middleware<any, any, any>, config?: C) {
|
||||
this.#board.use<C>(middleware, config);
|
||||
}
|
||||
|
||||
disuse(middleware: BoardMiddleware<any, any, any>) {
|
||||
disuse(middleware: Middleware<any, any, any>) {
|
||||
this.#board.disuse(middleware);
|
||||
}
|
||||
|
||||
resetMiddlewareConfig<C extends any = any>(middleware: BoardMiddleware<any, any, any>, config?: Partial<C>) {
|
||||
resetMiddlewareConfig<C extends any = any>(middleware: Middleware<any, any, any>, config?: Partial<C>) {
|
||||
this.#board.resetMiddlewareConfig(middleware, config);
|
||||
}
|
||||
|
||||
setData(
|
||||
data: Data,
|
||||
opts?: {
|
||||
modifiedOptions?: ModifyOptions;
|
||||
}
|
||||
) {
|
||||
setData(data: Data) {
|
||||
validateElements(data?.elements || []);
|
||||
this.#board.setData(data, opts);
|
||||
this.#board.setData(data);
|
||||
}
|
||||
|
||||
getData(): Data | null {
|
||||
|
|
@ -168,4 +188,265 @@ export class Core<E extends CoreEventMap = CoreEventMap> {
|
|||
offBoardWatcherEvents() {
|
||||
this.#board.offWatcherEvents();
|
||||
}
|
||||
|
||||
createElement<T extends ElementType = ElementType>(
|
||||
type: T,
|
||||
element: RecursivePartial<Element<T>>,
|
||||
opts?: {
|
||||
viewCenter?: boolean;
|
||||
}
|
||||
): Element<T> {
|
||||
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
|
||||
return createElement<T>(
|
||||
type,
|
||||
element || {},
|
||||
opts?.viewCenter === true
|
||||
? {
|
||||
viewScaleInfo,
|
||||
viewSizeInfo
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
}
|
||||
|
||||
updateElement(element: Element): ModifyRecord<'updateElement'> | null {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
const uuid = element.uuid;
|
||||
const position = getElementPositionFromList(uuid, data.elements);
|
||||
const beforeElem = findElementFromListByPosition(position, data.elements);
|
||||
if (!beforeElem) {
|
||||
return null;
|
||||
}
|
||||
const before = toFlattenElement(beforeElem);
|
||||
const updatedElement = updateElementInListByPosition(position, element, data.elements, { strict: true }) as Element;
|
||||
const after = toFlattenElement(updatedElement);
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
|
||||
const modifyRecord: ModifyRecord<'updateElement'> = {
|
||||
type: 'updateElement',
|
||||
time: Date.now(),
|
||||
content: { method: 'updateElement', uuid, before, after }
|
||||
};
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
modifyElement(
|
||||
element: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
|
||||
): ModifyRecord<'modifyElement'> | null {
|
||||
const { uuid, ...restElement } = element;
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
const position = getElementPositionFromList(uuid, data.elements);
|
||||
const beforeElem = findElementFromListByPosition(position, data.elements);
|
||||
if (!beforeElem) {
|
||||
return null;
|
||||
}
|
||||
const modifyRecord: ModifyRecord<'modifyElement'> = getModifyElementRecord({
|
||||
modifiedElement: element,
|
||||
beforeElement: beforeElem
|
||||
});
|
||||
updateElementInListByPosition(position, restElement, data.elements) as Element;
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
modifyElements(
|
||||
elements: Array<RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>>
|
||||
): ModifyRecord<'modifyElements'> | null {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
let modifyRecord: ModifyRecord<'modifyElements'> | null = null;
|
||||
const before: (FlattenLayout & { uuid: string })[] = [];
|
||||
const after: (FlattenLayout & { uuid: string })[] = [];
|
||||
elements.forEach((element) => {
|
||||
const { uuid, ...restElement } = element;
|
||||
const position = getElementPositionFromList(uuid, data.elements);
|
||||
const beforeElem = findElementFromListByPosition(position, data.elements);
|
||||
if (!beforeElem) {
|
||||
return null;
|
||||
}
|
||||
const tempRecord = getModifyElementRecord({
|
||||
modifiedElement: element,
|
||||
beforeElement: beforeElem
|
||||
});
|
||||
if (tempRecord.content) {
|
||||
before.push({
|
||||
...tempRecord.content.before,
|
||||
uuid
|
||||
});
|
||||
after.push({
|
||||
...tempRecord.content.after,
|
||||
uuid
|
||||
});
|
||||
}
|
||||
updateElementInListByPosition(position, restElement, data.elements) as Element;
|
||||
});
|
||||
|
||||
modifyRecord = {
|
||||
type: 'modifyElements',
|
||||
time: Date.now(),
|
||||
content: {
|
||||
method: 'modifyElements',
|
||||
before,
|
||||
after
|
||||
}
|
||||
};
|
||||
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
addElement(
|
||||
element: Element,
|
||||
opts?: {
|
||||
position: ElementPosition;
|
||||
}
|
||||
): ModifyRecord<'addElement'> {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
|
||||
if (!opts || !opts?.position?.length) {
|
||||
data.elements.push(element);
|
||||
} else if (opts?.position) {
|
||||
const position = [...(opts?.position || [])];
|
||||
insertElementToListByPosition(element, position, data.elements);
|
||||
}
|
||||
const position: ElementPosition = getElementPositionFromList(element.uuid, data.elements);
|
||||
const modifyRecord: ModifyRecord<'addElement'> = {
|
||||
type: 'addElement',
|
||||
time: Date.now(),
|
||||
content: { method: 'addElement', uuid: element.uuid, position, element: deepClone(element) }
|
||||
};
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
deleteElement(uuid: string): ModifyRecord<'deleteElement'> {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
const position = getElementPositionFromList(uuid, data.elements);
|
||||
const element = findElementFromListByPosition(position, data.elements);
|
||||
const modifyRecord: ModifyRecord<'deleteElement'> = {
|
||||
type: 'deleteElement',
|
||||
time: Date.now(),
|
||||
content: { method: 'deleteElement', uuid, position, element: element ? deepClone(element) : null }
|
||||
};
|
||||
deleteElementInList(uuid, data.elements);
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
moveElement(uuid: string, to: ElementPosition): ModifyRecord<'moveElement'> {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
const from = getElementPositionFromList(uuid, data.elements);
|
||||
|
||||
const modifyRecord: ModifyRecord<'moveElement'> = {
|
||||
type: 'moveElement',
|
||||
time: Date.now(),
|
||||
content: { method: 'moveElement', uuid, from: [...from], to: [...to] }
|
||||
};
|
||||
const { elements: list } = moveElementPosition(data.elements, { from, to });
|
||||
data.elements = list;
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
modifyLayout(layout: RecursivePartial<DataLayout> | null): ModifyRecord<'modifyLayout'> {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
const modifyRecord: ModifyRecord<'modifyLayout'> = {
|
||||
type: 'modifyLayout',
|
||||
time: Date.now(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: null,
|
||||
after: null
|
||||
}
|
||||
};
|
||||
|
||||
if (layout === null) {
|
||||
if (data.layout) {
|
||||
modifyRecord.content.before = toFlattenLayout(data.layout);
|
||||
delete data['layout'];
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
} else {
|
||||
return modifyRecord;
|
||||
}
|
||||
}
|
||||
|
||||
const beforeLayout = data.layout;
|
||||
let before: FlattenLayout = {};
|
||||
const after: FlattenLayout = toFlattenLayout(layout);
|
||||
|
||||
if (data.layout) {
|
||||
Object.keys(after).forEach((key: string) => {
|
||||
let val = get(beforeLayout, key);
|
||||
if (val === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(key)) {
|
||||
key = key.replace(/\[[0-9]{1,}\]$/, '');
|
||||
val = get(beforeLayout, key);
|
||||
}
|
||||
before[key] = val;
|
||||
});
|
||||
before = toFlattenLayout(before);
|
||||
modifyRecord.content.before = before;
|
||||
} else {
|
||||
data.layout = {} as any;
|
||||
}
|
||||
|
||||
modifyRecord.content.after = after;
|
||||
mergeLayout(data.layout as DataLayout, layout) as DataLayout;
|
||||
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
|
||||
modifyGlobal(global: RecursivePartial<DataGlobal> | null) {
|
||||
const data: Data = this.getData() || { elements: [] };
|
||||
const modifyRecord: ModifyRecord<'modifyGlobal'> = {
|
||||
type: 'modifyGlobal',
|
||||
time: Date.now(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: null,
|
||||
after: null
|
||||
}
|
||||
};
|
||||
|
||||
if (global === null) {
|
||||
if (data.global) {
|
||||
modifyRecord.content.before = toFlattenGlobal(data.global);
|
||||
delete data['global'];
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
} else {
|
||||
return modifyRecord;
|
||||
}
|
||||
}
|
||||
|
||||
const beforeGlobal = data.global;
|
||||
let before: FlattenLayout = {};
|
||||
const after: FlattenLayout = toFlattenGlobal(global);
|
||||
|
||||
if (data.global) {
|
||||
Object.keys(after).forEach((key: string) => {
|
||||
before[key] = get(beforeGlobal, key);
|
||||
});
|
||||
before = toFlattenGlobal(before);
|
||||
modifyRecord.content.before = before;
|
||||
} else {
|
||||
data.global = {} as any;
|
||||
}
|
||||
|
||||
modifyRecord.content.after = after;
|
||||
mergeGlobal(data.global as DataGlobal, global) as DataGlobal;
|
||||
|
||||
this.setData(data);
|
||||
this.refresh();
|
||||
return modifyRecord;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { BoardMiddleware, CoreEventMap, Point } from '@idraw/types';
|
||||
import type { Middleware, CoreEventMap, Point } from '@idraw/types';
|
||||
import { coreEventKeys } from '../../config';
|
||||
|
||||
const key = 'DRAG';
|
||||
|
|
@ -8,7 +8,7 @@ type DraggerSharedStorage = {
|
|||
[keyPrevPoint]: Point | null;
|
||||
};
|
||||
|
||||
export const MiddlewareDragger: BoardMiddleware<DraggerSharedStorage, CoreEventMap> = (opts) => {
|
||||
export const MiddlewareDragger: Middleware<DraggerSharedStorage, CoreEventMap> = (opts) => {
|
||||
const { eventHub, sharer, viewer } = opts;
|
||||
let isDragging = false;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,15 @@ const fontFamily = 'monospace';
|
|||
|
||||
export function drawSizeInfoText(
|
||||
ctx: ViewContext2D,
|
||||
opts: { point: PointSize; rotateCenter: PointSize; angle: number; text: string; fontSize: number; lineHeight: number; style: MiddlewareInfoStyle }
|
||||
opts: {
|
||||
point: PointSize;
|
||||
rotateCenter: PointSize;
|
||||
angle: number;
|
||||
text: string;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
style: MiddlewareInfoStyle;
|
||||
}
|
||||
) {
|
||||
const { point, rotateCenter, angle, text, style, fontSize, lineHeight } = opts;
|
||||
const { textColor, textBackground } = style;
|
||||
|
|
@ -39,7 +47,7 @@ export function drawSizeInfoText(
|
|||
ctx.lineTo(bgEnd.x, bgEnd.y);
|
||||
ctx.lineTo(bgStart.x, bgEnd.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textBaseline = 'top';
|
||||
|
|
@ -49,7 +57,15 @@ export function drawSizeInfoText(
|
|||
|
||||
export function drawPositionInfoText(
|
||||
ctx: ViewContext2D,
|
||||
opts: { point: PointSize; rotateCenter: PointSize; angle: number; text: string; fontSize: number; lineHeight: number; style: MiddlewareInfoStyle }
|
||||
opts: {
|
||||
point: PointSize;
|
||||
rotateCenter: PointSize;
|
||||
angle: number;
|
||||
text: string;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
style: MiddlewareInfoStyle;
|
||||
}
|
||||
) {
|
||||
const { point, rotateCenter, angle, text, style, fontSize, lineHeight } = opts;
|
||||
const { textBackground, textColor } = style;
|
||||
|
|
@ -82,7 +98,7 @@ export function drawPositionInfoText(
|
|||
ctx.lineTo(bgEnd.x, bgEnd.y);
|
||||
ctx.lineTo(bgStart.x, bgEnd.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textBaseline = 'top';
|
||||
|
|
@ -92,7 +108,15 @@ export function drawPositionInfoText(
|
|||
|
||||
export function drawAngleInfoText(
|
||||
ctx: ViewContext2D,
|
||||
opts: { point: PointSize; rotateCenter: PointSize; angle: number; text: string; fontSize: number; lineHeight: number; style: MiddlewareInfoStyle }
|
||||
opts: {
|
||||
point: PointSize;
|
||||
rotateCenter: PointSize;
|
||||
angle: number;
|
||||
text: string;
|
||||
fontSize: number;
|
||||
lineHeight: number;
|
||||
style: MiddlewareInfoStyle;
|
||||
}
|
||||
) {
|
||||
const { point, rotateCenter, angle, text, style, fontSize, lineHeight } = opts;
|
||||
const { textBackground, textColor } = style;
|
||||
|
|
@ -125,7 +149,7 @@ export function drawAngleInfoText(
|
|||
ctx.lineTo(bgEnd.x, bgEnd.y);
|
||||
ctx.lineTo(bgStart.x, bgEnd.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.textBaseline = 'top';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,13 @@
|
|||
import type { BoardMiddleware, ViewRectInfo, Element, MiddlewareInfoConfig, CoreEventMap } from '@idraw/types';
|
||||
import { formatNumber, getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot, createUUID, limitAngle, rotatePoint, parseAngleToRadian } from '@idraw/util';
|
||||
import type { Middleware, ViewRectInfo, Element, MiddlewareInfoConfig, CoreEventMap } from '@idraw/types';
|
||||
import {
|
||||
formatNumber,
|
||||
getViewScaleInfoFromSnapshot,
|
||||
getViewSizeInfoFromSnapshot,
|
||||
createUUID,
|
||||
limitAngle,
|
||||
rotatePoint,
|
||||
parseAngleToRadian
|
||||
} from '@idraw/util';
|
||||
import { keySelectedElementList, keyActionType, keyGroupQueue } from '../selector';
|
||||
import { drawSizeInfoText, drawPositionInfoText, drawAngleInfoText } from './draw-info';
|
||||
import type { DeepInfoSharedStorage } from './types';
|
||||
|
|
@ -10,7 +18,7 @@ export { MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE };
|
|||
const infoFontSize = 10;
|
||||
const infoLineHeight = 16;
|
||||
|
||||
export const MiddlewareInfo: BoardMiddleware<
|
||||
export const MiddlewareInfo: Middleware<
|
||||
DeepInfoSharedStorage,
|
||||
CoreEventMap & {
|
||||
[MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE]: { show: boolean };
|
||||
|
|
|
|||
|
|
@ -1,10 +1,21 @@
|
|||
import type { BoardMiddleware, ElementSize, Point, MiddlewareLayoutSelectorConfig, CoreEventMap } from '@idraw/types';
|
||||
import type {
|
||||
Middleware,
|
||||
ElementSize,
|
||||
Point,
|
||||
MiddlewareLayoutSelectorConfig,
|
||||
CoreEventMap,
|
||||
RecursivePartial,
|
||||
ModifyRecord,
|
||||
DataLayout
|
||||
} from '@idraw/types';
|
||||
import {
|
||||
calcLayoutSizeController,
|
||||
isViewPointInVertexes,
|
||||
getViewScaleInfoFromSnapshot,
|
||||
isViewPointInElementSize,
|
||||
calcViewElementSize
|
||||
calcViewElementSize,
|
||||
getElementSize,
|
||||
toFlattenLayout
|
||||
} from '@idraw/util';
|
||||
import type { LayoutSelectorSharedStorage, ControlType } from './types';
|
||||
import {
|
||||
|
|
@ -27,7 +38,7 @@ import { coreEventKeys } from '../../config';
|
|||
|
||||
export { keyLayoutIsSelected, keyLayoutIsBusyMoving };
|
||||
|
||||
export const MiddlewareLayoutSelector: BoardMiddleware<
|
||||
export const MiddlewareLayoutSelector: Middleware<
|
||||
LayoutSelectorSharedStorage,
|
||||
CoreEventMap,
|
||||
MiddlewareLayoutSelectorConfig
|
||||
|
|
@ -43,6 +54,8 @@ export const MiddlewareLayoutSelector: BoardMiddleware<
|
|||
let prevIsHoverContent: boolean | null = null;
|
||||
let prevIsSelected: boolean | null = null;
|
||||
|
||||
let pointStartLayoutSize: RecursivePartial<ElementSize> | null = null;
|
||||
|
||||
const clear = () => {
|
||||
prevPoint = null;
|
||||
sharer.setSharedStorage(keyLayoutActionType, null);
|
||||
|
|
@ -241,6 +254,12 @@ export const MiddlewareLayoutSelector: BoardMiddleware<
|
|||
}
|
||||
sharer.setSharedStorage(keyLayoutIsSelected, false);
|
||||
}
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (data?.layout) {
|
||||
pointStartLayoutSize = getElementSize(data.layout as any);
|
||||
} else {
|
||||
pointStartLayoutSize = null;
|
||||
}
|
||||
|
||||
resetController();
|
||||
const layoutControlType = resetControlType(e);
|
||||
|
|
@ -359,11 +378,25 @@ export const MiddlewareLayoutSelector: BoardMiddleware<
|
|||
const layoutControlType = sharer.getSharedStorage(keyLayoutControlType);
|
||||
const data = sharer.getActiveStorage('data');
|
||||
if (data && layoutActionType === 'resize' && layoutControlType) {
|
||||
let modifyRecord: ModifyRecord<'modifyLayout'> | undefined = undefined;
|
||||
if (pointStartLayoutSize) {
|
||||
modifyRecord = {
|
||||
type: 'modifyLayout',
|
||||
time: Date.now(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: toFlattenLayout(pointStartLayoutSize as DataLayout),
|
||||
after: toFlattenLayout(getElementSize(data.layout as any) as DataLayout)
|
||||
}
|
||||
};
|
||||
}
|
||||
eventHub.trigger(coreEventKeys.CHANGE, {
|
||||
type: 'dragLayout',
|
||||
data
|
||||
data,
|
||||
modifyRecord
|
||||
});
|
||||
}
|
||||
pointStartLayoutSize = null;
|
||||
|
||||
sharer.setSharedStorage(keyLayoutActionType, null);
|
||||
sharer.setSharedStorage(keyLayoutControlType, null);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,11 @@
|
|||
import type { ViewContext2D, LayoutSizeController, ViewRectVertexes, PointSize, ElementSize, MiddlewareLayoutSelectorStyle } from '@idraw/types';
|
||||
import type {
|
||||
ViewContext2D,
|
||||
LayoutSizeController,
|
||||
ViewRectVertexes,
|
||||
PointSize,
|
||||
ElementSize,
|
||||
MiddlewareLayoutSelectorStyle
|
||||
} from '@idraw/types';
|
||||
|
||||
function drawControllerBox(ctx: ViewContext2D, boxVertexes: ViewRectVertexes, style: MiddlewareLayoutSelectorStyle) {
|
||||
const { activeColor } = style;
|
||||
|
|
@ -10,7 +17,7 @@ function drawControllerBox(ctx: ViewContext2D, boxVertexes: ViewRectVertexes, st
|
|||
ctx.lineTo(boxVertexes[2].x, boxVertexes[2].y);
|
||||
ctx.lineTo(boxVertexes[3].x, boxVertexes[3].y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
ctx.strokeStyle = activeColor;
|
||||
ctx.lineWidth = 2;
|
||||
|
|
@ -52,9 +59,24 @@ export function drawLayoutController(
|
|||
const { topLeft, topRight, bottomLeft, bottomRight, topMiddle, rightMiddle, bottomMiddle, leftMiddle } = controller;
|
||||
|
||||
drawControllerLine(ctx, { start: topLeft.center, end: topRight.center, centerVertexes: topMiddle.vertexes, style });
|
||||
drawControllerLine(ctx, { start: topRight.center, end: bottomRight.center, centerVertexes: rightMiddle.vertexes, style });
|
||||
drawControllerLine(ctx, { start: bottomRight.center, end: bottomLeft.center, centerVertexes: bottomMiddle.vertexes, style });
|
||||
drawControllerLine(ctx, { start: bottomLeft.center, end: topLeft.center, centerVertexes: leftMiddle.vertexes, style });
|
||||
drawControllerLine(ctx, {
|
||||
start: topRight.center,
|
||||
end: bottomRight.center,
|
||||
centerVertexes: rightMiddle.vertexes,
|
||||
style
|
||||
});
|
||||
drawControllerLine(ctx, {
|
||||
start: bottomRight.center,
|
||||
end: bottomLeft.center,
|
||||
centerVertexes: bottomMiddle.vertexes,
|
||||
style
|
||||
});
|
||||
drawControllerLine(ctx, {
|
||||
start: bottomLeft.center,
|
||||
end: topLeft.center,
|
||||
centerVertexes: leftMiddle.vertexes,
|
||||
style
|
||||
});
|
||||
|
||||
drawControllerBox(ctx, topLeft.vertexes, style);
|
||||
drawControllerBox(ctx, topRight.vertexes, style);
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import type { BoardMiddleware, CoreEventMap } from '@idraw/types';
|
||||
import type { Middleware, CoreEventMap } from '@idraw/types';
|
||||
import type { DeepPointerSharedStorage } from './types';
|
||||
import { keySelectedElementList } from '../selector';
|
||||
import { coreEventKeys } from '../../config';
|
||||
|
||||
export const MiddlewarePointer: BoardMiddleware<DeepPointerSharedStorage, CoreEventMap> = (opts) => {
|
||||
export const MiddlewarePointer: Middleware<DeepPointerSharedStorage, CoreEventMap> = (opts) => {
|
||||
const { boardContent, eventHub, sharer } = opts;
|
||||
const canvas = boardContent.boardContext.canvas;
|
||||
const container = opts.container || document.body;
|
||||
|
|
@ -15,34 +15,23 @@ export const MiddlewarePointer: BoardMiddleware<DeepPointerSharedStorage, CoreEv
|
|||
return { left, top, width, height };
|
||||
};
|
||||
|
||||
const contextMenuPointer = document.createElement('div');
|
||||
contextMenuPointer.setAttribute('id', id);
|
||||
contextMenuPointer.style.position = 'fixed';
|
||||
contextMenuPointer.style.top = '0';
|
||||
contextMenuPointer.style.bottom = 'unset';
|
||||
contextMenuPointer.style.left = '0';
|
||||
contextMenuPointer.style.right = 'unset';
|
||||
|
||||
// // TODO
|
||||
// contextMenuPointer.style.width = '10px';
|
||||
// contextMenuPointer.style.height = '10px';
|
||||
// contextMenuPointer.style.background = 'red';
|
||||
|
||||
container.appendChild(contextMenuPointer);
|
||||
let contextMenuPointer = document.createElement('div');
|
||||
|
||||
return {
|
||||
name: '@middleware/pointer',
|
||||
use() {
|
||||
// TODO
|
||||
contextMenuPointer.setAttribute('id', id);
|
||||
contextMenuPointer.style.position = 'fixed';
|
||||
contextMenuPointer.style.top = '0';
|
||||
contextMenuPointer.style.bottom = 'unset';
|
||||
contextMenuPointer.style.left = '0';
|
||||
contextMenuPointer.style.right = 'unset';
|
||||
container.appendChild(contextMenuPointer);
|
||||
},
|
||||
disuse() {
|
||||
// TODO
|
||||
},
|
||||
pointStart(e) {
|
||||
// TODO
|
||||
},
|
||||
pointEnd() {
|
||||
// TODO
|
||||
container.removeChild(contextMenuPointer);
|
||||
contextMenuPointer.remove();
|
||||
contextMenuPointer = null as any;
|
||||
},
|
||||
contextMenu(e) {
|
||||
const { point } = e;
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ const scaleColor = '#000000';
|
|||
const textColor = '#00000080';
|
||||
const gridColor = '#AAAAAA20';
|
||||
const gridPrimaryColor = '#AAAAAA40';
|
||||
const selectedAreaColor = '#196097';
|
||||
const selectedAreaColor = '#19609780';
|
||||
|
||||
export const defaultStyle: MiddlewareRulerStyle = {
|
||||
background,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,22 @@
|
|||
import type { BoardMiddleware, CoreEventMap, MiddlewareRulerConfig } from '@idraw/types';
|
||||
import type { Middleware, CoreEventMap, MiddlewareRulerConfig } from '@idraw/types';
|
||||
import { getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from '@idraw/util';
|
||||
import { drawRulerBackground, drawXRuler, drawYRuler, calcXRulerScaleList, calcYRulerScaleList, drawGrid, drawScrollerSelectedArea } from './util';
|
||||
import {
|
||||
drawRulerBackground,
|
||||
drawXRuler,
|
||||
drawYRuler,
|
||||
calcXRulerScaleList,
|
||||
calcYRulerScaleList,
|
||||
drawGrid,
|
||||
drawScrollerSelectedArea
|
||||
} from './util';
|
||||
import type { DeepRulerSharedStorage } from './types';
|
||||
import { defaultStyle } from './config';
|
||||
import { coreEventKeys } from '../../config';
|
||||
|
||||
export const MiddlewareRuler: BoardMiddleware<DeepRulerSharedStorage, CoreEventMap, MiddlewareRulerConfig> = (opts, config) => {
|
||||
export const MiddlewareRuler: Middleware<DeepRulerSharedStorage, CoreEventMap, MiddlewareRulerConfig> = (
|
||||
opts,
|
||||
config
|
||||
) => {
|
||||
const { boardContent, viewer, eventHub, calculator } = opts;
|
||||
const { overlayContext, underlayContext } = boardContent;
|
||||
let innerConfig = {
|
||||
|
|
@ -45,7 +56,8 @@ export const MiddlewareRuler: BoardMiddleware<DeepRulerSharedStorage, CoreEventM
|
|||
},
|
||||
|
||||
beforeDrawFrame: ({ snapshot }) => {
|
||||
const { background, borderColor, scaleColor, textColor, gridColor, gridPrimaryColor, selectedAreaColor } = innerConfig;
|
||||
const { background, borderColor, scaleColor, textColor, gridColor, gridPrimaryColor, selectedAreaColor } =
|
||||
innerConfig;
|
||||
|
||||
const style = {
|
||||
background,
|
||||
|
|
@ -60,10 +72,10 @@ export const MiddlewareRuler: BoardMiddleware<DeepRulerSharedStorage, CoreEventM
|
|||
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
|
||||
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
|
||||
|
||||
drawScrollerSelectedArea(overlayContext, { snapshot, calculator, style });
|
||||
|
||||
drawRulerBackground(overlayContext, { viewScaleInfo, viewSizeInfo, style });
|
||||
|
||||
drawScrollerSelectedArea(overlayContext, { snapshot, calculator, style });
|
||||
|
||||
const { list: xList, rulerUnit } = calcXRulerScaleList({ viewScaleInfo, viewSizeInfo });
|
||||
drawXRuler(overlayContext, { scaleList: xList, style });
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ function limitRulerUnit(unit: number): number {
|
|||
return unit;
|
||||
}
|
||||
|
||||
function calcRulerScaleList(opts: { axis: 'X' | 'Y'; scale: number; viewLength: number; viewOffset: number }): { list: RulerScale[]; rulerUnit: number } {
|
||||
function calcRulerScaleList(opts: { axis: 'X' | 'Y'; scale: number; viewLength: number; viewOffset: number }): {
|
||||
list: RulerScale[];
|
||||
rulerUnit: number;
|
||||
} {
|
||||
const { scale, viewLength, viewOffset } = opts;
|
||||
const list: RulerScale[] = [];
|
||||
let rulerUnit = 10;
|
||||
|
|
@ -87,7 +90,10 @@ function calcRulerScaleList(opts: { axis: 'X' | 'Y'; scale: number; viewLength:
|
|||
return { list, rulerUnit };
|
||||
}
|
||||
|
||||
export function calcXRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): { list: RulerScale[]; rulerUnit: number } {
|
||||
export function calcXRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): {
|
||||
list: RulerScale[];
|
||||
rulerUnit: number;
|
||||
} {
|
||||
const { viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { scale, offsetLeft } = viewScaleInfo;
|
||||
const { width } = viewSizeInfo;
|
||||
|
|
@ -99,7 +105,10 @@ export function calcXRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSi
|
|||
});
|
||||
}
|
||||
|
||||
export function calcYRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): { list: RulerScale[]; rulerUnit: number } {
|
||||
export function calcYRulerScaleList(opts: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): {
|
||||
list: RulerScale[];
|
||||
rulerUnit: number;
|
||||
} {
|
||||
const { viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { scale, offsetTop } = viewScaleInfo;
|
||||
const { height } = viewSizeInfo;
|
||||
|
|
@ -222,7 +231,7 @@ export function drawRulerBackground(
|
|||
ctx.closePath();
|
||||
|
||||
ctx.fillStyle = background;
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
ctx.lineWidth = lineSize;
|
||||
ctx.setLineDash([]);
|
||||
ctx.strokeStyle = borderColor;
|
||||
|
|
@ -277,7 +286,11 @@ export function drawGrid(
|
|||
|
||||
export function drawScrollerSelectedArea(
|
||||
ctx: ViewContext2D,
|
||||
opts: { snapshot: BoardViewerFrameSnapshot<DeepRulerSharedStorage>; calculator: ViewCalculator; style: MiddlewareRulerStyle }
|
||||
opts: {
|
||||
snapshot: BoardViewerFrameSnapshot<DeepRulerSharedStorage>;
|
||||
calculator: ViewCalculator;
|
||||
style: MiddlewareRulerStyle;
|
||||
}
|
||||
) {
|
||||
const { snapshot, calculator, style } = opts;
|
||||
const { sharedStore } = snapshot;
|
||||
|
|
@ -285,7 +298,10 @@ export function drawScrollerSelectedArea(
|
|||
const selectedElementList = sharedStore[keySelectedElementList];
|
||||
const actionType = sharedStore[keyActionType];
|
||||
|
||||
if (['select', 'drag', 'drag-list', 'drag-list-end'].includes(actionType as string) && selectedElementList.length > 0) {
|
||||
if (
|
||||
['select', 'drag', 'drag-list', 'drag-list-end'].includes(actionType as string) &&
|
||||
selectedElementList.length > 0
|
||||
) {
|
||||
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
|
||||
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
|
||||
const rangeRectInfoList: ViewRectInfo[] = [];
|
||||
|
|
@ -325,7 +341,7 @@ export function drawScrollerSelectedArea(
|
|||
ctx.lineTo(xAreaStart, rulerSize);
|
||||
ctx.fillStyle = selectedAreaColor;
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, yAreaStart);
|
||||
|
|
@ -334,6 +350,6 @@ export function drawScrollerSelectedArea(
|
|||
ctx.lineTo(0, yAreaEnd);
|
||||
ctx.fillStyle = selectedAreaColor;
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import type { BoardMiddleware, CoreEventMap } from '@idraw/types';
|
||||
import type { Middleware, CoreEventMap } from '@idraw/types';
|
||||
import { formatNumber } from '@idraw/util';
|
||||
import { coreEventKeys } from '../../config';
|
||||
|
||||
export const MiddlewareScaler: BoardMiddleware<Record<string, any>, CoreEventMap> = (opts) => {
|
||||
export const MiddlewareScaler: Middleware<Record<string, any>, CoreEventMap> = (opts) => {
|
||||
const { viewer, sharer, eventHub } = opts;
|
||||
const maxScale = 50;
|
||||
const minScale = 0.05;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,29 @@
|
|||
import type { Point, BoardMiddleware, PointWatcherEvent, BoardWatherWheelEvent, MiddlewareScrollerConfig } from '@idraw/types';
|
||||
import type {
|
||||
Point,
|
||||
Middleware,
|
||||
PointWatcherEvent,
|
||||
BoardWatherWheelEvent,
|
||||
MiddlewareScrollerConfig
|
||||
} from '@idraw/types';
|
||||
import { drawScroller, isPointInScrollThumb } from './util';
|
||||
// import type { ScrollbarThumbType } from './util';
|
||||
import { keyXThumbRect, keyYThumbRect, keyPrevPoint, keyActivePoint, keyActiveThumbType, keyHoverXThumbRect, keyHoverYThumbRect, defaultStyle } from './config';
|
||||
import {
|
||||
keyXThumbRect,
|
||||
keyYThumbRect,
|
||||
keyPrevPoint,
|
||||
keyActivePoint,
|
||||
keyActiveThumbType,
|
||||
keyHoverXThumbRect,
|
||||
keyHoverYThumbRect,
|
||||
defaultStyle
|
||||
} from './config';
|
||||
import type { DeepScrollerSharedStorage } from './types';
|
||||
import { coreEventKeys } from '../../config';
|
||||
|
||||
export const MiddlewareScroller: BoardMiddleware<DeepScrollerSharedStorage, any, MiddlewareScrollerConfig> = (opts, config) => {
|
||||
export const MiddlewareScroller: Middleware<DeepScrollerSharedStorage, any, MiddlewareScrollerConfig> = (
|
||||
opts,
|
||||
config
|
||||
) => {
|
||||
const { viewer, boardContent, sharer, eventHub } = opts;
|
||||
const { overlayContext } = boardContent;
|
||||
sharer.setSharedStorage(keyXThumbRect, null); // null | ElementSize
|
||||
|
|
@ -136,7 +154,14 @@ export const MiddlewareScroller: BoardMiddleware<DeepScrollerSharedStorage, any,
|
|||
}
|
||||
},
|
||||
beforeDrawFrame({ snapshot }) {
|
||||
const { thumbBackground, thumbBorderColor, hoverThumbBackground, hoverThumbBorderColor, activeThumbBackground, activeThumbBorderColor } = innerConfig;
|
||||
const {
|
||||
thumbBackground,
|
||||
thumbBorderColor,
|
||||
hoverThumbBackground,
|
||||
hoverThumbBorderColor,
|
||||
activeThumbBackground,
|
||||
activeThumbBorderColor
|
||||
} = innerConfig;
|
||||
|
||||
const style = {
|
||||
thumbBackground,
|
||||
|
|
|
|||
|
|
@ -201,7 +201,7 @@ function drawScrollerThumb(
|
|||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = background;
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = 1;
|
||||
|
|
|
|||
|
|
@ -19,10 +19,15 @@ export function drawVertexes(
|
|||
ctx.lineTo(vertexes[0].x, vertexes[0].y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
}
|
||||
|
||||
export function drawLine(ctx: ViewContext2D, start: PointSize, end: PointSize, opts: { borderColor: string; borderWidth: number; lineDash: number[] }) {
|
||||
export function drawLine(
|
||||
ctx: ViewContext2D,
|
||||
start: PointSize,
|
||||
end: PointSize,
|
||||
opts: { borderColor: string; borderWidth: number; lineDash: number[] }
|
||||
) {
|
||||
const { borderColor, borderWidth, lineDash } = opts;
|
||||
ctx.setLineDash([]);
|
||||
ctx.lineWidth = borderWidth;
|
||||
|
|
@ -66,7 +71,7 @@ export function drawCircleController(
|
|||
ctx.fillStyle = background;
|
||||
ctx.circle(center.x, center.y, a, b, 0, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
}
|
||||
|
||||
// ctx.setLineDash([]);
|
||||
|
|
@ -82,10 +87,14 @@ export function drawCircleController(
|
|||
// ctx.lineTo(vertexes[0].x, vertexes[0].y);
|
||||
// ctx.closePath();
|
||||
// ctx.stroke();
|
||||
// ctx.fill();
|
||||
// ctx.fill('nonzero');
|
||||
}
|
||||
|
||||
export function drawCrossVertexes(ctx: ViewContext2D, vertexes: ViewRectVertexes, opts: { borderColor: string; borderWidth: number; lineDash: number[] }) {
|
||||
export function drawCrossVertexes(
|
||||
ctx: ViewContext2D,
|
||||
vertexes: ViewRectVertexes,
|
||||
opts: { borderColor: string; borderWidth: number; lineDash: number[] }
|
||||
) {
|
||||
const { borderColor, borderWidth, lineDash } = opts;
|
||||
ctx.setLineDash([]);
|
||||
ctx.lineWidth = borderWidth;
|
||||
|
|
@ -104,7 +113,11 @@ export function drawCrossVertexes(ctx: ViewContext2D, vertexes: ViewRectVertexes
|
|||
ctx.stroke();
|
||||
}
|
||||
|
||||
export function drawCrossByCenter(ctx: ViewContext2D, center: PointSize, opts: { size: number; borderColor: string; borderWidth: number; lineDash: number[] }) {
|
||||
export function drawCrossByCenter(
|
||||
ctx: ViewContext2D,
|
||||
center: PointSize,
|
||||
opts: { size: number; borderColor: string; borderWidth: number; lineDash: number[] }
|
||||
) {
|
||||
const { size, borderColor, borderWidth, lineDash } = opts;
|
||||
const minX = center.x - size / 2;
|
||||
const maxX = center.x + size / 2;
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ function drawDebugControllerVertexes(opts: {
|
|||
ctx.lineTo(v3.x, v3.y);
|
||||
ctx.lineTo(v0.x, v0.y);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
return false;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,7 +99,12 @@ export function drawSelectedElementControllersVertexes(
|
|||
const { devicePixelRatio = 1 } = viewSizeInfo;
|
||||
const { activeColor } = style;
|
||||
const { elementWrapper, topLeft, topRight, bottomLeft, bottomRight, rotate } = controller;
|
||||
const wrapperOpts = { borderColor: activeColor, borderWidth: selectWrapperBorderWidth, background: 'transparent', lineDash: [] };
|
||||
const wrapperOpts = {
|
||||
borderColor: activeColor,
|
||||
borderWidth: selectWrapperBorderWidth,
|
||||
background: 'transparent',
|
||||
lineDash: []
|
||||
};
|
||||
const ctrlOpts = { ...wrapperOpts, borderWidth: resizeControllerBorderWidth, background: '#FFFFFF' };
|
||||
|
||||
drawVertexes(ctx, calcViewVertexes(elementWrapper, opts), wrapperOpts);
|
||||
|
|
@ -115,7 +120,11 @@ export function drawSelectedElementControllersVertexes(
|
|||
drawVertexes(ctx, calcViewVertexes(bottomRight.vertexes, opts), ctrlOpts);
|
||||
|
||||
if (element?.operations?.rotatable !== false) {
|
||||
drawCircleController(ctx, calcViewPointSize(rotate.center, opts), { ...ctrlOpts, size: rotate.size, borderWidth: 0 });
|
||||
drawCircleController(ctx, calcViewPointSize(rotate.center, opts), {
|
||||
...ctrlOpts,
|
||||
size: rotate.size,
|
||||
borderWidth: 0
|
||||
});
|
||||
const rotateCenter = calcViewPointSize(rotate.center, opts);
|
||||
ctx.drawImage(
|
||||
rotateControllerPattern.canvas,
|
||||
|
|
@ -139,7 +148,11 @@ export function drawSelectedElementControllersVertexes(
|
|||
// });
|
||||
}
|
||||
|
||||
export function drawElementListShadows(ctx: ViewContext2D, elements: Element<ElementType>[], opts?: Omit<RendererDrawElementOptions, 'loader'>) {
|
||||
export function drawElementListShadows(
|
||||
ctx: ViewContext2D,
|
||||
elements: Element<ElementType>[],
|
||||
opts?: Omit<RendererDrawElementOptions, 'loader'>
|
||||
) {
|
||||
elements.forEach((elem) => {
|
||||
let { x, y, w, h } = elem;
|
||||
const { angle = 0 } = elem;
|
||||
|
|
@ -164,12 +177,15 @@ export function drawElementListShadows(ctx: ViewContext2D, elements: Element<Ele
|
|||
}
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function drawArea(ctx: ViewContext2D, opts: { start: PointSize; end: PointSize; style: MiddlewareSelectorStyle }) {
|
||||
export function drawArea(
|
||||
ctx: ViewContext2D,
|
||||
opts: { start: PointSize; end: PointSize; style: MiddlewareSelectorStyle }
|
||||
) {
|
||||
const { start, end, style } = opts;
|
||||
const { activeColor, activeAreaColor } = style;
|
||||
ctx.setLineDash([]);
|
||||
|
|
@ -183,7 +199,7 @@ export function drawArea(ctx: ViewContext2D, opts: { start: PointSize; end: Poin
|
|||
ctx.lineTo(start.x, end.y);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
}
|
||||
|
||||
export function drawListArea(ctx: ViewContext2D, opts: { areaSize: AreaSize; style: MiddlewareSelectorStyle }) {
|
||||
|
|
@ -201,7 +217,7 @@ export function drawListArea(ctx: ViewContext2D, opts: { areaSize: AreaSize; sty
|
|||
ctx.lineTo(x, y + h);
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
}
|
||||
|
||||
export function drawGroupQueueVertexesWrappers(
|
||||
|
|
@ -217,7 +233,12 @@ export function drawGroupQueueVertexesWrappers(
|
|||
const { activeColor } = style;
|
||||
for (let i = 0; i < vertexesList.length; i++) {
|
||||
const vertexes = vertexesList[i];
|
||||
const wrapperOpts = { borderColor: activeColor, borderWidth: selectWrapperBorderWidth, background: 'transparent', lineDash: [4, 4] };
|
||||
const wrapperOpts = {
|
||||
borderColor: activeColor,
|
||||
borderWidth: selectWrapperBorderWidth,
|
||||
background: 'transparent',
|
||||
lineDash: [4, 4]
|
||||
};
|
||||
drawVertexes(ctx, calcViewVertexes(vertexes, opts), wrapperOpts);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ import {
|
|||
deepResizeGroupElement,
|
||||
getElementSize,
|
||||
calcPointMoveElementInGroup,
|
||||
isSameElementSize
|
||||
isSameElementSize,
|
||||
toFlattenElement
|
||||
} from '@idraw/util';
|
||||
import type {
|
||||
Data,
|
||||
|
|
@ -24,13 +25,14 @@ import type {
|
|||
ViewSizeInfo,
|
||||
ElementSizeController,
|
||||
MiddlewareSelectorConfig,
|
||||
ElementSize
|
||||
ElementSize,
|
||||
ModifyRecord
|
||||
} from '@idraw/types';
|
||||
import type {
|
||||
Point,
|
||||
PointSize,
|
||||
PointWatcherEvent,
|
||||
BoardMiddleware,
|
||||
Middleware,
|
||||
Element,
|
||||
ActionType,
|
||||
ResizeType,
|
||||
|
|
@ -93,7 +95,7 @@ import { MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE } from '../info';
|
|||
export { keySelectedElementList, keyHoverElement, keyActionType, keyResizeType, keyGroupQueue };
|
||||
export type { DeepSelectorSharedStorage, ActionType };
|
||||
|
||||
export const MiddlewareSelector: BoardMiddleware<
|
||||
export const MiddlewareSelector: Middleware<
|
||||
DeepSelectorSharedStorage,
|
||||
CoreEventMap & {
|
||||
[MIDDLEWARE_INTERNAL_EVENT_SHOW_INFO_ANGLE]: { show: boolean };
|
||||
|
|
@ -107,6 +109,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
const { viewer, sharer, boardContent, calculator, eventHub } = opts;
|
||||
const { overlayContext } = boardContent;
|
||||
let prevPoint: Point | null = null;
|
||||
let pointStartElementSizeList: Array<Partial<ElementSize> & { uuid: string }> = [];
|
||||
let moveOriginalStartPoint: Point | null = null;
|
||||
let moveOriginalStartElementSize: ElementSize | null = null;
|
||||
let inBusyMode: 'resize' | 'drag' | 'drag-list' | 'area' | null = null;
|
||||
|
|
@ -283,6 +286,8 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
eventHub.off(coreEventKeys.CLEAR_SELECT, selectClearCallback);
|
||||
eventHub.off(coreEventKeys.SELECT_IN_GROUP, selectInGroupCallback);
|
||||
eventHub.off(coreEventKeys.SNAP_TO_GRID, setSnapToSnapCallback);
|
||||
clear();
|
||||
innerConfig = null as any;
|
||||
},
|
||||
|
||||
resetConfig(config) {
|
||||
|
|
@ -428,11 +433,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
const target = getPointTarget(e.point, pointTargetBaseOptions());
|
||||
const isLockedElement = target?.elements?.length === 1 && target.elements[0]?.operations?.locked === true;
|
||||
|
||||
// if (target?.elements?.length === 1 && target.elements[0]?.operations?.locked === true) {
|
||||
// return;
|
||||
// } else {
|
||||
updateHoverElement(null);
|
||||
// }
|
||||
|
||||
if (target?.elements?.length === 1) {
|
||||
moveOriginalStartElementSize = getElementSize(target?.elements[0]);
|
||||
|
|
@ -442,6 +443,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
} else if (target.type === 'over-element' && target?.elements?.length === 1) {
|
||||
updateSelectedElementList([target.elements[0]], { triggerEvent: true });
|
||||
sharer.setSharedStorage(keyActionType, 'drag');
|
||||
pointStartElementSizeList = [{ ...getElementSize(target?.elements[0]), uuid: target?.elements[0].uuid }];
|
||||
} else if (target.type?.startsWith('resize-')) {
|
||||
sharer.setSharedStorage(keyResizeType, target.type as ResizeType);
|
||||
sharer.setSharedStorage(keyActionType, 'resize');
|
||||
|
|
@ -487,6 +489,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
} else if (target.type === 'over-element' && target?.elements?.length === 1) {
|
||||
updateSelectedElementList([target.elements[0]], { triggerEvent: true });
|
||||
sharer.setSharedStorage(keyActionType, 'drag');
|
||||
pointStartElementSizeList = [{ ...getElementSize(target?.elements[0]), uuid: target?.elements[0].uuid }];
|
||||
} else if (target.type?.startsWith('resize-')) {
|
||||
sharer.setSharedStorage(keyResizeType, target.type as ResizeType);
|
||||
sharer.setSharedStorage(keyActionType, 'resize');
|
||||
|
|
@ -562,7 +565,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
elems[0].y = calculator.toGridNum(moveOriginalStartElementSize.y + totalMoveY);
|
||||
updateSelectedElementList([elems[0]]);
|
||||
calculator.modifyVirtualFlatItemMap(data, {
|
||||
modifyOptions: {
|
||||
modifyInfo: {
|
||||
type: 'updateElement',
|
||||
content: {
|
||||
element: elems[0],
|
||||
|
|
@ -586,7 +589,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
elem.y = calculator.toGridNum(elem.y + moveY);
|
||||
|
||||
calculator.modifyVirtualFlatItemMap(data, {
|
||||
modifyOptions: {
|
||||
modifyInfo: {
|
||||
type: 'updateElement',
|
||||
content: {
|
||||
element: elem,
|
||||
|
|
@ -675,7 +678,7 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
|
||||
updateSelectedElementList([elems[0]]);
|
||||
calculator.modifyVirtualFlatItemMap(data, {
|
||||
modifyOptions: {
|
||||
modifyInfo: {
|
||||
type: 'updateElement',
|
||||
content: {
|
||||
element: elems[0],
|
||||
|
|
@ -775,7 +778,37 @@ export const MiddlewareSelector: BoardMiddleware<
|
|||
type = 'resizeElement';
|
||||
}
|
||||
if (hasChangedData) {
|
||||
eventHub.trigger(coreEventKeys.CHANGE, { data, type, selectedElements, hoverElement });
|
||||
const startSize = pointStartElementSizeList[0] as ElementSize & { uuid: string };
|
||||
let modifyRecord: ModifyRecord | undefined = undefined;
|
||||
if (selectedElements.length === 1) {
|
||||
modifyRecord = {
|
||||
type: 'dragElement',
|
||||
time: 0,
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: startSize.uuid,
|
||||
before: toFlattenElement(startSize),
|
||||
after: toFlattenElement(getElementSize(selectedElements[0]))
|
||||
}
|
||||
};
|
||||
} else if (selectedElements.length > 1) {
|
||||
modifyRecord = {
|
||||
type: 'dragElements',
|
||||
time: 0,
|
||||
content: {
|
||||
method: 'modifyElements',
|
||||
before: pointStartElementSizeList.map((item) => ({
|
||||
...toFlattenElement(item),
|
||||
uuid: item.uuid
|
||||
})),
|
||||
after: selectedElements.map((item) => ({
|
||||
...toFlattenElement(getElementSize(item)),
|
||||
uuid: item.uuid
|
||||
}))
|
||||
}
|
||||
};
|
||||
}
|
||||
eventHub.trigger(coreEventKeys.CHANGE, { data, type, selectedElements, hoverElement, modifyRecord });
|
||||
hasChangedData = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import {
|
|||
ViewSizeInfo,
|
||||
ViewCalculator,
|
||||
PointWatcherEvent,
|
||||
BoardMiddleware,
|
||||
Middleware,
|
||||
ViewRectVertexes,
|
||||
ElementSizeController,
|
||||
ElementPosition
|
||||
|
|
@ -54,7 +54,7 @@ export {
|
|||
ViewSizeInfo,
|
||||
ViewCalculator,
|
||||
PointWatcherEvent,
|
||||
BoardMiddleware
|
||||
Middleware
|
||||
};
|
||||
|
||||
export type ControllerStyle = ElementSize & {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { BoardMiddleware, CoreEventMap, Element, ElementSize, ViewScaleInfo, ElementPosition } from '@idraw/types';
|
||||
import { limitAngle, getDefaultElementDetailConfig, enhanceFontFamliy } from '@idraw/util';
|
||||
import type { Middleware, CoreEventMap, Element, ElementSize, ViewScaleInfo, ElementPosition } from '@idraw/types';
|
||||
import { limitAngle, getDefaultElementDetailConfig, enhanceFontFamliy, updateElementInList } from '@idraw/util';
|
||||
import { coreEventKeys } from '../../config';
|
||||
|
||||
type TextEditEvent = {
|
||||
|
|
@ -19,21 +19,23 @@ type TextChangeEvent = {
|
|||
position: ElementPosition;
|
||||
};
|
||||
|
||||
type ExtendEventMap = Record<typeof coreEventKeys.TEXT_EDIT, TextEditEvent> & Record<typeof coreEventKeys.TEXT_CHANGE, TextChangeEvent>;
|
||||
type ExtendEventMap = Record<typeof coreEventKeys.TEXT_EDIT, TextEditEvent> &
|
||||
Record<typeof coreEventKeys.TEXT_CHANGE, TextChangeEvent>;
|
||||
|
||||
const defaultElementDetail = getDefaultElementDetailConfig();
|
||||
|
||||
export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap & ExtendEventMap> = (opts) => {
|
||||
const { eventHub, boardContent, viewer, sharer } = opts;
|
||||
export const MiddlewareTextEditor: Middleware<ExtendEventMap, CoreEventMap & ExtendEventMap> = (opts) => {
|
||||
const { eventHub, boardContent, viewer, sharer, calculator } = opts;
|
||||
const canvas = boardContent.boardContext.canvas;
|
||||
// const textarea = document.createElement('textarea');
|
||||
const textarea = document.createElement('div');
|
||||
textarea.setAttribute('contenteditable', 'true');
|
||||
const canvasWrapper = document.createElement('div');
|
||||
const container = opts.container || document.body;
|
||||
const mask = document.createElement('div');
|
||||
let textarea = document.createElement('div');
|
||||
textarea.setAttribute('contenteditable', 'true');
|
||||
let canvasWrapper = document.createElement('div');
|
||||
let mask = document.createElement('div');
|
||||
let activeElem: Element<'text'> | null = null;
|
||||
let activePosition: ElementPosition = [];
|
||||
let originText: string = '';
|
||||
|
||||
const id = `idraw-middleware-text-editor-${Math.random().toString(26).substring(2)}`;
|
||||
mask.setAttribute('id', id);
|
||||
canvasWrapper.appendChild(textarea);
|
||||
|
|
@ -53,12 +55,14 @@ export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap
|
|||
resetCanvasWrapper();
|
||||
resetTextArea(e);
|
||||
mask.style.display = 'block';
|
||||
originText = '';
|
||||
if (activeElem?.uuid) {
|
||||
sharer.setActiveOverrideElemenentMap({
|
||||
[activeElem.uuid]: {
|
||||
operations: { invisible: true }
|
||||
}
|
||||
});
|
||||
originText = activeElem.detail.text || '';
|
||||
viewer.drawFrame();
|
||||
}
|
||||
};
|
||||
|
|
@ -201,13 +205,15 @@ export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap
|
|||
// canvasWrapper.style.background = '#000000';
|
||||
};
|
||||
|
||||
mask.addEventListener('click', () => {
|
||||
const maskClickEvent = () => {
|
||||
hideTextArea();
|
||||
});
|
||||
textarea.addEventListener('click', (e) => {
|
||||
};
|
||||
|
||||
const textareaClickEvent = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
textarea.addEventListener('input', () => {
|
||||
};
|
||||
|
||||
const textareaInputEvent = () => {
|
||||
if (activeElem && activePosition) {
|
||||
// activeElem.detail.text = (e.target as any).value || '';
|
||||
activeElem.detail.text = textarea.innerText || '';
|
||||
|
|
@ -220,11 +226,14 @@ export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap
|
|||
},
|
||||
position: [...(activePosition || [])]
|
||||
});
|
||||
calculator.modifyText(activeElem);
|
||||
viewer.drawFrame();
|
||||
}
|
||||
});
|
||||
textarea.addEventListener('blur', () => {
|
||||
};
|
||||
|
||||
const textareaBlurEvent = () => {
|
||||
if (activeElem && activePosition) {
|
||||
activeElem.detail.text = textarea.innerText || '';
|
||||
eventHub.trigger(coreEventKeys.TEXT_CHANGE, {
|
||||
element: {
|
||||
uuid: activeElem.uuid,
|
||||
|
|
@ -234,23 +243,75 @@ export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap
|
|||
},
|
||||
position: [...activePosition]
|
||||
});
|
||||
|
||||
const data = sharer.getActiveStorage('data') || { elements: [] };
|
||||
const updateContent = {
|
||||
detail: {
|
||||
text: activeElem.detail.text
|
||||
}
|
||||
};
|
||||
updateElementInList(activeElem.uuid, updateContent, data.elements);
|
||||
|
||||
eventHub.trigger(coreEventKeys.CHANGE, {
|
||||
selectedElements: [
|
||||
{
|
||||
...activeElem,
|
||||
detail: {
|
||||
...activeElem.detail,
|
||||
...updateContent.detail
|
||||
}
|
||||
}
|
||||
],
|
||||
data,
|
||||
type: 'modifyElement',
|
||||
modifyRecord: {
|
||||
type: 'modifyElement',
|
||||
time: Date.now(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: activeElem.uuid as string,
|
||||
before: {
|
||||
'detail.text': originText
|
||||
},
|
||||
after: {
|
||||
'detail.text': activeElem.detail.text
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
calculator.modifyText(activeElem);
|
||||
viewer.drawFrame();
|
||||
}
|
||||
|
||||
hideTextArea();
|
||||
});
|
||||
textarea.addEventListener('keydown', (e) => {
|
||||
};
|
||||
|
||||
const textareaKeyDownEvent = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
textarea.addEventListener('keypress', (e) => {
|
||||
};
|
||||
|
||||
const textareaKeyPressEvent = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
textarea.addEventListener('keyup', (e) => {
|
||||
};
|
||||
|
||||
const textareaKeyUpEvent = (e: KeyboardEvent) => {
|
||||
e.stopPropagation();
|
||||
});
|
||||
textarea.addEventListener('wheel', (e) => {
|
||||
};
|
||||
|
||||
const textareaWheelEvent = (e: WheelEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
});
|
||||
};
|
||||
|
||||
mask.addEventListener('click', maskClickEvent);
|
||||
textarea.addEventListener('click', textareaClickEvent);
|
||||
textarea.addEventListener('input', textareaInputEvent);
|
||||
textarea.addEventListener('blur', textareaBlurEvent);
|
||||
textarea.addEventListener('keydown', textareaKeyDownEvent);
|
||||
textarea.addEventListener('keypress', textareaKeyPressEvent);
|
||||
textarea.addEventListener('keyup', textareaKeyUpEvent);
|
||||
textarea.addEventListener('wheel', textareaWheelEvent);
|
||||
|
||||
const textEditCallback = (e: TextEditEvent) => {
|
||||
if (e?.position && e?.element && e?.element?.type === 'text') {
|
||||
|
|
@ -267,6 +328,27 @@ export const MiddlewareTextEditor: BoardMiddleware<ExtendEventMap, CoreEventMap
|
|||
},
|
||||
disuse() {
|
||||
eventHub.off(coreEventKeys.TEXT_EDIT, textEditCallback);
|
||||
mask.removeEventListener('click', maskClickEvent);
|
||||
textarea.removeEventListener('click', textareaClickEvent);
|
||||
textarea.removeEventListener('input', textareaInputEvent);
|
||||
textarea.removeEventListener('blur', textareaBlurEvent);
|
||||
textarea.removeEventListener('keydown', textareaKeyDownEvent);
|
||||
textarea.removeEventListener('keypress', textareaKeyPressEvent);
|
||||
textarea.removeEventListener('keyup', textareaKeyUpEvent);
|
||||
textarea.removeEventListener('wheel', textareaWheelEvent);
|
||||
canvasWrapper.removeChild(textarea);
|
||||
mask.removeChild(canvasWrapper);
|
||||
container.removeChild(mask);
|
||||
|
||||
textarea.remove();
|
||||
canvasWrapper.remove();
|
||||
mask = null as any;
|
||||
textarea = null as any;
|
||||
canvasWrapper = null as any;
|
||||
mask = null as any;
|
||||
activeElem = null;
|
||||
activePosition = null as any;
|
||||
originText = null as any;
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
|||
34
packages/core/src/record.ts
Normal file
34
packages/core/src/record.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { RecursivePartial, FlattenElement, Element, ModifyRecord } from '@idraw/types';
|
||||
import { toFlattenElement, get } from '@idraw/util';
|
||||
|
||||
export function getModifyElementRecord(opts: {
|
||||
modifiedElement: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>;
|
||||
beforeElement: Element;
|
||||
}): ModifyRecord<'modifyElement'> {
|
||||
const { modifiedElement, beforeElement } = opts;
|
||||
const { uuid, ...restElement } = modifiedElement;
|
||||
const after = toFlattenElement(restElement);
|
||||
let before: FlattenElement = {};
|
||||
Object.keys(after).forEach((key: string) => {
|
||||
let val = get(beforeElement, key);
|
||||
if (val === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(key)) {
|
||||
key = key.replace(/\[[0-9]{1,}\]$/, '');
|
||||
val = get(beforeElement, key);
|
||||
}
|
||||
before[key] = val;
|
||||
});
|
||||
before = toFlattenElement(before);
|
||||
|
||||
const record: ModifyRecord<'modifyElement'> = {
|
||||
type: 'modifyElement',
|
||||
time: Date.now(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid,
|
||||
before,
|
||||
after
|
||||
}
|
||||
};
|
||||
|
||||
return record;
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
171
packages/idraw/__tests__/history-addElement.test.ts
Normal file
171
packages/idraw/__tests__/history-addElement.test.ts
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, findElementFromListByPosition } from 'idraw';
|
||||
|
||||
const createData = () => ({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-001',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('group', {
|
||||
uuid: 'test-005',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('circle', { uuid: 'test-007' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-008',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('updateElement', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
|
||||
// modify 1: do
|
||||
const newElement1 = idraw.createElement('rect', {
|
||||
x: 22,
|
||||
y: 33,
|
||||
h: 300,
|
||||
w: 400,
|
||||
name: 'new element 001',
|
||||
detail: {
|
||||
background: '#666666'
|
||||
}
|
||||
});
|
||||
const position = [1, 2];
|
||||
idraw.addElement(newElement1, {
|
||||
position
|
||||
});
|
||||
const record1 = {
|
||||
type: 'addElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'addElement',
|
||||
uuid: newElement1.uuid,
|
||||
position: [...position],
|
||||
element: deepClone(newElement1)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const newElement2 = idraw.createElement('text', {
|
||||
x: 22,
|
||||
y: 33,
|
||||
h: 300,
|
||||
w: 400,
|
||||
name: 'new element 002',
|
||||
detail: {
|
||||
text: 'Hello Element'
|
||||
}
|
||||
});
|
||||
idraw.addElement(newElement2, { position });
|
||||
const record2 = {
|
||||
type: 'addElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'addElement',
|
||||
uuid: newElement2.uuid,
|
||||
position: [...position],
|
||||
element: deepClone(newElement2)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'deleteElement',
|
||||
uuid: record2.content.uuid,
|
||||
position: deepClone(record2.content.position),
|
||||
element: deepClone(record2.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'deleteElement',
|
||||
uuid: record1.content.uuid,
|
||||
position: deepClone(record1.content.position),
|
||||
element: deepClone(record1.content.element)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(createData());
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'addElement',
|
||||
uuid: record4.content.uuid,
|
||||
position: record4.content.position,
|
||||
element: deepClone(record4.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'addElement',
|
||||
uuid: record3.content.uuid,
|
||||
position: record3.content.position,
|
||||
element: deepClone(record3.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(newElement2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
158
packages/idraw/__tests__/history-deleteElement.test.ts
Normal file
158
packages/idraw/__tests__/history-deleteElement.test.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, findElementFromListByPosition } from 'idraw';
|
||||
import type { Element } from 'idraw';
|
||||
|
||||
const createData = () => ({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-000',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('group', {
|
||||
uuid: 'test-001',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('image', { uuid: 'test-001-000', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('circle', { uuid: 'test-001-001' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-001-002',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-001-003', detail: { src: 'https://example.com/002.png' } }),
|
||||
createElement('rect', { uuid: 'test-001-004' }),
|
||||
createElement('circle', { uuid: 'test-001-005' })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('updateElement', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
|
||||
const position = [1, 2];
|
||||
const nextPosition = [1, 3];
|
||||
|
||||
// modify 1: do
|
||||
const deletedElem1 = deepClone(findElementFromListByPosition(position, data.elements) as Element);
|
||||
const expectedElem1 = deepClone(findElementFromListByPosition(nextPosition, data.elements) as Element);
|
||||
idraw.deleteElement(deletedElem1?.uuid);
|
||||
const record1 = {
|
||||
type: 'deleteElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'deleteElement',
|
||||
uuid: deletedElem1.uuid,
|
||||
position: [...position],
|
||||
element: deepClone(deletedElem1)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const deletedElem2 = deepClone(findElementFromListByPosition(position, data.elements) as Element);
|
||||
const expectedElem2 = deepClone(findElementFromListByPosition(nextPosition, data.elements) as Element);
|
||||
idraw.deleteElement(deletedElem2?.uuid);
|
||||
const record2 = {
|
||||
type: 'deleteElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'deleteElement',
|
||||
uuid: deletedElem2.uuid,
|
||||
position: [...position],
|
||||
element: deepClone(deletedElem2)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'addElement',
|
||||
uuid: record2.content.uuid,
|
||||
position: deepClone(record2.content.position),
|
||||
element: deepClone(record2.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(deletedElem2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'addElement',
|
||||
uuid: record1.content.uuid,
|
||||
position: deepClone(record1.content.position),
|
||||
element: deepClone(record1.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(deletedElem1);
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'deleteElement',
|
||||
uuid: record4.content.uuid,
|
||||
position: record4.content.position,
|
||||
element: deepClone(record4.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'deleteElement',
|
||||
uuid: record3.content.uuid,
|
||||
position: record3.content.position,
|
||||
element: deepClone(record3.content.element)
|
||||
}
|
||||
};
|
||||
expect(findElementFromListByPosition(position, idraw.getData()?.elements || [])).toStrictEqual(expectedElem2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
204
packages/idraw/__tests__/history-modifyElement.test.ts
Normal file
204
packages/idraw/__tests__/history-modifyElement.test.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenElement } from 'idraw';
|
||||
import type { RecursivePartial, Element } from 'idraw';
|
||||
|
||||
const createData = () => ({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-001',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('circle', { uuid: 'test-002' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-003',
|
||||
detail: {
|
||||
text: 'Hello World'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('group', {
|
||||
uuid: 'test-005',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('rect', { uuid: 'test-006' }),
|
||||
createElement('circle', { uuid: 'test-007' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-008',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('modifyElement', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
const targetElement = deepClone(data.elements[0]);
|
||||
|
||||
// modify 1: do
|
||||
const modifiedInfo1 = {
|
||||
x: targetElement.x + 1,
|
||||
y: targetElement.y + 2,
|
||||
detail: {
|
||||
background: '#123456',
|
||||
borderRadius: 3
|
||||
}
|
||||
};
|
||||
idraw.modifyElement({
|
||||
uuid: targetElement.uuid,
|
||||
...deepClone(modifiedInfo1)
|
||||
});
|
||||
const expectedData1 = createData();
|
||||
const flattenModifiedInfo1 = toFlattenElement(modifiedInfo1);
|
||||
const beforeInfo1: Record<string, any> = {};
|
||||
const afterInfo1 = { ...flattenModifiedInfo1 };
|
||||
Object.keys(flattenModifiedInfo1).forEach((key) => {
|
||||
beforeInfo1[key] = get(expectedData1.elements[0], key);
|
||||
set(expectedData1.elements[0], key, flattenModifiedInfo1[key]);
|
||||
});
|
||||
const record1 = {
|
||||
type: 'modifyElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: beforeInfo1,
|
||||
after: afterInfo1
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const modifiedInfo2 = {
|
||||
x: modifiedInfo1.x + 3,
|
||||
y: modifiedInfo1.y + 4,
|
||||
detail: {
|
||||
borderRadius: [2, 4, 6, 8]
|
||||
}
|
||||
} as unknown as RecursivePartial<Omit<Element, 'uuid'>>;
|
||||
|
||||
idraw.modifyElement({
|
||||
uuid: targetElement.uuid,
|
||||
...deepClone(modifiedInfo2)
|
||||
} as RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>);
|
||||
|
||||
const expectedData2 = deepClone(expectedData1);
|
||||
const flattenModifiedInfo2 = toFlattenElement(modifiedInfo2);
|
||||
const beforeInfo2: Record<string, any> = {};
|
||||
const afterInfo2 = { ...flattenModifiedInfo2 };
|
||||
|
||||
Object.keys(flattenModifiedInfo2).forEach((key) => {
|
||||
let beforeVal = get(expectedData1.elements[0], key);
|
||||
let beforeKey = key;
|
||||
if (beforeVal === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
|
||||
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
|
||||
beforeVal = get(expectedData1.elements[0], beforeKey);
|
||||
}
|
||||
beforeInfo2[beforeKey] = beforeVal;
|
||||
set(expectedData2.elements[0], key, flattenModifiedInfo2[key]);
|
||||
});
|
||||
const record2 = {
|
||||
type: 'modifyElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: beforeInfo2,
|
||||
after: afterInfo2
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record2.content.after),
|
||||
after: deepClone(record2.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record1.content.after),
|
||||
after: deepClone(record1.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(createData());
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record4.content.after),
|
||||
after: deepClone(record4.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record3.content.after),
|
||||
after: deepClone(record3.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
180
packages/idraw/__tests__/history-modifyGlobal.test.ts
Normal file
180
packages/idraw/__tests__/history-modifyGlobal.test.ts
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenGlobal } from 'idraw';
|
||||
import type { Data, DataGlobal, RecursivePartial } from 'idraw';
|
||||
|
||||
const createData = () =>
|
||||
({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-001',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('circle', { uuid: 'test-002' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-003',
|
||||
detail: {
|
||||
text: 'Hello World'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('group', {
|
||||
uuid: 'test-005',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('rect', { uuid: 'test-006' }),
|
||||
createElement('circle', { uuid: 'test-007' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-008',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
} as Data);
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('modifyGlobal', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
|
||||
// modify 1: do
|
||||
const modifiedInfo1 = {
|
||||
background: '#123456'
|
||||
};
|
||||
idraw.modifyGlobal({
|
||||
...deepClone(modifiedInfo1)
|
||||
});
|
||||
const expectedData1 = createData();
|
||||
const flattenModifiedInfo1 = toFlattenGlobal(modifiedInfo1);
|
||||
const beforeInfo1: Record<string, any> | null = null;
|
||||
const afterInfo1 = { ...flattenModifiedInfo1 };
|
||||
|
||||
Object.keys(flattenModifiedInfo1).forEach((k) => {
|
||||
const key = `global.${k}`;
|
||||
set(expectedData1, key, flattenModifiedInfo1[k]);
|
||||
});
|
||||
const record1 = {
|
||||
type: 'modifyGlobal',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: beforeInfo1,
|
||||
after: afterInfo1
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const modifiedInfo2 = {
|
||||
background: '#AAAAAA'
|
||||
} as unknown as RecursivePartial<DataGlobal>;
|
||||
|
||||
idraw.modifyGlobal({ ...modifiedInfo2 });
|
||||
|
||||
const expectedData2 = deepClone(expectedData1);
|
||||
const flattenModifiedInfo2 = toFlattenGlobal(modifiedInfo2);
|
||||
const beforeInfo2: Record<string, any> = {};
|
||||
const afterInfo2 = { ...flattenModifiedInfo2 };
|
||||
|
||||
Object.keys(flattenModifiedInfo2).forEach((key) => {
|
||||
beforeInfo2[key] = get(expectedData1.global, key);
|
||||
set(expectedData2.global, key, flattenModifiedInfo2[key]);
|
||||
});
|
||||
const record2 = {
|
||||
type: 'modifyGlobal',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: beforeInfo2,
|
||||
after: afterInfo2
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: deepClone(record2.content.after),
|
||||
after: deepClone(record2.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: deepClone(record1.content.after),
|
||||
after: deepClone(record1.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(createData());
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: deepClone(record4.content.after),
|
||||
after: deepClone(record4.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyGlobal',
|
||||
before: deepClone(record3.content.after),
|
||||
after: deepClone(record3.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
197
packages/idraw/__tests__/history-modifyLayout.test.ts
Normal file
197
packages/idraw/__tests__/history-modifyLayout.test.ts
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenLayout } from 'idraw';
|
||||
import type { Data, DataLayout, RecursivePartial } from 'idraw';
|
||||
|
||||
const createData = () =>
|
||||
({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-001',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('circle', { uuid: 'test-002' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-003',
|
||||
detail: {
|
||||
text: 'Hello World'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('group', {
|
||||
uuid: 'test-005',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('rect', { uuid: 'test-006' }),
|
||||
createElement('circle', { uuid: 'test-007' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-008',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
} as Data);
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('modifyLayout', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
|
||||
// modify 1: do
|
||||
const modifiedInfo1 = {
|
||||
x: 1,
|
||||
y: 2,
|
||||
w: 100,
|
||||
h: 200,
|
||||
detail: {
|
||||
background: '#123456',
|
||||
borderRadius: 3
|
||||
}
|
||||
};
|
||||
idraw.modifyLayout({
|
||||
...deepClone(modifiedInfo1)
|
||||
});
|
||||
const expectedData1 = createData();
|
||||
const flattenModifiedInfo1 = toFlattenLayout(modifiedInfo1);
|
||||
const beforeInfo1: Record<string, any> | null = null;
|
||||
const afterInfo1 = { ...flattenModifiedInfo1 };
|
||||
|
||||
Object.keys(flattenModifiedInfo1).forEach((k) => {
|
||||
const key = `layout.${k}`;
|
||||
set(expectedData1, key, flattenModifiedInfo1[k]);
|
||||
});
|
||||
const record1 = {
|
||||
type: 'modifyLayout',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: beforeInfo1,
|
||||
after: afterInfo1
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const modifiedInfo2 = {
|
||||
x: modifiedInfo1.x + 3,
|
||||
y: modifiedInfo1.y + 4,
|
||||
detail: {
|
||||
borderRadius: [2, 4, 6, 8]
|
||||
}
|
||||
} as unknown as RecursivePartial<DataLayout>;
|
||||
|
||||
idraw.modifyLayout({ ...modifiedInfo2 });
|
||||
|
||||
const expectedData2 = deepClone(expectedData1);
|
||||
const flattenModifiedInfo2 = toFlattenLayout(modifiedInfo2);
|
||||
const beforeInfo2: Record<string, any> = {};
|
||||
const afterInfo2 = { ...flattenModifiedInfo2 };
|
||||
|
||||
Object.keys(flattenModifiedInfo2).forEach((key) => {
|
||||
let beforeVal = get(expectedData1.layout, key);
|
||||
let beforeKey = key;
|
||||
if (beforeVal === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
|
||||
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
|
||||
beforeVal = get(expectedData1.layout, beforeKey);
|
||||
}
|
||||
beforeInfo2[beforeKey] = beforeVal;
|
||||
set(expectedData2.layout, key, flattenModifiedInfo2[key]);
|
||||
});
|
||||
const record2 = {
|
||||
type: 'modifyLayout',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: beforeInfo2,
|
||||
after: afterInfo2
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: deepClone(record2.content.after),
|
||||
after: deepClone(record2.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: deepClone(record1.content.after),
|
||||
after: deepClone(record1.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(createData());
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: deepClone(record4.content.after),
|
||||
after: deepClone(record4.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyLayout',
|
||||
before: deepClone(record3.content.after),
|
||||
after: deepClone(record3.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
315
packages/idraw/__tests__/history-moveElement.test.ts
Normal file
315
packages/idraw/__tests__/history-moveElement.test.ts
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
import { iDraw, useHistory, findElementFromListByPosition, calcResultMovePosition } from 'idraw';
|
||||
import type { Elements } from 'idraw';
|
||||
|
||||
const getElemBase = () => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 1
|
||||
};
|
||||
};
|
||||
|
||||
function generateElements(list: any[]): Elements {
|
||||
const elements: Elements = list.map((item) => {
|
||||
if (Array.isArray(item)) {
|
||||
const groupIds = item[0].split('-');
|
||||
groupIds.pop();
|
||||
return {
|
||||
...getElemBase(),
|
||||
uuid: groupIds.join('-'),
|
||||
type: 'group',
|
||||
detail: {
|
||||
children: generateElements(item)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...getElemBase(),
|
||||
uuid: item,
|
||||
type: 'rect',
|
||||
detail: {}
|
||||
};
|
||||
}
|
||||
}) as Elements;
|
||||
return elements;
|
||||
}
|
||||
|
||||
const createData = (list: any[]) => ({
|
||||
elements: generateElements(list)
|
||||
});
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('moveElement', () => {
|
||||
const getList1 = () => ['0', '1', '2', ['3-0', '3-1', ['3-2-0', '3-2-1', '3-2-2', '3-2-3'], '3-3'], '4', '5'];
|
||||
const data = createData(getList1());
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
|
||||
// modify 1: do
|
||||
const from1 = [3, 2, 1];
|
||||
const to1 = [2];
|
||||
// result from: [ 4, 2, 1 ], to: [ 2 ]
|
||||
const uuid1 = findElementFromListByPosition(from1, data.elements)?.uuid as string;
|
||||
idraw.moveElement(uuid1, to1);
|
||||
|
||||
const record1 = {
|
||||
type: 'moveElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: uuid1,
|
||||
from: [...from1],
|
||||
to: [...to1]
|
||||
}
|
||||
};
|
||||
|
||||
// ['0', '1', '3-2-1', '2', ['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'], '4', '5'];
|
||||
const expectedElements1 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'3-2-1',
|
||||
'2',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
// const expectedElements1 = moveElementPosition(generateElements(getList1()), {
|
||||
// from: [...from1],
|
||||
// to: [...to1]
|
||||
// }).elements;
|
||||
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const from2 = [2];
|
||||
const to2 = [4];
|
||||
const uuid2 = findElementFromListByPosition(from2, data.elements)?.uuid as string;
|
||||
// console.log('uuid2 ----- ', uuid2, findElementFromListByPosition(to2, data.elements)?.uuid);
|
||||
idraw.moveElement(uuid1, to2);
|
||||
const record2 = {
|
||||
type: 'moveElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: uuid2,
|
||||
from: [...from2],
|
||||
to: [...to2]
|
||||
}
|
||||
};
|
||||
const expectedElements2 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3-2-1',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
// const expectedElements2 = moveElementPosition(expectedElements1, {
|
||||
// from: [...from2],
|
||||
// to: [...to2]
|
||||
// }).elements;
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const moveResult2 = calcResultMovePosition({
|
||||
from: [...from2],
|
||||
to: [...to2]
|
||||
}) as { from: number[]; to: number[] };
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: record2.content.uuid,
|
||||
from: [...moveResult2.to],
|
||||
to: [...moveResult2.from]
|
||||
}
|
||||
};
|
||||
const expectedElements3 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'3-2-1',
|
||||
'2',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
// const expectedElements3 = moveElementPosition(expectedElements1, {
|
||||
// from: [...moveResult2.to],
|
||||
// to: [...moveResult2.from]
|
||||
// }).elements;
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements3);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const moveResult3 = calcResultMovePosition({
|
||||
from: [...from1],
|
||||
to: [...to1]
|
||||
}) as { from: number[]; to: number[] };
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: record1.content.uuid,
|
||||
from: [...moveResult3.to],
|
||||
to: [...moveResult3.from]
|
||||
}
|
||||
};
|
||||
const expectedElements4 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-1', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
// const expectedElements4 = moveElementPosition(expectedElements3, {
|
||||
// from: [...moveResult3.to],
|
||||
// to: [...moveResult3.from]
|
||||
// }).elements;
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements4);
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const moveResult4 = calcResultMovePosition({
|
||||
from: [...record4.content.from],
|
||||
to: [...record4.content.to]
|
||||
}) as { from: number[]; to: number[] };
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: record4.content.uuid,
|
||||
from: [...moveResult4.to],
|
||||
to: [...moveResult4.from]
|
||||
}
|
||||
};
|
||||
const expectedElements5 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'3-2-1',
|
||||
'2',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
// const expectedElements5 = moveElementPosition(expectedElements3, {
|
||||
// from: [...moveResult4.from],
|
||||
// to: [...moveResult4.to]
|
||||
// }).elements;
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements5);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 6: redo
|
||||
redo();
|
||||
const moveResult5 = calcResultMovePosition({
|
||||
from: [...record3.content.from],
|
||||
to: [...record3.content.to]
|
||||
}) as { from: number[]; to: number[] };
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: record4.content.uuid,
|
||||
from: [...moveResult5.to],
|
||||
to: [...moveResult5.from]
|
||||
}
|
||||
};
|
||||
const expectedElements6 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
'3-2-1',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements6);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 7: undo
|
||||
undo();
|
||||
const moveResult6 = calcResultMovePosition({
|
||||
from: [...record6.content.from],
|
||||
to: [...record6.content.to]
|
||||
}) as { from: number[]; to: number[] };
|
||||
const record7 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: record6.content.uuid,
|
||||
from: [...moveResult6.to],
|
||||
to: [...moveResult6.from]
|
||||
}
|
||||
};
|
||||
const expectedElements7 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'3-2-1',
|
||||
'2',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements7);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record7]);
|
||||
|
||||
// modify 8: undo
|
||||
undo();
|
||||
const moveResult7 = calcResultMovePosition({
|
||||
from: [...record5.content.from],
|
||||
to: [...record5.content.to]
|
||||
}) as { from: number[]; to: number[] };
|
||||
const record8 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'moveElement',
|
||||
uuid: record5.content.uuid,
|
||||
from: [...moveResult7.to],
|
||||
to: [...moveResult7.from]
|
||||
}
|
||||
};
|
||||
const expectedElements8 = generateElements([
|
||||
'0',
|
||||
'1',
|
||||
'2',
|
||||
['3-0', '3-1', ['3-2-0', '3-2-1', '3-2-2', '3-2-3'], '3-3'],
|
||||
'4',
|
||||
'5'
|
||||
]);
|
||||
expect(idraw.getData()?.elements).toStrictEqual(expectedElements8);
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record7, record8]);
|
||||
});
|
||||
});
|
||||
177
packages/idraw/__tests__/history-updateElement.test.ts
Normal file
177
packages/idraw/__tests__/history-updateElement.test.ts
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, toFlattenElement, mergeElement } from 'idraw';
|
||||
|
||||
const createData = () => ({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-001',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('circle', { uuid: 'test-002' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-003',
|
||||
detail: {
|
||||
text: 'Hello World'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('group', {
|
||||
uuid: 'test-005',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('rect', { uuid: 'test-006' }),
|
||||
createElement('circle', { uuid: 'test-007' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-008',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('updateElement', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
const targetElement = deepClone(data.elements[0]);
|
||||
|
||||
// modify 1: do
|
||||
const updatedElement1 = deepClone(targetElement);
|
||||
updatedElement1.x += 1;
|
||||
updatedElement1.y += 2;
|
||||
updatedElement1.detail.background = '#123456';
|
||||
updatedElement1.detail.borderRadius = 3;
|
||||
idraw.updateElement(updatedElement1);
|
||||
|
||||
const beforeInfo1: Record<string, any> = toFlattenElement(targetElement);
|
||||
const afterInfo1: Record<string, any> = toFlattenElement(updatedElement1);
|
||||
|
||||
const expectedData1 = createData();
|
||||
mergeElement(expectedData1.elements[0], updatedElement1);
|
||||
const record1 = {
|
||||
type: 'updateElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'updateElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: beforeInfo1,
|
||||
after: afterInfo1
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const updatedElement2 = deepClone(updatedElement1);
|
||||
updatedElement2.x += 3;
|
||||
updatedElement2.y += 4;
|
||||
updatedElement2.detail.borderRadius = [2, 4, 6, 8];
|
||||
idraw.updateElement(updatedElement2);
|
||||
const beforeInfo2: Record<string, any> = toFlattenElement(updatedElement1);
|
||||
const afterInfo2: Record<string, any> = toFlattenElement(updatedElement2);
|
||||
|
||||
const expectedData2 = createData();
|
||||
mergeElement(expectedData2.elements[0], updatedElement2);
|
||||
const record2 = {
|
||||
type: 'updateElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'updateElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: beforeInfo2,
|
||||
after: afterInfo2
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'updateElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record2.content.after),
|
||||
after: deepClone(record2.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'updateElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record1.content.after),
|
||||
after: deepClone(record1.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(createData());
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'updateElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record4.content.after),
|
||||
after: deepClone(record4.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'updateElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record3.content.after),
|
||||
after: deepClone(record3.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
204
packages/idraw/__tests__/history.test.ts
Normal file
204
packages/idraw/__tests__/history.test.ts
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
import { iDraw, useHistory, deepClone, createElement, set, get, toFlattenElement } from 'idraw';
|
||||
import type { RecursivePartial, Element } from 'idraw';
|
||||
|
||||
const createData = () => ({
|
||||
elements: [
|
||||
createElement('rect', {
|
||||
uuid: 'test-001',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
background: '#DDDDDD'
|
||||
}
|
||||
}),
|
||||
createElement('circle', { uuid: 'test-002' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-003',
|
||||
detail: {
|
||||
text: 'Hello World'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-004', detail: { src: 'https://example.com/001.png' } }),
|
||||
createElement('group', {
|
||||
uuid: 'test-005',
|
||||
detail: {
|
||||
children: [
|
||||
createElement('rect', { uuid: 'test-006' }),
|
||||
createElement('circle', { uuid: 'test-007' }),
|
||||
createElement('text', {
|
||||
uuid: 'test-008',
|
||||
detail: {
|
||||
text: 'Text in Group'
|
||||
}
|
||||
}),
|
||||
createElement('image', { uuid: 'test-009', detail: { src: 'https://example.com/002.png' } })
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
describe('idraw: useHistory ', () => {
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers().setSystemTime(new Date('2025-01-01'));
|
||||
});
|
||||
|
||||
test('modifyElement', () => {
|
||||
const data = createData();
|
||||
const div = document.createElement('div') as HTMLDivElement;
|
||||
|
||||
const idraw = new iDraw(div, {
|
||||
height: 200,
|
||||
width: 200
|
||||
});
|
||||
const { MiddlewareHistory, history } = useHistory({ instance: idraw });
|
||||
const { undo, redo, __getDoRecords, __getUndoRecords } = history;
|
||||
idraw.use(MiddlewareHistory);
|
||||
idraw.setData(data);
|
||||
const targetElement = deepClone(data.elements[0]);
|
||||
|
||||
// modify 1: do
|
||||
const modifiedInfo1 = {
|
||||
x: targetElement.x + 1,
|
||||
y: targetElement.y + 2,
|
||||
detail: {
|
||||
background: '#123456',
|
||||
borderRadius: 3
|
||||
}
|
||||
};
|
||||
idraw.modifyElement({
|
||||
uuid: targetElement.uuid,
|
||||
...deepClone(modifiedInfo1)
|
||||
});
|
||||
const expectedData1 = createData();
|
||||
const flattenModifiedInfo1 = toFlattenElement(modifiedInfo1);
|
||||
const beforeInfo1: Record<string, any> = {};
|
||||
const afterInfo1 = { ...flattenModifiedInfo1 };
|
||||
Object.keys(flattenModifiedInfo1).forEach((key) => {
|
||||
beforeInfo1[key] = get(expectedData1.elements[0], key);
|
||||
set(expectedData1.elements[0], key, flattenModifiedInfo1[key]);
|
||||
});
|
||||
const record1 = {
|
||||
type: 'modifyElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: beforeInfo1,
|
||||
after: afterInfo1
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 2: do
|
||||
const modifiedInfo2 = {
|
||||
x: modifiedInfo1.x + 3,
|
||||
y: modifiedInfo1.y + 4,
|
||||
detail: {
|
||||
borderRadius: [2, 4, 6, 8]
|
||||
}
|
||||
} as unknown as RecursivePartial<Omit<Element, 'uuid'>>;
|
||||
|
||||
idraw.modifyElement({
|
||||
uuid: targetElement.uuid,
|
||||
...deepClone(modifiedInfo2)
|
||||
} as RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>);
|
||||
|
||||
const expectedData2 = deepClone(expectedData1);
|
||||
const flattenModifiedInfo2 = toFlattenElement(modifiedInfo2);
|
||||
const beforeInfo2: Record<string, any> = {};
|
||||
const afterInfo2 = { ...flattenModifiedInfo2 };
|
||||
|
||||
Object.keys(flattenModifiedInfo2).forEach((key) => {
|
||||
let beforeVal = get(expectedData1.elements[0], key);
|
||||
let beforeKey = key;
|
||||
if (beforeVal === undefined && /(borderRadius|borderWidth)\[[0-9]{1,}\]$/.test(beforeKey)) {
|
||||
beforeKey = beforeKey.replace(/\[[0-9]{1,}\]$/, '');
|
||||
beforeVal = get(expectedData1.elements[0], beforeKey);
|
||||
}
|
||||
beforeInfo2[beforeKey] = beforeVal;
|
||||
set(expectedData2.elements[0], key, flattenModifiedInfo2[key]);
|
||||
});
|
||||
const record2 = {
|
||||
type: 'modifyElement',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: beforeInfo2,
|
||||
after: afterInfo2
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record1, record2]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
|
||||
// modify 3: undo
|
||||
undo();
|
||||
const record3 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record2.content.after),
|
||||
after: deepClone(record2.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record1]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 4: undo
|
||||
undo();
|
||||
const record4 = {
|
||||
type: 'undo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record1.content.after),
|
||||
after: deepClone(record1.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(createData());
|
||||
expect(__getDoRecords()).toStrictEqual([]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3, record4]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record5 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record4.content.after),
|
||||
after: deepClone(record4.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData1);
|
||||
expect(__getDoRecords()).toStrictEqual([record5]);
|
||||
expect(__getUndoRecords()).toStrictEqual([record3]);
|
||||
|
||||
// modify 5: redo
|
||||
redo();
|
||||
const record6 = {
|
||||
type: 'redo',
|
||||
time: new Date().getTime(),
|
||||
content: {
|
||||
method: 'modifyElement',
|
||||
uuid: targetElement.uuid,
|
||||
before: deepClone(record3.content.after),
|
||||
after: deepClone(record3.content.before)
|
||||
}
|
||||
};
|
||||
expect(idraw.getData()).toStrictEqual(expectedData2);
|
||||
expect(__getDoRecords()).toStrictEqual([record5, record6]);
|
||||
expect(__getUndoRecords()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
import { requestAnimationFrameMock } from './../../../__tests__/polyfill/requestanimateframe';
|
||||
import './../../../__tests__/polyfill/image';
|
||||
|
||||
import IDraw from './../src';
|
||||
import { getData } from './data';
|
||||
|
||||
function delay(time: number): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve();
|
||||
}, time);
|
||||
});
|
||||
}
|
||||
|
||||
describe('idraw', () => {
|
||||
beforeEach(() => {
|
||||
requestAnimationFrameMock.reset();
|
||||
});
|
||||
|
||||
test('context', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="mount"></div>
|
||||
`;
|
||||
const opts = {
|
||||
width: 600,
|
||||
height: 400,
|
||||
contextWidth: 600,
|
||||
contextHeight: 400,
|
||||
devicePixelRatio: 4
|
||||
};
|
||||
const mount = document.querySelector('#mount') as HTMLDivElement;
|
||||
const idraw = new IDraw(mount, opts);
|
||||
const data = getData();
|
||||
idraw.setData(data, { triggerChangeEvent: true });
|
||||
|
||||
requestAnimationFrameMock.triggerNextAnimationFrame();
|
||||
|
||||
const originCtx = idraw.$getOriginContext2D();
|
||||
// @ts-ignore;
|
||||
const originCalls = originCtx.__getDrawCalls();
|
||||
expect(originCalls).toMatchSnapshot();
|
||||
|
||||
const displayCtx = idraw.$getDisplayContext2D();
|
||||
// @ts-ignore;
|
||||
const displayCalls = displayCtx.__getDrawCalls();
|
||||
expect(displayCalls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('undo/redo', async () => {
|
||||
document.body.innerHTML = `
|
||||
<div id="mount"></div>
|
||||
`;
|
||||
const opts = {
|
||||
width: 600,
|
||||
height: 400,
|
||||
contextWidth: 600,
|
||||
contextHeight: 400,
|
||||
devicePixelRatio: 4
|
||||
};
|
||||
const mount = document.querySelector('#mount') as HTMLDivElement;
|
||||
const idraw = new IDraw(mount, opts);
|
||||
const data = getData();
|
||||
idraw.setData(data, { triggerChangeEvent: true });
|
||||
idraw.moveDownElement('svg-004');
|
||||
idraw.moveDownElement('image-003');
|
||||
await delay(10);
|
||||
|
||||
const undo1 = idraw.undo();
|
||||
expect(undo1.doRecordCount).toBe(2);
|
||||
expect(undo1.data?.elements?.length).toBe(4);
|
||||
|
||||
const undo2 = idraw.undo();
|
||||
expect(undo2.doRecordCount).toBe(1);
|
||||
expect(undo2.data?.elements?.length).toBe(4);
|
||||
|
||||
const redo1 = idraw.redo();
|
||||
expect(redo1.undoRecordCount).toBe(1);
|
||||
expect(redo1.data?.elements?.length).toBe(4);
|
||||
|
||||
idraw.moveDownElement('image-003');
|
||||
|
||||
const redo2 = idraw.redo();
|
||||
expect(redo2.undoRecordCount).toBe(0);
|
||||
expect(redo2.data).toBe(null);
|
||||
|
||||
requestAnimationFrameMock.triggerNextAnimationFrame();
|
||||
|
||||
const originCtx = idraw.$getOriginContext2D();
|
||||
// @ts-ignore;
|
||||
const originCalls = originCtx.__getDrawCalls();
|
||||
expect(originCalls).toMatchSnapshot();
|
||||
|
||||
const displayCtx = idraw.$getDisplayContext2D();
|
||||
// @ts-ignore;
|
||||
const displayCalls = displayCtx.__getDrawCalls();
|
||||
expect(displayCalls).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -25,9 +25,12 @@ export type ExportImageFileResult = {
|
|||
export async function exportImageFileBlobURL(opts: ExportImageFileOptions): Promise<ExportImageFileResult> {
|
||||
const { data, width, height, devicePixelRatio, viewScaleInfo, viewSizeInfo, loadItemMap } = opts;
|
||||
let viewContext: ViewContext2D | null = createOffscreenContext2D({ width, height, devicePixelRatio });
|
||||
let tempContext: ViewContext2D | null = createOffscreenContext2D({ width, height, devicePixelRatio });
|
||||
|
||||
// let calculator: Calculator | null = new Calculator({ viewContext });
|
||||
let renderer: Renderer | null = new Renderer({
|
||||
viewContext
|
||||
viewContext,
|
||||
tempContext
|
||||
});
|
||||
renderer.setLoadItemMap(loadItemMap);
|
||||
renderer.drawData(data, {
|
||||
|
|
@ -35,6 +38,7 @@ export async function exportImageFileBlobURL(opts: ExportImageFileOptions): Prom
|
|||
viewSizeInfo,
|
||||
forceDrawAll: true
|
||||
});
|
||||
|
||||
let blobURL: string | null = null;
|
||||
let offScreenCanvas = viewContext.$getOffscreenCanvas();
|
||||
if (offScreenCanvas) {
|
||||
|
|
@ -44,6 +48,7 @@ export async function exportImageFileBlobURL(opts: ExportImageFileOptions): Prom
|
|||
|
||||
offScreenCanvas = null;
|
||||
viewContext = null;
|
||||
tempContext = null;
|
||||
renderer = null;
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -13,35 +13,21 @@ import type {
|
|||
RecursivePartial,
|
||||
ElementPosition,
|
||||
IDrawStorage,
|
||||
DataLayout
|
||||
DataLayout,
|
||||
DataGlobal,
|
||||
Middleware
|
||||
} from '@idraw/types';
|
||||
import {
|
||||
createElement,
|
||||
insertElementToListByPosition,
|
||||
updateElementInList,
|
||||
deleteElementInList,
|
||||
moveElementPosition,
|
||||
getElementPositionFromList,
|
||||
calcElementListSize,
|
||||
filterCompactData,
|
||||
calcViewCenterContent,
|
||||
calcViewCenter,
|
||||
Store,
|
||||
merge
|
||||
} from '@idraw/util';
|
||||
import {
|
||||
defaultSettings,
|
||||
defaultOptions,
|
||||
getDefaultStorage,
|
||||
defaultMode,
|
||||
parseStyles,
|
||||
parseSettings
|
||||
} from './setting/config';
|
||||
import { exportImageFileBlobURL } from './file';
|
||||
import { filterCompactData, calcViewCenterContent, calcViewCenter, Store } from '@idraw/util';
|
||||
import { defaultSettings, defaultOptions, getDefaultStorage, defaultMode, parseStyles } from './setting/config';
|
||||
import type { ExportImageFileBaseOptions, ExportImageFileResult } from './file';
|
||||
import type { IDrawEvent } from './event';
|
||||
import { changeMode, runMiddlewares } from './setting/mode';
|
||||
import { changeStyles } from './setting/style';
|
||||
import { changeMode } from './setting/mode';
|
||||
import { createElement, updateElement, modifyElement, addElement, deleteElement, moveElement } from './methods/element';
|
||||
import { modifyLayout } from './methods/layout';
|
||||
import { modifyGlobal } from './methods/global';
|
||||
import { reset } from './methods/reset';
|
||||
import { setFeature } from './methods/feature';
|
||||
import { getImageBlobURL } from './methods/image';
|
||||
|
||||
export class iDraw {
|
||||
#core: Core<IDrawEvent>;
|
||||
|
|
@ -67,54 +53,20 @@ export class iDraw {
|
|||
}
|
||||
|
||||
#setFeature(feat: IDrawFeature, status: boolean) {
|
||||
const store = this.#store;
|
||||
if (['ruler', 'scroll', 'scale', 'info'].includes(feat)) {
|
||||
const map: Record<IDrawFeature | string, keyof Omit<IDrawStorage, 'mode'>> = {
|
||||
ruler: 'enableRuler',
|
||||
scroll: 'enableScroll',
|
||||
scale: 'enableScale',
|
||||
info: 'enableInfo'
|
||||
};
|
||||
store.set(map[feat], !!status);
|
||||
runMiddlewares(this.#core, store);
|
||||
this.#core.refresh();
|
||||
} else if (feat === 'selectInGroup') {
|
||||
this.#core.trigger(coreEventKeys.SELECT_IN_GROUP, {
|
||||
enable: !!status
|
||||
});
|
||||
} else if (feat === 'snapToGrid') {
|
||||
this.#core.trigger(coreEventKeys.SNAP_TO_GRID, {
|
||||
enable: !!status
|
||||
});
|
||||
}
|
||||
return setFeature({ core: this.#core, store: this.#store }, feat, status);
|
||||
}
|
||||
|
||||
use<C extends any = any>(middleware: Middleware<any, any, any>, config?: C) {
|
||||
this.#core.use<C>(middleware, config);
|
||||
}
|
||||
|
||||
disuse(middleware: Middleware<any, any, any>) {
|
||||
this.#core.disuse(middleware);
|
||||
}
|
||||
|
||||
reset(opts: IDrawSettings) {
|
||||
const core = this.#core;
|
||||
const store = this.#store;
|
||||
const { mode, styles } = parseSettings(opts);
|
||||
let needFresh = false;
|
||||
const newOpts: IDrawSettings = {};
|
||||
store.clear();
|
||||
if (mode) {
|
||||
changeMode(mode, core, store);
|
||||
newOpts.mode = mode;
|
||||
needFresh = true;
|
||||
}
|
||||
if (styles) {
|
||||
changeStyles(styles, core, store);
|
||||
newOpts.styles = styles;
|
||||
needFresh = true;
|
||||
}
|
||||
|
||||
if (needFresh === true) {
|
||||
core.refresh();
|
||||
}
|
||||
|
||||
this.#opts = {
|
||||
...this.#opts,
|
||||
...newOpts
|
||||
};
|
||||
const newOpts = reset({ core: this.#core, store: this.#store }, opts);
|
||||
this.#opts = { ...this.#opts, ...newOpts };
|
||||
}
|
||||
|
||||
setMode(mode: IDrawMode) {
|
||||
|
|
@ -148,10 +100,7 @@ export class iDraw {
|
|||
return data;
|
||||
}
|
||||
|
||||
getViewInfo(): {
|
||||
viewSizeInfo: ViewSizeInfo;
|
||||
viewScaleInfo: ViewScaleInfo;
|
||||
} {
|
||||
getViewInfo(): { viewSizeInfo: ViewSizeInfo; viewScaleInfo: ViewScaleInfo } {
|
||||
return this.#core.getViewInfo();
|
||||
}
|
||||
|
||||
|
|
@ -212,108 +161,44 @@ export class iDraw {
|
|||
|
||||
createElement<T extends ElementType>(
|
||||
type: T,
|
||||
opts?: {
|
||||
element?: RecursivePartial<Element<T>>;
|
||||
viewCenter?: boolean;
|
||||
}
|
||||
element: RecursivePartial<Element<T>>,
|
||||
opts?: { viewCenter?: boolean }
|
||||
): Element<T> {
|
||||
const { viewScaleInfo, viewSizeInfo } = this.#core.getViewInfo();
|
||||
return createElement<T>(
|
||||
type,
|
||||
opts?.element || {},
|
||||
opts?.viewCenter === true
|
||||
? {
|
||||
viewScaleInfo,
|
||||
viewSizeInfo
|
||||
}
|
||||
: undefined
|
||||
);
|
||||
return createElement<T>({ core: this.#core }, type, element, opts);
|
||||
}
|
||||
|
||||
updateElement(element: Element) {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
updateElementInList(element.uuid, element, data.elements);
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'updateElement' });
|
||||
return updateElement({ core: this.#core }, element);
|
||||
}
|
||||
|
||||
updateElementName(uuid: string, name: string) {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
updateElementInList(uuid, { name }, data.elements);
|
||||
core.setData(data);
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'updateElementName' });
|
||||
modifyElement(element: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>) {
|
||||
return modifyElement({ core: this.#core }, element);
|
||||
}
|
||||
|
||||
addElement(
|
||||
element: Element,
|
||||
opts?: {
|
||||
position: ElementPosition;
|
||||
}
|
||||
): Data {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
if (!opts || !opts?.position?.length) {
|
||||
data.elements.push(element);
|
||||
} else if (opts?.position) {
|
||||
const position = [...(opts?.position || [])];
|
||||
insertElementToListByPosition(element, position, data.elements);
|
||||
}
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'addElement' });
|
||||
return data;
|
||||
addElement(element: Element, opts?: { position: ElementPosition }): Data {
|
||||
return addElement({ core: this.#core }, element, opts);
|
||||
}
|
||||
|
||||
deleteElement(uuid: string) {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
deleteElementInList(uuid, data.elements);
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'deleteElement' });
|
||||
return deleteElement({ core: this.#core }, uuid);
|
||||
}
|
||||
|
||||
moveElement(uuid: string, to: ElementPosition) {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
const from = getElementPositionFromList(uuid, data.elements);
|
||||
const { elements: list } = moveElementPosition(data.elements, { from, to });
|
||||
data.elements = list;
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'moveElement' });
|
||||
return moveElement({ core: this.#core }, uuid, to);
|
||||
}
|
||||
|
||||
updateLayout(layout: Partial<DataLayout>) {
|
||||
const core = this.#core;
|
||||
const data: Data = core.getData() || { elements: [] };
|
||||
data.layout = merge(data.layout || {}, layout) as DataLayout;
|
||||
core.setData(data);
|
||||
core.refresh();
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'updateLayout' });
|
||||
modifyLayout(layout: RecursivePartial<DataLayout> | null) {
|
||||
return modifyLayout({ core: this.#core }, layout);
|
||||
}
|
||||
|
||||
modifyGlobal(global: RecursivePartial<DataGlobal> | null) {
|
||||
return modifyGlobal({ core: this.#core }, global);
|
||||
}
|
||||
|
||||
async getImageBlobURL(opts?: ExportImageFileBaseOptions): Promise<ExportImageFileResult> {
|
||||
const data = this.getData() || { elements: [] };
|
||||
const { devicePixelRatio } = opts || { devicePixelRatio: 1 };
|
||||
|
||||
const outputSize = calcElementListSize(data.elements);
|
||||
const { viewSizeInfo } = this.getViewInfo();
|
||||
return await exportImageFileBlobURL({
|
||||
width: outputSize.w,
|
||||
height: outputSize.h,
|
||||
devicePixelRatio,
|
||||
data,
|
||||
viewScaleInfo: { scale: 1, offsetLeft: -outputSize.x, offsetTop: -outputSize.y, offsetBottom: 0, offsetRight: 0 },
|
||||
viewSizeInfo: {
|
||||
...viewSizeInfo,
|
||||
...{ devicePixelRatio }
|
||||
},
|
||||
loadItemMap: this.#core.getLoadItemMap()
|
||||
});
|
||||
return await getImageBlobURL({ data, viewSizeInfo, core: this.#core }, opts);
|
||||
}
|
||||
|
||||
isDestroyed() {
|
||||
|
|
@ -321,10 +206,8 @@ export class iDraw {
|
|||
}
|
||||
|
||||
destroy() {
|
||||
const core = this.#core;
|
||||
const store = this.#store;
|
||||
core.destroy();
|
||||
store.destroy();
|
||||
this.#core.destroy();
|
||||
this.#store.destroy();
|
||||
}
|
||||
|
||||
getViewCenter(): PointSize {
|
||||
|
|
@ -333,11 +216,15 @@ export class iDraw {
|
|||
return pointSize;
|
||||
}
|
||||
|
||||
$onBoardWatcherEvents() {
|
||||
this.#core.onBoardWatcherEvents();
|
||||
}
|
||||
// $onBoardWatcherEvents() {
|
||||
// this.#core.onBoardWatcherEvents();
|
||||
// }
|
||||
|
||||
$offBoardWatcherEvents() {
|
||||
this.#core.offBoardWatcherEvents();
|
||||
// $offBoardWatcherEvents() {
|
||||
// this.#core.offBoardWatcherEvents();
|
||||
// }
|
||||
|
||||
getCore() {
|
||||
return this.#core;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export {
|
|||
Sharer,
|
||||
Calculator,
|
||||
Core,
|
||||
Board,
|
||||
MiddlewareSelector,
|
||||
MiddlewareScroller,
|
||||
MiddlewareScaler,
|
||||
|
|
@ -118,15 +119,25 @@ export {
|
|||
deepCloneElement,
|
||||
calcViewCenterContent,
|
||||
calcViewCenter,
|
||||
modifyElement,
|
||||
calcElementViewRectInfo,
|
||||
calcElementOriginRectInfo,
|
||||
flatElementList,
|
||||
calcPointMoveElementInGroup,
|
||||
merge,
|
||||
omit
|
||||
omit,
|
||||
toFlattenElement,
|
||||
toFlattenGlobal,
|
||||
toFlattenLayout,
|
||||
flatObject,
|
||||
unflatObject,
|
||||
set,
|
||||
get,
|
||||
mergeElement,
|
||||
calcResultMovePosition,
|
||||
calcRevertMovePosition
|
||||
} from '@idraw/util';
|
||||
export { iDraw } from './idraw';
|
||||
export { eventKeys } from './event';
|
||||
export type { IDrawEvent, IDrawEventKeys } from './event';
|
||||
export type { ExportImageFileResult, ExportImageFileBaseOptions } from './file';
|
||||
export { useHistory } from './middlewares/use-history';
|
||||
|
|
|
|||
84
packages/idraw/src/methods/element.ts
Normal file
84
packages/idraw/src/methods/element.ts
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
import type { Data, Element, ElementType, ElementPosition, RecursivePartial } from '@idraw/types';
|
||||
import { Core, coreEventKeys } from '@idraw/core';
|
||||
import { IDrawEvent } from '../event';
|
||||
|
||||
export function createElement<T extends ElementType = ElementType>(
|
||||
depOptions: {
|
||||
core: Core<IDrawEvent>;
|
||||
},
|
||||
type: T,
|
||||
element: RecursivePartial<Element<T>>,
|
||||
opts?: {
|
||||
viewCenter?: boolean;
|
||||
}
|
||||
): Element<T> {
|
||||
const { core } = depOptions;
|
||||
return core.createElement<T>(type, element, opts);
|
||||
}
|
||||
|
||||
export function updateElement(
|
||||
depOptions: {
|
||||
core: Core<IDrawEvent>;
|
||||
},
|
||||
element: Element
|
||||
) {
|
||||
const { core } = depOptions;
|
||||
|
||||
const modifyRecord = core.updateElement(element);
|
||||
if (!modifyRecord) {
|
||||
return;
|
||||
}
|
||||
const data = core.getData();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'updateElement', modifyRecord });
|
||||
}
|
||||
|
||||
export function modifyElement(
|
||||
depOptions: {
|
||||
core: Core<IDrawEvent>;
|
||||
},
|
||||
element: RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
|
||||
) {
|
||||
const { core } = depOptions;
|
||||
const modifyRecord = core.modifyElement(element);
|
||||
if (!modifyRecord) {
|
||||
return;
|
||||
}
|
||||
const data = core.getData();
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'updateElement', modifyRecord });
|
||||
}
|
||||
|
||||
export function addElement(
|
||||
depOptions: {
|
||||
core: Core<IDrawEvent>;
|
||||
},
|
||||
element: Element,
|
||||
opts?: {
|
||||
position: ElementPosition;
|
||||
}
|
||||
): Data {
|
||||
const { core } = depOptions;
|
||||
const modifyRecord = core.addElement(element, opts);
|
||||
const data = core.getData() as Data;
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'addElement', modifyRecord });
|
||||
return data;
|
||||
}
|
||||
|
||||
export function deleteElement(depOptions: { core: Core<IDrawEvent> }, uuid: string) {
|
||||
const { core } = depOptions;
|
||||
const modifyRecord = core.deleteElement(uuid);
|
||||
const data = core.getData() as Data;
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'deleteElement', modifyRecord });
|
||||
}
|
||||
|
||||
export function moveElement(depOptions: { core: Core<IDrawEvent> }, uuid: string, to: ElementPosition) {
|
||||
const { core } = depOptions;
|
||||
const modifyRecord = core.moveElement(uuid, to);
|
||||
const data = core.getData() as Data;
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'moveElement', modifyRecord });
|
||||
}
|
||||
32
packages/idraw/src/methods/feature.ts
Normal file
32
packages/idraw/src/methods/feature.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import type { IDrawFeature, IDrawStorage } from '@idraw/types';
|
||||
import { Core, coreEventKeys } from '@idraw/core';
|
||||
import { Store } from '@idraw/util';
|
||||
import { IDrawEvent } from '../event';
|
||||
import { runMiddlewares } from '../setting/mode';
|
||||
|
||||
export function setFeature(
|
||||
depOptions: { core: Core<IDrawEvent>; store: Store<IDrawStorage> },
|
||||
feat: IDrawFeature,
|
||||
status: boolean
|
||||
) {
|
||||
const { core, store } = depOptions;
|
||||
if (['ruler', 'scroll', 'scale', 'info'].includes(feat)) {
|
||||
const map: Record<IDrawFeature | string, keyof Omit<IDrawStorage, 'mode'>> = {
|
||||
ruler: 'enableRuler',
|
||||
scroll: 'enableScroll',
|
||||
scale: 'enableScale',
|
||||
info: 'enableInfo'
|
||||
};
|
||||
store.set(map[feat], !!status);
|
||||
runMiddlewares(core, store);
|
||||
core.refresh();
|
||||
} else if (feat === 'selectInGroup') {
|
||||
core.trigger(coreEventKeys.SELECT_IN_GROUP, {
|
||||
enable: !!status
|
||||
});
|
||||
} else if (feat === 'snapToGrid') {
|
||||
core.trigger(coreEventKeys.SNAP_TO_GRID, {
|
||||
enable: !!status
|
||||
});
|
||||
}
|
||||
}
|
||||
10
packages/idraw/src/methods/global.ts
Normal file
10
packages/idraw/src/methods/global.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Data, DataGlobal, RecursivePartial } from '@idraw/types';
|
||||
import { Core, coreEventKeys } from '@idraw/core';
|
||||
import { IDrawEvent } from '../event';
|
||||
|
||||
export function modifyGlobal(depOptions: { core: Core<IDrawEvent> }, global: RecursivePartial<DataGlobal> | null) {
|
||||
const { core } = depOptions;
|
||||
const modifyRecord = core.modifyGlobal(global);
|
||||
const data = core.getData() as Data;
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'modifyGlobal', modifyRecord });
|
||||
}
|
||||
29
packages/idraw/src/methods/image.ts
Normal file
29
packages/idraw/src/methods/image.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import type { Data, ViewSizeInfo } from '@idraw/types';
|
||||
import { Core } from '@idraw/core';
|
||||
import { calcElementListSize } from '@idraw/util';
|
||||
import { IDrawEvent } from '../event';
|
||||
import { exportImageFileBlobURL } from '../file';
|
||||
import type { ExportImageFileBaseOptions, ExportImageFileResult } from '../file';
|
||||
|
||||
export async function getImageBlobURL(
|
||||
depOptions: { data: Data; viewSizeInfo: ViewSizeInfo; core: Core<IDrawEvent> },
|
||||
opts?: ExportImageFileBaseOptions
|
||||
): Promise<ExportImageFileResult> {
|
||||
const { data, viewSizeInfo, core } = depOptions;
|
||||
const { devicePixelRatio } = opts || { devicePixelRatio: 1 };
|
||||
|
||||
const outputSize = calcElementListSize(data.elements);
|
||||
|
||||
return await exportImageFileBlobURL({
|
||||
width: outputSize.w,
|
||||
height: outputSize.h,
|
||||
devicePixelRatio,
|
||||
data,
|
||||
viewScaleInfo: { scale: 1, offsetLeft: -outputSize.x, offsetTop: -outputSize.y, offsetBottom: 0, offsetRight: 0 },
|
||||
viewSizeInfo: {
|
||||
...viewSizeInfo,
|
||||
...{ devicePixelRatio }
|
||||
},
|
||||
loadItemMap: core.getLoadItemMap()
|
||||
});
|
||||
}
|
||||
10
packages/idraw/src/methods/layout.ts
Normal file
10
packages/idraw/src/methods/layout.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import type { Data, DataLayout, RecursivePartial } from '@idraw/types';
|
||||
import { Core, coreEventKeys } from '@idraw/core';
|
||||
import { IDrawEvent } from '../event';
|
||||
|
||||
export function modifyLayout(depOptions: { core: Core<IDrawEvent> }, layout: RecursivePartial<DataLayout> | null) {
|
||||
const { core } = depOptions;
|
||||
const modifyRecord = core.modifyLayout(layout);
|
||||
const data = core.getData() as Data;
|
||||
core.trigger(coreEventKeys.CHANGE, { data, type: 'modifyLayout', modifyRecord });
|
||||
}
|
||||
34
packages/idraw/src/methods/reset.ts
Normal file
34
packages/idraw/src/methods/reset.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
import type { IDrawSettings, IDrawStorage } from '@idraw/types';
|
||||
import { Core } from '@idraw/core';
|
||||
import { Store } from '@idraw/util';
|
||||
import { IDrawEvent } from '../event';
|
||||
import { parseSettings } from '../setting/config';
|
||||
import { changeMode } from '../setting/mode';
|
||||
import { changeStyles } from '../setting/style';
|
||||
|
||||
export function reset(
|
||||
depOptions: { core: Core<IDrawEvent>; store: Store<IDrawStorage> },
|
||||
opts: IDrawSettings
|
||||
): IDrawSettings {
|
||||
const { core, store } = depOptions;
|
||||
const { mode, styles } = parseSettings(opts);
|
||||
let needFresh = false;
|
||||
const newOpts: IDrawSettings = {};
|
||||
store.clear();
|
||||
if (mode) {
|
||||
changeMode(mode, core, store);
|
||||
newOpts.mode = mode;
|
||||
needFresh = true;
|
||||
}
|
||||
if (styles) {
|
||||
changeStyles(styles, core, store);
|
||||
newOpts.styles = styles;
|
||||
needFresh = true;
|
||||
}
|
||||
|
||||
if (needFresh === true) {
|
||||
core.refresh();
|
||||
}
|
||||
|
||||
return newOpts;
|
||||
}
|
||||
221
packages/idraw/src/middlewares/use-history.ts
Normal file
221
packages/idraw/src/middlewares/use-history.ts
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import type {
|
||||
Middleware,
|
||||
ModifyRecord,
|
||||
Element,
|
||||
DataLayout,
|
||||
RecursivePartial,
|
||||
DataGlobal,
|
||||
IDrawHistory
|
||||
} from '@idraw/types';
|
||||
import { unflatObject, calcResultMovePosition } from '@idraw/util';
|
||||
import type { IDrawEvent } from '../event';
|
||||
import { eventKeys } from '../event';
|
||||
import type { iDraw } from '../idraw';
|
||||
|
||||
const supportRecordTypes = [
|
||||
'updateElement',
|
||||
'modifyElement',
|
||||
'deleteElement',
|
||||
'moveElement',
|
||||
'addElement',
|
||||
'dragElement',
|
||||
'resizeElement',
|
||||
'dragLayout',
|
||||
'modifyLayout',
|
||||
'modifyGlobal'
|
||||
];
|
||||
|
||||
export const useHistory = (opts: { instance: iDraw }) => {
|
||||
const { instance } = opts;
|
||||
const core = instance.getCore();
|
||||
let doRecords: ModifyRecord[] = [];
|
||||
let undoRecords: ModifyRecord[] = [];
|
||||
|
||||
const doAction = (record: ModifyRecord) => {
|
||||
doRecords.push(record);
|
||||
};
|
||||
|
||||
const undoAction = () => {
|
||||
if (doRecords?.length > 0) {
|
||||
const record = doRecords.pop();
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
let undoRecord: ModifyRecord = { ...record };
|
||||
if (record.content.method === 'modifyElement') {
|
||||
const info = unflatObject(record.content.before || {});
|
||||
undoRecord = core.modifyElement({
|
||||
...info,
|
||||
uuid: (record as ModifyRecord<'modifyElement'>).content?.uuid
|
||||
}) as ModifyRecord;
|
||||
} else if (record.content.method === 'updateElement') {
|
||||
const info = unflatObject(record.content.before || {}) as Element;
|
||||
undoRecord = core.updateElement({ ...info, uuid: record.content.uuid }) as ModifyRecord;
|
||||
} else if (record.content.method === 'addElement') {
|
||||
const uuid = record.content.uuid;
|
||||
undoRecord = core.deleteElement(uuid) as ModifyRecord;
|
||||
} else if (record.content.method === 'deleteElement') {
|
||||
const { element, position } = record.content;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
undoRecord = core.addElement(element, { position }) as ModifyRecord;
|
||||
} else if (record.content.method === 'moveElement') {
|
||||
const uuid = record.content.uuid;
|
||||
const moveResult = calcResultMovePosition({
|
||||
from: record.content.from,
|
||||
to: record.content.to
|
||||
});
|
||||
if (!moveResult) {
|
||||
return;
|
||||
}
|
||||
undoRecord = core.moveElement(uuid, moveResult.from) as ModifyRecord;
|
||||
} else if (record.content.method === 'modifyLayout') {
|
||||
const info =
|
||||
record.content.before === null
|
||||
? null
|
||||
: (unflatObject(record.content.before || {}) as RecursivePartial<DataLayout>);
|
||||
undoRecord = core.modifyLayout(info) as ModifyRecord;
|
||||
} else if (record.content.method === 'modifyGlobal') {
|
||||
const info =
|
||||
record.content.before === null
|
||||
? null
|
||||
: (unflatObject(record.content.before || {}) as RecursivePartial<DataGlobal>);
|
||||
undoRecord = core.modifyGlobal(info) as ModifyRecord;
|
||||
} else if (record.content.method === 'modifyElements') {
|
||||
undoRecord = core.modifyElements(
|
||||
record.content.before.forEach((item) => unflatObject(item)) as unknown as Array<
|
||||
RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
|
||||
>
|
||||
) as ModifyRecord;
|
||||
}
|
||||
|
||||
undoRecord = { ...undoRecord, type: 'undo' } as ModifyRecord<'undo'>;
|
||||
undoRecords.push(undoRecord);
|
||||
}
|
||||
};
|
||||
|
||||
const redoAction = () => {
|
||||
if (undoRecords?.length > 0) {
|
||||
const record = undoRecords.pop();
|
||||
if (!record) {
|
||||
return;
|
||||
}
|
||||
let redoRecord: ModifyRecord = { ...record };
|
||||
if (record.content.method === 'modifyElement') {
|
||||
const info = unflatObject(record.content.before || {});
|
||||
redoRecord = core.modifyElement({
|
||||
...info,
|
||||
uuid: (record as ModifyRecord<'modifyElement'>).content.uuid
|
||||
}) as ModifyRecord;
|
||||
} else if (record.content.method === 'updateElement') {
|
||||
const info = unflatObject(record.content.before || {}) as Element;
|
||||
redoRecord = core.updateElement({ ...info, uuid: record.content.uuid }) as ModifyRecord;
|
||||
} else if (record.content.method === 'addElement') {
|
||||
const uuid = record.content.uuid;
|
||||
redoRecord = core.deleteElement(uuid) as ModifyRecord;
|
||||
} else if (record.content.method === 'deleteElement') {
|
||||
const { element, position } = record.content;
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
redoRecord = core.addElement(element, { position }) as ModifyRecord;
|
||||
} else if (record.content.method === 'moveElement') {
|
||||
const uuid = record.content.uuid;
|
||||
const moveResult = calcResultMovePosition({
|
||||
from: record.content.from,
|
||||
to: record.content.to
|
||||
});
|
||||
if (!moveResult) {
|
||||
return;
|
||||
}
|
||||
redoRecord = core.moveElement(uuid, moveResult.from) as ModifyRecord;
|
||||
} else if (record.content.method === 'modifyLayout') {
|
||||
const info =
|
||||
record.content.before === null
|
||||
? null
|
||||
: (unflatObject(record.content.before || {}) as RecursivePartial<DataLayout>);
|
||||
redoRecord = core.modifyLayout(info) as ModifyRecord;
|
||||
} else if (record.content.method === 'modifyGlobal') {
|
||||
const info =
|
||||
record.content.before === null
|
||||
? null
|
||||
: (unflatObject(record.content.before || {}) as RecursivePartial<DataGlobal>);
|
||||
redoRecord = core.modifyGlobal(info) as ModifyRecord;
|
||||
} else if (record.content.method === 'modifyElements') {
|
||||
redoRecord = core.modifyElements(
|
||||
record.content.before.forEach((item) => unflatObject(item)) as unknown as Array<
|
||||
RecursivePartial<Omit<Element, 'uuid'>> & Pick<Element, 'uuid'>
|
||||
>
|
||||
) as ModifyRecord;
|
||||
}
|
||||
redoRecord = { ...redoRecord, type: 'redo' } as ModifyRecord<'redo'>;
|
||||
doRecords.push(redoRecord);
|
||||
}
|
||||
};
|
||||
|
||||
const MiddlewareHistory: Middleware<any, IDrawEvent> = (opts) => {
|
||||
const { eventHub } = opts;
|
||||
const changeEvent = (e: IDrawEvent['change']) => {
|
||||
const { modifyRecord } = e;
|
||||
if (modifyRecord && supportRecordTypes.includes(modifyRecord?.type)) {
|
||||
doAction(modifyRecord);
|
||||
}
|
||||
};
|
||||
|
||||
const onEvents = () => {
|
||||
eventHub.on(eventKeys.CHANGE, changeEvent);
|
||||
};
|
||||
|
||||
const offEvents = () => {
|
||||
eventHub.off(eventKeys.CHANGE, changeEvent);
|
||||
};
|
||||
|
||||
return {
|
||||
name: '@middleware/history',
|
||||
use() {
|
||||
onEvents();
|
||||
},
|
||||
disuse() {
|
||||
offEvents();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
clear();
|
||||
doRecords = null as any;
|
||||
undoRecords = null as any;
|
||||
};
|
||||
|
||||
const clear = () => {
|
||||
while (doRecords?.length > 0) {
|
||||
doRecords.pop();
|
||||
}
|
||||
while (undoRecords?.length > 0) {
|
||||
undoRecords.pop();
|
||||
}
|
||||
};
|
||||
|
||||
const getDoRecords = () => doRecords;
|
||||
const getUndoRecords = () => undoRecords;
|
||||
|
||||
const history: IDrawHistory = {
|
||||
undo: undoAction,
|
||||
redo: redoAction,
|
||||
destroy,
|
||||
clear,
|
||||
canUndo: () => doRecords.length > 0,
|
||||
canRedo: () => undoRecords.length > 0,
|
||||
__getDoRecords: getDoRecords,
|
||||
__getUndoRecords: getUndoRecords
|
||||
};
|
||||
|
||||
return {
|
||||
MiddlewareHistory,
|
||||
history
|
||||
} as const;
|
||||
};
|
||||
|
|
@ -3,8 +3,9 @@ import { istype } from '@idraw/util';
|
|||
|
||||
export const defaultMode: IDrawMode = 'select';
|
||||
|
||||
export const defaultSettings: Required<Pick<IDrawSettings, 'mode'>> = {
|
||||
mode: defaultMode
|
||||
export const defaultSettings: Required<Pick<IDrawSettings, 'mode' | 'history'>> = {
|
||||
mode: defaultMode,
|
||||
history: false
|
||||
};
|
||||
|
||||
export const defaultOptions: Required<Pick<IDrawOptions, 'devicePixelRatio'>> = {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ function isValidMode(mode: string | IDrawMode) {
|
|||
}
|
||||
|
||||
export function runMiddlewares(core: Core<IDrawEvent>, store: Store<IDrawStorage>) {
|
||||
const { enableRuler, enableScale, enableScroll, enableSelect, enableTextEdit, enableDrag, enableInfo } = store.getSnapshot();
|
||||
const { enableRuler, enableScale, enableScroll, enableSelect, enableTextEdit, enableDrag, enableInfo } =
|
||||
store.getSnapshot();
|
||||
const styles = store.get('middlewareStyles');
|
||||
if (enableScroll === true) {
|
||||
core.use(MiddlewareScroller, styles?.scroller);
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import type {
|
|||
ViewSizeInfo,
|
||||
VirtualFlatStorage,
|
||||
ViewRectInfo,
|
||||
ModifyOptions,
|
||||
ModifyInfo,
|
||||
VirtualFlatItem
|
||||
} from '@idraw/types';
|
||||
import {
|
||||
|
|
@ -25,6 +25,7 @@ import {
|
|||
} from '@idraw/util';
|
||||
import { sortElementsViewVisiableInfoMap, updateVirtualFlatItemMapStatus } from './view-visible';
|
||||
import { calcVirtualFlatDetail } from './virtual-flat';
|
||||
import { calcVirtualTextDetail } from './virtual-flat/text';
|
||||
|
||||
export class Calculator implements ViewCalculator {
|
||||
#opts: ViewCalculatorOptions;
|
||||
|
|
@ -169,20 +170,35 @@ export class Calculator implements ViewCalculator {
|
|||
return viewRectInfo;
|
||||
}
|
||||
|
||||
modifyText(element: Element<'text'>): void {
|
||||
const virtualFlatItemMap = this.#store.get('virtualFlatItemMap');
|
||||
const flatItem = virtualFlatItemMap[element.uuid];
|
||||
if (element && element.type === 'text') {
|
||||
const newVirtualFlatItem: VirtualFlatItem = {
|
||||
...flatItem,
|
||||
...calcVirtualTextDetail(element, {
|
||||
tempContext: this.#opts.tempContext
|
||||
})
|
||||
};
|
||||
virtualFlatItemMap[element.uuid] = newVirtualFlatItem;
|
||||
this.#store.set('virtualFlatItemMap', virtualFlatItemMap);
|
||||
}
|
||||
}
|
||||
|
||||
modifyVirtualFlatItemMap(
|
||||
data: Data,
|
||||
opts: {
|
||||
modifyOptions: ModifyOptions; // TODO
|
||||
modifyInfo: ModifyInfo; // TODO
|
||||
viewScaleInfo: ViewScaleInfo;
|
||||
viewSizeInfo: ViewSizeInfo;
|
||||
}
|
||||
): void {
|
||||
const { modifyOptions, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { type, content } = modifyOptions;
|
||||
const { modifyInfo, viewScaleInfo, viewSizeInfo } = opts;
|
||||
const { type, content } = modifyInfo;
|
||||
const list = data.elements;
|
||||
const virtualFlatItemMap = this.#store.get('virtualFlatItemMap');
|
||||
if (type === 'deleteElement') {
|
||||
const { element } = content as ModifyOptions<'deleteElement'>['content'];
|
||||
const { element } = content as ModifyInfo<'deleteElement'>['content'];
|
||||
const uuids: string[] = [];
|
||||
const _walk = (e: Element) => {
|
||||
uuids.push(e.uuid);
|
||||
|
|
@ -203,7 +219,7 @@ export class Calculator implements ViewCalculator {
|
|||
// this.resetVirtualFlatItemMap(data, { viewScaleInfo, viewSizeInfo });
|
||||
// }
|
||||
else if (type === 'addElement' || type === 'updateElement') {
|
||||
const { position } = content as ModifyOptions<'addElement'>['content'];
|
||||
const { position } = content as ModifyInfo<'addElement'>['content'];
|
||||
const element = findElementFromListByPosition(position, data.elements);
|
||||
const groupQueue = getGroupQueueByElementPosition(list, position);
|
||||
if (element) {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ function drawClipPath(
|
|||
ctx.scale(totalScale * scaleW, totalScale * scaleH);
|
||||
const pathStr = generateSVGPath(clipPath.commands || []);
|
||||
const path2d = new Path2D(pathStr);
|
||||
ctx.clip(path2d);
|
||||
ctx.clip(path2d, 'nonzero');
|
||||
|
||||
ctx.translate(0 - (internalX as number), 0 - (internalY as number));
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
|
|
@ -236,7 +236,7 @@ export function drawBoxBackground(
|
|||
}
|
||||
}
|
||||
}
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
|
||||
if (transform && transform.length > 0) {
|
||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||
|
|
@ -322,7 +322,7 @@ export function drawBoxBorder(
|
|||
// ctx.quadraticCurveTo(op3.x, op3.y, op3e.x, op3e.y);
|
||||
// ctx.lineTo(op0s.x, op0s.y);
|
||||
// ctx.closePath();
|
||||
// ctx.fill();
|
||||
// ctx.fill('nonzero');
|
||||
|
||||
// ctx.fillStyle = '#000000';
|
||||
// ctx.globalCompositeOperation = 'destination-out';
|
||||
|
|
@ -337,7 +337,7 @@ export function drawBoxBorder(
|
|||
// ctx.quadraticCurveTo(ip3.x, ip3.y, ip3e.x, ip3e.y);
|
||||
// ctx.lineTo(ip0s.x, ip0s.y);
|
||||
// ctx.closePath();
|
||||
// ctx.fill();
|
||||
// ctx.fill('nonzero');
|
||||
// ctx.globalCompositeOperation = 'source-over';
|
||||
// return;
|
||||
// // TODO
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function drawCircle(ctx: ViewContext2D, elem: Element<'circle'>, opts: Re
|
|||
ctx.fillStyle = fillStyle;
|
||||
ctx.circle(centerX, centerY, radiusA, radiusB, 0, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.fill('nonzero');
|
||||
ctx.globalAlpha = parentOpacity;
|
||||
|
||||
// draw border
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import type { RendererDrawElementOptions, ViewContext2D, ElementGlobalDetail } from '@idraw/types';
|
||||
import type { RendererDrawElementOptions, ViewContext2D, ElementGlobal } from '@idraw/types';
|
||||
|
||||
export function drawGlobalBackground(ctx: ViewContext2D, global: ElementGlobalDetail | undefined, opts: RendererDrawElementOptions) {
|
||||
export function drawGlobalBackground(
|
||||
ctx: ViewContext2D,
|
||||
global: ElementGlobal | undefined,
|
||||
opts: RendererDrawElementOptions
|
||||
) {
|
||||
if (typeof global?.background === 'string') {
|
||||
const { viewSizeInfo } = opts;
|
||||
const { width, height } = viewSizeInfo;
|
||||
|
|
|
|||
|
|
@ -110,8 +110,8 @@ export function drawGroup(ctx: ViewContext2D, elem: Element<'group'>, opts: Rend
|
|||
ctx.arcTo(x, y + h, x, y, radiusList[3]);
|
||||
ctx.arcTo(x, y, x + w, y, radiusList[0]);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.clip();
|
||||
ctx.fill('nonzero');
|
||||
ctx.clip('nonzero');
|
||||
}
|
||||
|
||||
if (Array.isArray(elem.detail.children)) {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: Rend
|
|||
ctx.arcTo(x, y + h, x, y, radiusList[3]);
|
||||
ctx.arcTo(x, y, x + w, y, radiusList[0]);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.clip();
|
||||
ctx.fill('nonzero');
|
||||
ctx.clip('nonzero');
|
||||
|
||||
if (scaleMode && originH && originW) {
|
||||
let sx = 0;
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ export function drawLayout(
|
|||
ctx.arcTo(x, y + h, x, y, radiusList[3]);
|
||||
ctx.arcTo(x, y, x + w, y, radiusList[0]);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
ctx.clip();
|
||||
ctx.fill('nonzero');
|
||||
ctx.clip('nonzero');
|
||||
}
|
||||
|
||||
renderContent(ctx);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
import type { Element, RendererDrawElementOptions, ViewContext2D, LinearGradientColor, RadialGradientColor } from '@idraw/types';
|
||||
import type {
|
||||
Element,
|
||||
RendererDrawElementOptions,
|
||||
ViewContext2D,
|
||||
LinearGradientColor,
|
||||
RadialGradientColor
|
||||
} from '@idraw/types';
|
||||
import { rotateElement, generateSVGPath, calcViewElementSize } from '@idraw/util';
|
||||
import { drawBox, drawBoxShadow } from './box';
|
||||
|
||||
|
|
@ -23,7 +29,11 @@ export function drawPath(ctx: ViewContext2D, elem: Element<'path'>, opts: Render
|
|||
let boxOriginElem = { ...elem };
|
||||
boxOriginElem.detail = restDetail;
|
||||
|
||||
if (detail.fill && detail.fill !== 'string' && (detail.fill as LinearGradientColor | RadialGradientColor)?.type?.includes('gradient')) {
|
||||
if (
|
||||
detail.fill &&
|
||||
detail.fill !== 'string' &&
|
||||
(detail.fill as LinearGradientColor | RadialGradientColor)?.type?.includes('gradient')
|
||||
) {
|
||||
boxViewElem = {
|
||||
...viewElem,
|
||||
...{
|
||||
|
|
@ -65,7 +75,7 @@ export function drawPath(ctx: ViewContext2D, elem: Element<'path'>, opts: Render
|
|||
// ctx.lineTo(viewOriginX + w, viewOriginY + h);
|
||||
// ctx.lineTo(viewOriginX, viewOriginY + h);
|
||||
// ctx.closePath();
|
||||
// ctx.clip();
|
||||
// ctx.clip('nonzero');
|
||||
ctx.scale((scaleNum * scaleW) / viewScaleInfo.scale, (scaleNum * scaleH) / viewScaleInfo.scale);
|
||||
const pathStr = generateSVGPath(detail.commands || []);
|
||||
const path2d = new Path2D(pathStr);
|
||||
|
|
@ -79,7 +89,7 @@ export function drawPath(ctx: ViewContext2D, elem: Element<'path'>, opts: Render
|
|||
}
|
||||
|
||||
if (detail.fill) {
|
||||
ctx.fill(path2d, fillRule as CanvasFillRule);
|
||||
ctx.fill(path2d, (fillRule as CanvasFillRule) || 'nonzero');
|
||||
}
|
||||
|
||||
if (detail.stroke && detail.strokeWidth !== 0) {
|
||||
|
|
|
|||
|
|
@ -70,6 +70,14 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
|
|||
// ...opts
|
||||
// });
|
||||
// }
|
||||
|
||||
if (opts.forceDrawAll === true) {
|
||||
this.#calculator.resetVirtualFlatItemMap(data, {
|
||||
viewScaleInfo: opts.viewScaleInfo,
|
||||
viewSizeInfo: opts.viewSizeInfo
|
||||
});
|
||||
}
|
||||
|
||||
const drawOpts = {
|
||||
loader,
|
||||
calculator,
|
||||
|
|
|
|||
|
|
@ -131,9 +131,9 @@ export function calcVirtualTextDetail(elem: Element<'text'>, opts: CalcVirtualDe
|
|||
eachLineStartY = (fontHeight - fontSize) / 2;
|
||||
}
|
||||
if (lines.length * fontHeight < h) {
|
||||
if (elem.detail.verticalAlign === 'top') {
|
||||
if (detail.verticalAlign === 'top') {
|
||||
startY = 0;
|
||||
} else if (elem.detail.verticalAlign === 'bottom') {
|
||||
} else if (detail.verticalAlign === 'bottom') {
|
||||
startY += h - lines.length * fontHeight;
|
||||
} else {
|
||||
// middle and default
|
||||
|
|
|
|||
|
|
@ -18,4 +18,5 @@ export * from './lib/html';
|
|||
export * from './lib/svg-path';
|
||||
export * from './lib/config';
|
||||
export * from './lib/modify';
|
||||
export * from './lib/modify-info';
|
||||
export * from './lib/virtual-flat';
|
||||
|
|
|
|||
|
|
@ -143,22 +143,6 @@ export interface BoardViewer extends UtilEventEmitter<BoardViewerEventMap> {
|
|||
scroll(opts: { moveX?: number; moveY?: number; ignoreUpdateVisibleStatus?: boolean }): ViewScaleInfo;
|
||||
resize(viewSize: Partial<ViewSizeInfo>, opts?: { ignoreUpdateVisibleStatus?: boolean }): ViewSizeInfo;
|
||||
updateViewScaleInfo(opts: { scale: number; offsetX: number; offsetY: number }): ViewScaleInfo;
|
||||
|
||||
// resetVirtualFlatItemMap(
|
||||
// data: Data,
|
||||
// opts: {
|
||||
// viewScaleInfo: ViewScaleInfo;
|
||||
// viewSizeInfo: ViewSizeInfo;
|
||||
// }
|
||||
// ): void;
|
||||
// modifyVirtualFlatItemMap(
|
||||
// data: Data,
|
||||
// opts: {
|
||||
// modifyOptions: ModifyOptions;
|
||||
// viewScaleInfo: ViewScaleInfo;
|
||||
// viewSizeInfo: ViewSizeInfo;
|
||||
// }
|
||||
// ): void;
|
||||
}
|
||||
|
||||
export interface BoardRenderer extends UtilEventEmitter<RendererEventMap> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { Element, ElementSize, ElementType, ElementPosition } from './eleme
|
|||
import type { ViewScaleInfo } from './view';
|
||||
import type { Data } from './data';
|
||||
import type { BoardBaseEventMap } from './board';
|
||||
import type { ModifyType, ModifyRecord } from './modify';
|
||||
|
||||
export interface CoreOptions {
|
||||
width: number;
|
||||
|
|
@ -32,24 +33,12 @@ export interface CoreEventCursor {
|
|||
// uuids: string[];
|
||||
// positions?: Array<Array<number>>;
|
||||
// }
|
||||
export interface CoreEventChange {
|
||||
export interface CoreEventChange<T extends ModifyType = ModifyType> {
|
||||
data: Data;
|
||||
type:
|
||||
| 'updateElement'
|
||||
| 'deleteElement'
|
||||
| 'moveElement'
|
||||
| 'addElement'
|
||||
| 'dragElement'
|
||||
| 'resizeElement'
|
||||
| 'setData'
|
||||
| 'undo'
|
||||
| 'redo'
|
||||
| 'dragLayout'
|
||||
| 'updateLayout'
|
||||
| 'updateElementName'
|
||||
| 'other';
|
||||
type: T | 'setData' | 'other' | string;
|
||||
selectedElements?: Element[] | null;
|
||||
hoverElement?: Element | null;
|
||||
modifyRecord?: ModifyRecord<T>;
|
||||
}
|
||||
export interface CoreEventScale {
|
||||
scale: number;
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export type DataLayout = Pick<ElementSize, 'x' | 'y' | 'w' | 'h'> & {
|
|||
};
|
||||
};
|
||||
|
||||
export interface DataGlobalDetail {
|
||||
export interface DataGlobal {
|
||||
background?: string;
|
||||
}
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export type Data<E extends Record<string, any> = Record<string, any>> = {
|
|||
elements: Element<ElementType, E>[];
|
||||
assets?: ElementAssets;
|
||||
layout?: DataLayout;
|
||||
global?: DataGlobalDetail;
|
||||
global?: DataGlobal;
|
||||
};
|
||||
|
||||
export type Matrix = [
|
||||
|
|
|
|||
|
|
@ -178,19 +178,19 @@ export interface ElementOperations {
|
|||
lastModified?: number;
|
||||
}
|
||||
|
||||
export interface ElementGlobalDetail {
|
||||
export interface ElementGlobal {
|
||||
background?: string;
|
||||
}
|
||||
|
||||
export interface Element<T extends ElementType = ElementType, E extends Record<string, any> = Record<string, any>>
|
||||
extends ElementSize {
|
||||
uuid: string;
|
||||
name?: string;
|
||||
name?: string | null;
|
||||
type: T;
|
||||
detail: ElementDetailMap[T];
|
||||
operations?: ElementOperations;
|
||||
extends?: E;
|
||||
// global?: ElementGlobalDetail;
|
||||
// global?: ElementGlobal;
|
||||
}
|
||||
|
||||
export type Elements = Element<ElementType>[];
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { ModifyRecord } from './modify';
|
||||
import type { CoreOptions } from './core';
|
||||
import type { MiddlewareSelectorStyle, MiddlewareInfoStyle, MiddlewareRulerStyle, MiddlewareScrollerStyle, MiddlewareLayoutSelectorStyle } from './middleware';
|
||||
import type {
|
||||
MiddlewareSelectorStyle,
|
||||
MiddlewareInfoStyle,
|
||||
MiddlewareRulerStyle,
|
||||
MiddlewareScrollerStyle,
|
||||
MiddlewareLayoutSelectorStyle
|
||||
} from './middleware';
|
||||
|
||||
export type IDrawMode = 'select' | 'drag' | 'readOnly';
|
||||
|
||||
|
|
@ -14,10 +21,22 @@ export interface IDrawSettings {
|
|||
scroller?: Partial<MiddlewareScrollerStyle>;
|
||||
layoutSelector?: Partial<MiddlewareLayoutSelectorStyle>;
|
||||
};
|
||||
history?: boolean;
|
||||
}
|
||||
|
||||
export type IDrawOptions = CoreOptions & IDrawSettings;
|
||||
|
||||
export type IDrawHistory = {
|
||||
undo: () => void;
|
||||
redo: () => void;
|
||||
canUndo: () => void;
|
||||
canRedo: () => void;
|
||||
destroy: () => void;
|
||||
clear: () => void;
|
||||
__getDoRecords: () => ModifyRecord[];
|
||||
__getUndoRecords: () => ModifyRecord[];
|
||||
};
|
||||
|
||||
export interface IDrawStorage {
|
||||
mode: IDrawMode;
|
||||
enableRuler: boolean;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import type { BoardMiddlewareObject, BoardMiddleware } from './board';
|
||||
import type { BoardExtendEventMap, BoardMiddlewareObject, BoardMiddleware } from './board';
|
||||
|
||||
export type Middleware = BoardMiddleware;
|
||||
export type Middleware<
|
||||
S extends Record<any | symbol, any> = any,
|
||||
E extends BoardExtendEventMap = BoardExtendEventMap,
|
||||
C extends any = undefined
|
||||
> = BoardMiddleware<S, E, C>;
|
||||
|
||||
export type MiddlewareObject = BoardMiddlewareObject;
|
||||
|
||||
|
|
|
|||
22
packages/types/src/lib/modify-info.ts
Normal file
22
packages/types/src/lib/modify-info.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import type { Element, ElementPosition } from './element';
|
||||
import type { RecursivePartial } from './util';
|
||||
|
||||
type ModifyInfoType = 'updateElement' | 'addElement' | 'deleteElement' | 'moveElement';
|
||||
|
||||
type ModifiedElement = Omit<RecursivePartial<Element>, 'uuid'>;
|
||||
|
||||
interface ModifyInfoContentMap {
|
||||
updateElement: {
|
||||
position: ElementPosition;
|
||||
beforeModifiedElement: ModifiedElement;
|
||||
afterModifiedElement: ModifiedElement;
|
||||
};
|
||||
addElement: { position: ElementPosition; element: Element };
|
||||
deleteElement: { position: ElementPosition; element: Element };
|
||||
moveElement: { from: ElementPosition; to: ElementPosition };
|
||||
}
|
||||
|
||||
export interface ModifyInfo<T extends ModifyInfoType = ModifyInfoType> {
|
||||
type: T;
|
||||
content: ModifyInfoContentMap[T];
|
||||
}
|
||||
|
|
@ -1,43 +1,196 @@
|
|||
import type { Element, ElementPosition } from './element';
|
||||
import type { RecursivePartial } from './util';
|
||||
import type { Element, ElementPosition, ElementOperations } from './element';
|
||||
import type { DataGlobal } from './data';
|
||||
|
||||
export type ModifyType = 'updateElement' | 'addElement' | 'deleteElement' | 'moveElement';
|
||||
export type ModifyMethod =
|
||||
| 'updateElement'
|
||||
| 'modifyElement'
|
||||
| 'deleteElement'
|
||||
| 'moveElement'
|
||||
| 'addElement'
|
||||
| 'dragElement'
|
||||
| 'dragElements'
|
||||
| 'modifyElements'
|
||||
| 'dragLayout'
|
||||
| 'modifyLayout'
|
||||
| 'modifyGlobal';
|
||||
|
||||
export type ModifiedElement = Omit<RecursivePartial<Element>, 'uuid'>;
|
||||
export type ModifyType = ModifyMethod | 'undo' | 'redo';
|
||||
|
||||
export type ModifiedTargetElement = ModifiedElement & { uuid: string };
|
||||
/**
|
||||
* FlattenElement
|
||||
For example:
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
"h": 0,
|
||||
"detail.color": "#FFFFFF",
|
||||
"detail.borderWidth[0]": 10,
|
||||
"detail.borderWidth[1]": 20,
|
||||
"detail.borderWidth[2]": 30,
|
||||
"detail.borderWidth[3]": 40,
|
||||
}
|
||||
*/
|
||||
export type FlattenElement = Record<string, string | number | undefined | null>;
|
||||
|
||||
/**
|
||||
* ModifiedLayoutDetail
|
||||
For example:
|
||||
{
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
"h": 0,
|
||||
"detail.color": "#FFFFFF",
|
||||
"detail.borderWidth[0]": 10,
|
||||
"detail.borderWidth[1]": 20,
|
||||
"detail.borderWidth[2]": 30,
|
||||
"detail.borderWidth[3]": 40,
|
||||
}
|
||||
*/
|
||||
export type FlattenLayout = Record<string, string | number | undefined | null>;
|
||||
|
||||
/**
|
||||
* FlattenGlobal
|
||||
For example:
|
||||
{
|
||||
"background": "#FFFFFF",
|
||||
}
|
||||
*/
|
||||
export type FlattenGlobal = Partial<DataGlobal>;
|
||||
|
||||
export type ModifiedElementOperations = Partial<ElementOperations>;
|
||||
|
||||
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 ModifyOptions<T extends ModifyType = ModifyType> {
|
||||
type: T;
|
||||
content: ModifyContentMap[T];
|
||||
updateElement: {
|
||||
method: 'updateElement';
|
||||
uuid: string;
|
||||
before: FlattenElement | null;
|
||||
after: FlattenElement | null;
|
||||
};
|
||||
modifyElement: {
|
||||
method: 'modifyElement';
|
||||
uuid: string;
|
||||
before: FlattenElement | null;
|
||||
after: FlattenElement | null;
|
||||
};
|
||||
addElement: {
|
||||
method: 'addElement';
|
||||
uuid: string;
|
||||
position: ElementPosition;
|
||||
element: Element;
|
||||
};
|
||||
deleteElement: {
|
||||
method: 'deleteElement';
|
||||
uuid: string;
|
||||
position: ElementPosition;
|
||||
element: Element | null;
|
||||
};
|
||||
moveElement: {
|
||||
method: 'moveElement';
|
||||
uuid: string;
|
||||
from: ElementPosition;
|
||||
to: ElementPosition;
|
||||
};
|
||||
dragElement: {
|
||||
method: 'modifyElement';
|
||||
uuid: string;
|
||||
before: FlattenElement | null;
|
||||
after: FlattenElement | null;
|
||||
};
|
||||
dragElements: {
|
||||
method: 'modifyElements';
|
||||
before: (FlattenLayout & { uuid: string })[];
|
||||
after: (FlattenLayout & { uuid: string })[];
|
||||
};
|
||||
dragLayout: {
|
||||
method: 'modifyElement';
|
||||
before: FlattenLayout;
|
||||
after: FlattenLayout;
|
||||
};
|
||||
modifyLayout: {
|
||||
method: 'modifyLayout';
|
||||
before: FlattenLayout | null;
|
||||
after: FlattenLayout | null;
|
||||
};
|
||||
modifyElements: {
|
||||
method: 'modifyElements';
|
||||
before: (FlattenLayout & { uuid: string })[];
|
||||
after: (FlattenLayout & { uuid: string })[];
|
||||
};
|
||||
modifyGlobal: {
|
||||
method: 'modifyGlobal';
|
||||
before: FlattenGlobal | null;
|
||||
after: FlattenGlobal | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ModifyRecordMap {
|
||||
updateElement: {
|
||||
type: 'updateElement';
|
||||
time: number;
|
||||
} & Required<ModifyContentMap['updateElement']>;
|
||||
content: ModifyContentMap['updateElement'];
|
||||
};
|
||||
modifyElement: {
|
||||
type: 'modifyElement';
|
||||
time: number;
|
||||
content: ModifyContentMap['modifyElement'];
|
||||
};
|
||||
addElement: {
|
||||
type: 'addElement';
|
||||
time: number;
|
||||
} & Required<ModifyContentMap['addElement']>;
|
||||
content: ModifyContentMap['addElement'];
|
||||
};
|
||||
deleteElement: {
|
||||
type: 'deleteElement';
|
||||
time: number;
|
||||
} & Required<ModifyContentMap['deleteElement']>;
|
||||
content: ModifyContentMap['deleteElement'];
|
||||
};
|
||||
moveElement: {
|
||||
type: 'moveElement';
|
||||
time: number;
|
||||
afterModifiedFrom: ElementPosition;
|
||||
afterModifiedTo: ElementPosition;
|
||||
} & Required<ModifyContentMap['moveElement']>;
|
||||
content: ModifyContentMap['moveElement'];
|
||||
};
|
||||
dragElement: {
|
||||
type: 'dragElement';
|
||||
time: number;
|
||||
content: ModifyContentMap['modifyElement'];
|
||||
};
|
||||
dragElements: {
|
||||
type: 'dragElements';
|
||||
time: number;
|
||||
content: ModifyContentMap['modifyElements'];
|
||||
};
|
||||
modifyElements: {
|
||||
type: 'modifyElements';
|
||||
time: number;
|
||||
content: ModifyContentMap['modifyElements'];
|
||||
};
|
||||
dragLayout: {
|
||||
type: 'dragLayout';
|
||||
time: number;
|
||||
content: ModifyContentMap['dragLayout'];
|
||||
};
|
||||
modifyLayout: {
|
||||
type: 'modifyLayout';
|
||||
time: number;
|
||||
content: ModifyContentMap['modifyLayout'];
|
||||
};
|
||||
modifyGlobal: {
|
||||
type: 'modifyGlobal';
|
||||
time: number;
|
||||
content: ModifyContentMap['modifyGlobal'];
|
||||
};
|
||||
undo: {
|
||||
type: 'undo';
|
||||
time: number;
|
||||
content: ModifyContentMap[ModifyMethod];
|
||||
};
|
||||
redo: {
|
||||
type: 'redo';
|
||||
time: number;
|
||||
content: ModifyContentMap[ModifyMethod];
|
||||
};
|
||||
}
|
||||
|
||||
export type ModifyRecord<T extends ModifyType = ModifyType> = ModifyRecordMap[T];
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import type { Element, ElementType } from './element';
|
|||
import type { Point, PointSize } from './point';
|
||||
import type { Data } from './data';
|
||||
import type { ViewContext2D } from './context2d';
|
||||
import type { ModifyOptions } from './modify';
|
||||
import type { ModifyInfo } from './modify-info';
|
||||
import { VirtualFlatItem } from './virtual-flat';
|
||||
// import type { BoxInfo } from './box';
|
||||
|
||||
|
|
@ -72,7 +72,7 @@ export interface ViewCalculator {
|
|||
modifyVirtualFlatItemMap(
|
||||
data: Data,
|
||||
opts: {
|
||||
modifyOptions: ModifyOptions;
|
||||
modifyInfo: ModifyInfo;
|
||||
viewScaleInfo: ViewScaleInfo;
|
||||
viewSizeInfo: ViewSizeInfo;
|
||||
}
|
||||
|
|
@ -80,6 +80,7 @@ export interface ViewCalculator {
|
|||
|
||||
toGridNum(num: number, opts?: { ignore?: boolean }): number;
|
||||
getVirtualFlatItem: (uuid: string) => VirtualFlatItem | null;
|
||||
modifyText(element: Element<'text'>): void;
|
||||
}
|
||||
|
||||
export type ViewRectVertexes = [PointSize, PointSize, PointSize, PointSize];
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
toColorHexNum,
|
||||
toColorHexStr,
|
||||
isColorStr
|
||||
} from '../../src/lib/color';
|
||||
|
||||
import { toColorHexNum, toColorHexStr, isColorStr } from '@idraw/util';
|
||||
|
||||
describe('@idraw/util: lib/color', () => {
|
||||
const hex = '#f0f0f0';
|
||||
|
|
@ -23,6 +18,4 @@ describe('@idraw/util: lib/color', () => {
|
|||
const result = isColorStr(hex);
|
||||
expect(result).toStrictEqual(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||
import { Context2D } from './../../src/lib/context2d';
|
||||
import { deepClone } from './../../src/index';
|
||||
import { Context2D, deepClone } from '@idraw/util';
|
||||
import { getData } from './data';
|
||||
|
||||
describe('@idraw/board: src/lib/context', () => {
|
||||
|
|
|
|||
|
|
@ -1,128 +1,92 @@
|
|||
import { deepClone, filterCompactData } from '../../src/lib/data';
|
||||
import { imageBase64, html, svg } from '../_assets/base';
|
||||
import { deepClone, filterCompactData } from '@idraw/util';
|
||||
import type { Data } from '@idraw/types';
|
||||
import { imageBase64, html, svg } from '../_assets/base';
|
||||
|
||||
describe('@idraw/util: lib/data', () => {
|
||||
const json = {
|
||||
num: 123,
|
||||
str: 'abc',
|
||||
bool: true,
|
||||
arr: [
|
||||
{
|
||||
num: 1,
|
||||
str: 'a',
|
||||
bool: false
|
||||
},
|
||||
{
|
||||
num: 2,
|
||||
str: 'b',
|
||||
bool: false
|
||||
const originData: Data = {
|
||||
elements: [
|
||||
{
|
||||
uuid: 'b37213ce-d711-cbb3-51ac-d8081c19f127',
|
||||
type: 'image',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
src: imageBase64
|
||||
}
|
||||
],
|
||||
json: {
|
||||
num: 10,
|
||||
str: 'aaaa',
|
||||
bool: false,
|
||||
json: {
|
||||
num: 11,
|
||||
str: 'bbbb',
|
||||
bool: false
|
||||
},
|
||||
{
|
||||
uuid: '39308517-e10f-76df-43a9-50ed7295e61e',
|
||||
type: 'svg',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
svg: svg
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'ef934ab7-a32e-040c-9ac0-ed193405e6e4',
|
||||
type: 'html',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
html: html
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '063e3a80-1ede-7912-f919-975e34a9bd01',
|
||||
type: 'group',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
children: [
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const json2 = deepClone(json);
|
||||
json2.json.json.num *= 2;
|
||||
|
||||
test('deepClone', async () => {
|
||||
const result = deepClone(json);
|
||||
result.json.json.num *= 2;
|
||||
expect(result).toStrictEqual(json2);
|
||||
});
|
||||
]
|
||||
};
|
||||
|
||||
describe('@idraw/util: data ', () => {
|
||||
test('filterCompactData', () => {
|
||||
const originData: Data = {
|
||||
elements: [
|
||||
{
|
||||
uuid: 'b37213ce-d711-cbb3-51ac-d8081c19f127',
|
||||
type: 'image',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
src: imageBase64
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '39308517-e10f-76df-43a9-50ed7295e61e',
|
||||
type: 'svg',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
svg: svg
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: 'ef934ab7-a32e-040c-9ac0-ed193405e6e4',
|
||||
type: 'html',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
html: html
|
||||
}
|
||||
},
|
||||
{
|
||||
uuid: '063e3a80-1ede-7912-f919-975e34a9bd01',
|
||||
type: 'group',
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: {
|
||||
children: [
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
const data = deepClone(originData);
|
||||
const compactData = filterCompactData(data);
|
||||
|
||||
|
|
@ -135,7 +99,7 @@ describe('@idraw/util: lib/data', () => {
|
|||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: { src: '@assets/1919ff71-124e-2766-23bb-9a251bf3241c' }
|
||||
detail: { src: '@assets/1k7sknuo56gr0h9ug9hs5g5xxgzeee07' }
|
||||
},
|
||||
{
|
||||
uuid: '39308517-e10f-76df-43a9-50ed7295e61e',
|
||||
|
|
@ -144,7 +108,7 @@ describe('@idraw/util: lib/data', () => {
|
|||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: { svg: '@assets/b9b92016-5290-54e8-9668-807574952823' }
|
||||
detail: { svg: '@assets/36jxqyevkyph8yveb6zalsgxj5vc8not' }
|
||||
},
|
||||
{
|
||||
uuid: 'ef934ab7-a32e-040c-9ac0-ed193405e6e4',
|
||||
|
|
@ -153,7 +117,7 @@ describe('@idraw/util: lib/data', () => {
|
|||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: { html: '@assets/34017fa0-2d48-2506-3464-238f34642b5c' }
|
||||
detail: { html: '@assets/cevdw4d1r85ynahctsjex89y03yev87a' }
|
||||
},
|
||||
{
|
||||
uuid: '063e3a80-1ede-7912-f919-975e34a9bd01',
|
||||
|
|
@ -171,7 +135,7 @@ describe('@idraw/util: lib/data', () => {
|
|||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: { src: '@assets/1919ff71-124e-2766-23bb-9a251bf3241c' }
|
||||
detail: { src: '@assets/1k7sknuo56gr0h9ug9hs5g5xxgzeee07' }
|
||||
},
|
||||
{
|
||||
uuid: 'b60e64e8-833e-e112-d7eb-1ab6e7d6870c',
|
||||
|
|
@ -180,7 +144,7 @@ describe('@idraw/util: lib/data', () => {
|
|||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: { svg: '@assets/b9b92016-5290-54e8-9668-807574952823' }
|
||||
detail: { svg: '@assets/36jxqyevkyph8yveb6zalsgxj5vc8not' }
|
||||
},
|
||||
{
|
||||
uuid: '61f2a61e-cdd5-ae36-983f-686ba8e35973',
|
||||
|
|
@ -189,22 +153,22 @@ describe('@idraw/util: lib/data', () => {
|
|||
y: 0,
|
||||
w: 100,
|
||||
h: 100,
|
||||
detail: { html: '@assets/34017fa0-2d48-2506-3464-238f34642b5c' }
|
||||
detail: { html: '@assets/cevdw4d1r85ynahctsjex89y03yev87a' }
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
assets: {
|
||||
'@assets/1919ff71-124e-2766-23bb-9a251bf3241c': {
|
||||
'@assets/1k7sknuo56gr0h9ug9hs5g5xxgzeee07': {
|
||||
type: 'image',
|
||||
value: imageBase64
|
||||
},
|
||||
'@assets/b9b92016-5290-54e8-9668-807574952823': {
|
||||
'@assets/36jxqyevkyph8yveb6zalsgxj5vc8not': {
|
||||
type: 'svg',
|
||||
value: svg
|
||||
},
|
||||
'@assets/34017fa0-2d48-2506-3464-238f34642b5c': {
|
||||
'@assets/cevdw4d1r85ynahctsjex89y03yev87a': {
|
||||
type: 'html',
|
||||
value: html
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,21 @@
|
|||
import {
|
||||
downloadImageFromCanvas
|
||||
} from '../../src/lib/file';
|
||||
|
||||
import { downloadImageFromCanvas } from '@idraw/util';
|
||||
|
||||
describe('@idraw/util: lib/file', () => {
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
const opts = {
|
||||
width: 600,
|
||||
height: 400,
|
||||
}
|
||||
height: 400
|
||||
};
|
||||
ctx.clearRect(0, 0, opts.width, opts.height);
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, opts.width / 2, opts.height / 2);
|
||||
|
||||
test('downloadImageFromCanvas', async () => {
|
||||
test('downloadImageFromCanvas', async () => {
|
||||
downloadImageFromCanvas(canvas, {
|
||||
filename: 'hello',
|
||||
type: 'image/png',
|
||||
});
|
||||
fileName: 'hello',
|
||||
type: 'image/png'
|
||||
});
|
||||
expect(canvas).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
42
packages/util/__tests__/lib/flat-object.test.ts
Normal file
42
packages/util/__tests__/lib/flat-object.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { flatObject } from '@idraw/util';
|
||||
|
||||
describe('flatObject', () => {
|
||||
test('Basic object flattening', () => {
|
||||
expect(flatObject({ a: { b: { c: 1 } } })).toEqual({ 'a.b.c': 1 });
|
||||
});
|
||||
|
||||
test('Array flattening', () => {
|
||||
expect(flatObject({ a: { b: ['c', 'd'] } })).toEqual({
|
||||
'a.b[0]': 'c',
|
||||
'a.b[1]': 'd'
|
||||
});
|
||||
});
|
||||
|
||||
test('Mixed types handling', () => {
|
||||
const date = new Date();
|
||||
expect(
|
||||
flatObject({
|
||||
a: {
|
||||
b: 1,
|
||||
c: [{ d: date }, null]
|
||||
}
|
||||
})
|
||||
).toStrictEqual({
|
||||
'a.b': 1,
|
||||
'a.c[0].d': date,
|
||||
'a.c[1]': null
|
||||
});
|
||||
});
|
||||
|
||||
test('Primitive values handling', () => {
|
||||
expect(flatObject(42 as any)).toEqual({ '': 42 });
|
||||
expect(flatObject('text' as any)).toEqual({ '': 'text' });
|
||||
expect(flatObject(null as any)).toEqual({ '': null });
|
||||
});
|
||||
|
||||
test('Circular reference', () => {
|
||||
const obj: Record<string, any> = { a: 1 };
|
||||
obj.self = obj;
|
||||
expect(() => flatObject(obj)).toThrow();
|
||||
});
|
||||
});
|
||||
114
packages/util/__tests__/lib/get-set.test.ts
Normal file
114
packages/util/__tests__/lib/get-set.test.ts
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
// get-set.test.ts
|
||||
import { get, set, toPath } from '@idraw/util';
|
||||
|
||||
describe('toPath()', () => {
|
||||
test('should handle string path with dots and brackets', () => {
|
||||
expect(toPath('a[0].b.c')).toEqual(['a', '0', 'b', 'c']);
|
||||
expect(toPath('x.y.z')).toEqual(['x', 'y', 'z']);
|
||||
expect(toPath('arr[3].prop')).toEqual(['arr', '3', 'prop']);
|
||||
});
|
||||
|
||||
test('should handle array path', () => {
|
||||
expect(toPath(['a', '0', 'b'])).toEqual(['a', '0', 'b']);
|
||||
});
|
||||
|
||||
test('should handle empty path', () => {
|
||||
expect(toPath('')).toEqual([]);
|
||||
expect(toPath([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
const testObj = {
|
||||
a: {
|
||||
b: {
|
||||
c: 42,
|
||||
d: [null, { e: 'value' }]
|
||||
}
|
||||
},
|
||||
x: null,
|
||||
y: undefined
|
||||
};
|
||||
|
||||
test('should retrieve nested values', () => {
|
||||
expect(get(testObj, 'a.b.c')).toBe(42);
|
||||
expect(get(testObj, ['a', 'b', 'd', '1', 'e'])).toBe('value');
|
||||
expect(get(testObj, 'a.b.d[0]')).toBe(null);
|
||||
});
|
||||
|
||||
test('should handle invalid paths', () => {
|
||||
expect(get(testObj, 'a.b.z')).toBeUndefined();
|
||||
expect(get(testObj, 'x.y.z')).toBeUndefined();
|
||||
expect(get(testObj, 'y.z')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should return defaultValue for missing paths', () => {
|
||||
expect(get(testObj, 'a.missing', 'default')).toBe('default');
|
||||
expect(get({}, 'any.path', 123)).toBe(123);
|
||||
});
|
||||
|
||||
test('should handle edge cases', () => {
|
||||
expect(get(null, 'any.path', 'default')).toBe('default');
|
||||
expect(get(undefined, 'any.path', 'default')).toBe('default');
|
||||
expect(get({ a: 1 }, '')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('set()', () => {
|
||||
test('should set nested values in existing structure', () => {
|
||||
const obj = { a: { b: { c: 1 } } };
|
||||
set(obj, 'a.b.c', 2);
|
||||
expect(obj.a.b.c).toBe(2);
|
||||
});
|
||||
|
||||
test('should create missing path structures', () => {
|
||||
const obj = {};
|
||||
set(obj, 'x[0].y.z', 'value');
|
||||
expect(obj).toEqual({
|
||||
x: [
|
||||
{
|
||||
y: {
|
||||
z: 'value'
|
||||
}
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle array indexes', () => {
|
||||
const obj = { arr: [] };
|
||||
set(obj, 'arr[2].name', 'third');
|
||||
expect(obj.arr).toEqual([, , { name: 'third' }]); // eslint-disable-line no-sparse-arrays
|
||||
});
|
||||
|
||||
test('should overwrite existing primitives', () => {
|
||||
const obj = { a: 1 };
|
||||
set(obj, 'a.b.c', 'new-value');
|
||||
expect(obj).toEqual({ a: { b: { c: 'new-value' } } });
|
||||
});
|
||||
|
||||
test('should handle empty path', () => {
|
||||
const obj = { a: 1 };
|
||||
set(obj, '', 42);
|
||||
expect(obj).toEqual({ a: 1 });
|
||||
});
|
||||
|
||||
test('should handle null/undefined objects', () => {
|
||||
const obj = null;
|
||||
set(obj, 'a.b.c', 'value');
|
||||
expect(obj).toBeNull();
|
||||
});
|
||||
|
||||
test('should create arrays when next key is numeric', () => {
|
||||
const obj: any = {};
|
||||
set(obj, 'arr[1].prop', 'test');
|
||||
expect(Array.isArray(obj.arr)).toBe(true);
|
||||
expect(obj.arr[1].prop).toBe('test');
|
||||
});
|
||||
|
||||
test('should handle intermediate null values', () => {
|
||||
const obj: any = { a: null };
|
||||
set(obj, 'a.b.c', 'value');
|
||||
expect(obj.a.b.c).toBe('value');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||
import { istype } from '../../src/lib/istype';
|
||||
import { istype } from '@idraw/util';
|
||||
|
||||
describe('@idraw/util: lib/istype', () => {
|
||||
const _num = 123;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import '../../../../__tests__/polyfill/image';
|
||||
import { loadHTML, loadImage, loadSVG } from '../../src/lib/load';
|
||||
import { parseHTMLToDataURL, parseSVGToDataURL } from '../../src/lib/parser';
|
||||
import { loadHTML, loadImage, loadSVG } from '@idraw/util';
|
||||
import { parseHTMLToDataURL, parseSVGToDataURL } from '../../src/view/parser';
|
||||
|
||||
describe('@idraw/util: lib/loader', () => {
|
||||
test('loadHTML', async () => {
|
||||
|
|
|
|||
|
|
@ -1,441 +0,0 @@
|
|||
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([]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
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'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
199
packages/util/__tests__/lib/move-element.test.ts
Normal file
199
packages/util/__tests__/lib/move-element.test.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { moveElementPosition, calcRevertMovePosition } from '@idraw/util';
|
||||
import type { Elements } from '@idraw/types';
|
||||
const getElemBase = () => {
|
||||
return {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: 1,
|
||||
h: 1
|
||||
};
|
||||
};
|
||||
|
||||
function generateElements(list: any[]): Elements {
|
||||
const elements: Elements = list.map((item) => {
|
||||
if (Array.isArray(item)) {
|
||||
return {
|
||||
...getElemBase(),
|
||||
uuid: `group`,
|
||||
type: 'group',
|
||||
detail: {
|
||||
children: generateElements(item)
|
||||
}
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...getElemBase(),
|
||||
uuid: `rect-${item}`,
|
||||
type: 'rect',
|
||||
detail: {}
|
||||
};
|
||||
}
|
||||
}) as Elements;
|
||||
return elements;
|
||||
}
|
||||
|
||||
describe('@idraw/util: handle-element ', () => {
|
||||
// [2] -> [4]
|
||||
// [0, 1, 2, 3, 2, 4, 5]
|
||||
// [0, 1, 3, 2, 4, 5]
|
||||
test('moveElementPosition, move-down [2] -> [4]', () => {
|
||||
const list: Elements = generateElements([0, 1, 2, 3, 4, 5]);
|
||||
const from = [2];
|
||||
const to = [4];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([0, 1, 3, 2, 4, 5]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
// revert action
|
||||
// result [2] [3]
|
||||
const revertInfo = calcRevertMovePosition({ from, to }) as { from: number[]; to: number[] };
|
||||
// revert [3] -> [2]
|
||||
moveElementPosition(list, {
|
||||
from: revertInfo.from,
|
||||
to: revertInfo.to
|
||||
});
|
||||
expect(list).toStrictEqual(generateElements([0, 1, 2, 3, 4, 5]));
|
||||
});
|
||||
|
||||
// [4] -> [2] yes
|
||||
// [0, 1, 4, 2, 3, 4, 5]
|
||||
// [0, 1, 4, 2, 3, 5]
|
||||
test('moveElementPosition, move-up [4] -> [2]', () => {
|
||||
const list: Elements = generateElements([0, 1, 2, 3, 4, 5]);
|
||||
const from = [4];
|
||||
const to = [2];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([0, 1, 4, 2, 3, 5]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
// revert action
|
||||
// result [5] [2]
|
||||
const revertInfo = calcRevertMovePosition({ from, to }) as { from: number[]; to: number[] };
|
||||
// revert [2] -> [5]
|
||||
moveElementPosition(list, {
|
||||
from: revertInfo.from,
|
||||
to: revertInfo.to
|
||||
});
|
||||
expect(list).toStrictEqual(generateElements([0, 1, 2, 3, 4, 5]));
|
||||
});
|
||||
|
||||
// [3, 2, 1] -> [2]
|
||||
test('moveElementPosition, move-up [3, 2, 1] -> [2]', () => {
|
||||
const list: Elements = generateElements([0, 1, 2, [0, 1, [0, 1, 2, 3], 3], 4, 5]);
|
||||
const from = [3, 2, 1];
|
||||
const to = [2];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([0, 1, 1, 2, [0, 1, [0, 2, 3], 3], 4, 5]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
// revert action
|
||||
// result from: [ 4, 2, 1 ], to: [ 2 ]
|
||||
const revertInfo = calcRevertMovePosition({ from, to }) as { from: number[]; to: number[] };
|
||||
// revert [2] -> [ 4, 2, 1 ]
|
||||
moveElementPosition(list, {
|
||||
from: revertInfo.from,
|
||||
to: revertInfo.to
|
||||
});
|
||||
expect(list).toStrictEqual(generateElements([0, 1, 2, [0, 1, [0, 1, 2, 3], 3], 4, 5]));
|
||||
});
|
||||
|
||||
// [1] -> [1, 2, 3]
|
||||
test('moveElementPosition, move-up [1] -> [1, 2, 3]', () => {
|
||||
const list: Elements = generateElements([0, [0, 1, [0, 1, 2, 3, 4, 5], 3, 4, 5], 2, 3, 4, 5]);
|
||||
const from = [1];
|
||||
const to = [1, 2, 3];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([0, [0, 1, [0, 1, 2, 3, 4, 5], 3, 4, 5], 2, 3, 4, 5]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
// revert action null
|
||||
const revertInfo = calcRevertMovePosition({ from, to });
|
||||
expect(revertInfo).toBeNull();
|
||||
});
|
||||
|
||||
// [1, 2, 3, 4, 5] -> [1, 2, 2]
|
||||
test('moveElementPosition, move-up [1, 2, 3, 4, 5] -> [1, 2, 2]', () => {
|
||||
const list: Elements = generateElements([
|
||||
0,
|
||||
[0, 1, [0, 1, 2, [0, 1, 2, 3, [0, 1, 2, 3, 4, 5], 5], 4, 5], 3, 4, 5],
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]);
|
||||
const from = [1, 2, 3, 4, 5];
|
||||
const to = [1, 2, 2];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([
|
||||
0,
|
||||
[0, 1, [0, 1, 5, 2, [0, 1, 2, 3, [0, 1, 2, 3, 4], 5], 4, 5], 3, 4, 5],
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5
|
||||
]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
// revert action
|
||||
// result from: [ 1, 2, 4, 4, 5 ], to: [ 1, 2, 2 ]
|
||||
const revertInfo = calcRevertMovePosition({ from, to }) as { from: number[]; to: number[] };
|
||||
// revert [ 1, 2, 2 ] -> [ 1, 2, 4, 4, 5 ]
|
||||
moveElementPosition(list, {
|
||||
from: revertInfo.from,
|
||||
to: revertInfo.to
|
||||
});
|
||||
expect(list).toStrictEqual(
|
||||
generateElements([0, [0, 1, [0, 1, 2, [0, 1, 2, 3, [0, 1, 2, 3, 4, 5], 5], 4, 5], 3, 4, 5], 2, 3, 4, 5])
|
||||
);
|
||||
});
|
||||
|
||||
// [1] -> [1]
|
||||
test('moveElementPosition, move-up [1] -> [1]', () => {
|
||||
const list: Elements = generateElements([0, 1, 2, [0, 1, [0, 1, 2, 3], 3], 4, 5]);
|
||||
const from = [1];
|
||||
const to = [1];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([0, 1, 2, [0, 1, [0, 1, 2, 3], 3], 4, 5]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
// revert action null
|
||||
const revertInfo = calcRevertMovePosition({ from, to });
|
||||
expect(revertInfo).toBeNull();
|
||||
});
|
||||
|
||||
// [2, 4] -> [1, 2]
|
||||
test('moveElementPosition, move-up [1, 2] -> [2, 4]', () => {
|
||||
const list: Elements = generateElements([0, [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], 3, 4]);
|
||||
const from = [2, 4];
|
||||
const to = [1, 2];
|
||||
moveElementPosition(list, {
|
||||
from,
|
||||
to
|
||||
});
|
||||
const expectResult = generateElements([0, [0, 1, 4, 2, 3, 4], [0, 1, 2, 3], 3, 4]);
|
||||
expect(list).toStrictEqual(expectResult);
|
||||
|
||||
// revert action
|
||||
// result from: [ 2, 4 ], to: [ 1, 2 ]
|
||||
const revertInfo = calcRevertMovePosition({ from, to }) as { from: number[]; to: number[] };
|
||||
|
||||
// revert [ 1, 2 ] -> [ 2, 4 ]
|
||||
moveElementPosition(list, {
|
||||
from: revertInfo.from,
|
||||
to: revertInfo.to
|
||||
});
|
||||
expect(list).toStrictEqual(generateElements([0, [0, 1, 2, 3, 4], [0, 1, 2, 3, 4], 3, 4]));
|
||||
});
|
||||
});
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { parseHTMLToDataURL, parseSVGToDataURL } from '../../src/lib/parser';
|
||||
import { parseHTMLToDataURL, parseSVGToDataURL } from '../../src/view/parser';
|
||||
|
||||
describe('@idraw/util: lib/parser', () => {
|
||||
test('parseHTMLToDataURL', async () => {
|
||||
|
|
|
|||
|
|
@ -1,58 +1,53 @@
|
|||
import {
|
||||
delay, throttle, compose,
|
||||
} from '../../src/lib/time';
|
||||
|
||||
import { delay, throttle, compose } from '@idraw/util';
|
||||
|
||||
describe('@idraw/util: lib/delay', () => {
|
||||
|
||||
test('delay', async () => {
|
||||
test('delay', async () => {
|
||||
const start = Date.now();
|
||||
const time = 1000;
|
||||
await delay(time);
|
||||
const count = Date.now() - start;
|
||||
expect(count >= time).toStrictEqual(true);
|
||||
});
|
||||
|
||||
test('throttle', async () => {
|
||||
|
||||
test('throttle', async () => {
|
||||
let count = 0;
|
||||
function doThrottle() {
|
||||
return new Promise((resolve) => {
|
||||
const func = throttle(function() {
|
||||
count ++;
|
||||
const func = throttle(function () {
|
||||
count++;
|
||||
}, 100);
|
||||
let interval = setInterval(() => {
|
||||
if (count >= 5) {
|
||||
clearInterval(interval);
|
||||
resolve(null)
|
||||
resolve(null);
|
||||
}
|
||||
func();
|
||||
}, 5);
|
||||
})
|
||||
});
|
||||
}
|
||||
await doThrottle();
|
||||
expect(count).toStrictEqual(5);
|
||||
});
|
||||
|
||||
|
||||
test('compose', async () => {
|
||||
test('compose', async () => {
|
||||
let middleware = [];
|
||||
let context = {
|
||||
data: []
|
||||
};
|
||||
|
||||
middleware.push(async(ctx: any, next: any) => {
|
||||
middleware.push(async (ctx: any, next: any) => {
|
||||
ctx.data.push(1);
|
||||
await next();
|
||||
ctx.data.push(6);
|
||||
});
|
||||
|
||||
middleware.push(async(ctx: any, next: any) => {
|
||||
middleware.push(async (ctx: any, next: any) => {
|
||||
ctx.data.push(2);
|
||||
await next();
|
||||
ctx.data.push(5);
|
||||
});
|
||||
|
||||
middleware.push(async(ctx: any, next: any) => {
|
||||
middleware.push(async (ctx: any, next: any) => {
|
||||
ctx.data.push(3);
|
||||
await next();
|
||||
ctx.data.push(4);
|
||||
|
|
@ -61,8 +56,6 @@ describe('@idraw/util: lib/delay', () => {
|
|||
const fn = compose(middleware);
|
||||
|
||||
await fn(context);
|
||||
expect(context).toStrictEqual({data: [1, 2, 3, 4, 5, 6]});
|
||||
expect(context).toStrictEqual({ data: [1, 2, 3, 4, 5, 6] });
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
50
packages/util/__tests__/lib/unflat-object.test.ts
Normal file
50
packages/util/__tests__/lib/unflat-object.test.ts
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { unflatObject } from '@idraw/util';
|
||||
|
||||
describe('unflatObject', () => {
|
||||
test('unflat object', () => {
|
||||
expect(unflatObject({ 'a.b.c': 1 })).toStrictEqual({
|
||||
a: { b: { c: 1 } }
|
||||
});
|
||||
});
|
||||
|
||||
test('unflat array', () => {
|
||||
const input = {
|
||||
'list[0]': 'a',
|
||||
'list[1].name': 'b'
|
||||
};
|
||||
expect(unflatObject(input)).toEqual({ list: ['a', { name: 'b' }] });
|
||||
});
|
||||
|
||||
test('unflat array and object', () => {
|
||||
const input = {
|
||||
'user.name': 'Alice',
|
||||
'posts[0].title': 'First',
|
||||
'posts[1].tags[0]': 'tech'
|
||||
};
|
||||
expect(unflatObject(input)).toEqual({
|
||||
user: { name: 'Alice' },
|
||||
posts: [{ title: 'First' }, { tags: ['tech'] }]
|
||||
});
|
||||
});
|
||||
|
||||
test('Error', () => {
|
||||
expect(() =>
|
||||
unflatObject({
|
||||
'a.b': 1,
|
||||
'a[0]': 2
|
||||
})
|
||||
).toThrow(`Structure conflict at path 'a.0': Expected array but found object`);
|
||||
|
||||
// expect(() =>
|
||||
// unflatObject({
|
||||
// 'a..b': 1
|
||||
// })
|
||||
// ).toStrictEqual({});
|
||||
});
|
||||
|
||||
test('Side cases', () => {
|
||||
// expect(unflatObject({})).toEqual({});
|
||||
// expect(unflatObject({ '': 42 })).toEqual({ '': 42 });
|
||||
expect(unflatObject({ '': 42 })).toEqual({});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import {
|
||||
createUUID
|
||||
} from '../../src/lib/uuid';
|
||||
import { createAssetId, isAssetId } from '@idraw/util';
|
||||
|
||||
describe('@idraw/util: createAssetId ', () => {
|
||||
test('url', () => {
|
||||
const url1 = 'https://example.com/2025/01/01/000001.jpg';
|
||||
const assetId1 = createAssetId(url1);
|
||||
expect(isAssetId(assetId1)).toBeTruthy();
|
||||
|
||||
describe('@idraw/util: lib/uuid', () => {
|
||||
|
||||
test('createUUID', async () => {
|
||||
const uuid = createUUID();
|
||||
expect(typeof uuid).toStrictEqual('string');
|
||||
expect(uuid.length).toStrictEqual(36);
|
||||
const url2 = 'https://example.com/2025/01/01/000002.jpg';
|
||||
const assetId2 = createAssetId(url2);
|
||||
expect(isAssetId(assetId2)).toBeTruthy();
|
||||
|
||||
expect(url1).not.toBe(url2);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
export { delay, compose, throttle, debounce } from './lib/time';
|
||||
export { delay, compose, throttle, debounce } from './tool/time';
|
||||
export {
|
||||
downloadImageFromCanvas,
|
||||
parseFileToBase64,
|
||||
pickFile,
|
||||
parseFileToText,
|
||||
downloadFileFromText
|
||||
} from './lib/file';
|
||||
} from './tool/file';
|
||||
export {
|
||||
toColorHexStr,
|
||||
toColorHexNum,
|
||||
|
|
@ -14,15 +14,15 @@ export {
|
|||
colorToCSS,
|
||||
colorToLinearGradientCSS,
|
||||
mergeHexColorAlpha
|
||||
} from './lib/color';
|
||||
export { createUUID, isAssetId, createAssetId } from './lib/uuid';
|
||||
export { deepClone, sortDataAsserts, deepCloneElement, deepCloneData, filterCompactData } from './lib/data';
|
||||
export { istype } from './lib/istype';
|
||||
export { loadImage, loadSVG, loadHTML } from './lib/load';
|
||||
export { is } from './lib/is';
|
||||
export { check } from './lib/check';
|
||||
export { createBoardContent, createContext2D, createOffscreenContext2D } from './lib/canvas';
|
||||
export { EventEmitter } from './lib/event';
|
||||
} from './tool/color';
|
||||
export { createUUID, isAssetId, createAssetId } from './tool/uuid';
|
||||
export { deepClone, sortDataAsserts, deepCloneElement, deepCloneData, filterCompactData } from './view/data';
|
||||
export { istype } from './tool/istype';
|
||||
export { loadImage, loadSVG, loadHTML } from './view/load';
|
||||
export { is } from './view/is';
|
||||
export { check } from './view/check';
|
||||
export { createBoardContent, createContext2D, createOffscreenContext2D } from './view/canvas';
|
||||
export { EventEmitter } from './tool/event';
|
||||
export {
|
||||
calcDistance,
|
||||
calcSpeed,
|
||||
|
|
@ -31,10 +31,10 @@ export {
|
|||
vaildPoint,
|
||||
vaildTouchPoint,
|
||||
getCenterFromTwoPoints
|
||||
} from './lib/point';
|
||||
export { Store } from './lib/store';
|
||||
export { getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from './lib/middleware';
|
||||
export { Context2D } from './lib/context2d';
|
||||
} from './view/point';
|
||||
export { Store } from './tool/store';
|
||||
export { getViewScaleInfoFromSnapshot, getViewSizeInfoFromSnapshot } from './view/middleware';
|
||||
export { Context2D } from './view/context2d';
|
||||
export {
|
||||
rotateElement,
|
||||
parseRadianToAngle,
|
||||
|
|
@ -46,7 +46,7 @@ export {
|
|||
rotatePointInGroup,
|
||||
limitAngle,
|
||||
calcRadian
|
||||
} from './lib/rotate';
|
||||
} from './view/rotate';
|
||||
export {
|
||||
getSelectedElementUUIDs,
|
||||
validateElements,
|
||||
|
|
@ -68,8 +68,8 @@ export {
|
|||
getElementPositionMapFromList,
|
||||
calcElementListSize,
|
||||
isSameElementSize
|
||||
} from './lib/element';
|
||||
export { checkRectIntersect } from './lib/rect';
|
||||
} from './view/element';
|
||||
export { checkRectIntersect } from './view/rect';
|
||||
export {
|
||||
viewScale,
|
||||
viewScroll,
|
||||
|
|
@ -85,23 +85,24 @@ export {
|
|||
originRectInfoToRangeRectInfo,
|
||||
isViewPointInElementSize,
|
||||
isViewPointInVertexes
|
||||
} from './lib/view-calc';
|
||||
export { rotatePoint, rotateVertexes, rotateByCenter } from './lib/rotate';
|
||||
} from './view/view-calc';
|
||||
export { rotatePoint, rotateVertexes, rotateByCenter } from './view/rotate';
|
||||
export {
|
||||
getElementVertexes,
|
||||
calcElementVertexesInGroup,
|
||||
calcElementVertexesQueueInGroup,
|
||||
calcElementQueueVertexesQueueInGroup
|
||||
} from './lib/vertex';
|
||||
export { calcElementSizeController, calcLayoutSizeController } from './lib/controller';
|
||||
export { generateSVGPath, parseSVGPath } from './lib/svg-path';
|
||||
export { generateHTML, parseHTML } from './lib/html';
|
||||
export { compressImage } from './lib/image';
|
||||
export { formatNumber } from './lib/number';
|
||||
export { matrixToAngle, matrixToRadian } from './lib/matrix';
|
||||
export { getDefaultElementDetailConfig, getDefaultElementRectDetail } from './lib/config';
|
||||
export { calcViewBoxSize } from './lib/view-box';
|
||||
} from './view/vertex';
|
||||
export { calcElementSizeController, calcLayoutSizeController } from './view/controller';
|
||||
export { generateSVGPath, parseSVGPath } from './view/svg-path';
|
||||
export { generateHTML, parseHTML } from './tool/html';
|
||||
export { compressImage } from './tool/image';
|
||||
export { formatNumber } from './tool/number';
|
||||
export { matrixToAngle, matrixToRadian } from './view/matrix';
|
||||
export { getDefaultElementDetailConfig, getDefaultElementRectDetail } from './view/config';
|
||||
export { calcViewBoxSize } from './view/view-box';
|
||||
export {
|
||||
mergeElement,
|
||||
createElement,
|
||||
insertElementToListByPosition,
|
||||
deleteElementInListByPosition,
|
||||
|
|
@ -109,15 +110,21 @@ export {
|
|||
moveElementPosition,
|
||||
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';
|
||||
export { enhanceFontFamliy } from './lib/text';
|
||||
export { flatElementList } from './lib/flat';
|
||||
export { groupElementsByPosition, ungroupElementsByPosition } from './lib/group';
|
||||
export { calcPointMoveElementInGroup } from './lib/point-move-element';
|
||||
export { merge } from './lib/merge';
|
||||
export { omit } from './lib/omit';
|
||||
export { elementToBoxInfo } from './lib/box';
|
||||
} from './view/handle-element';
|
||||
export { deepResizeGroupElement } from './view/resize-element';
|
||||
export { calcViewCenterContent, calcViewCenter } from './view/view-content';
|
||||
export { toFlattenElement, toFlattenLayout, toFlattenGlobal } from './view/modify-record';
|
||||
export { enhanceFontFamliy } from './view/text';
|
||||
export { flatElementList } from './view/flat';
|
||||
export { groupElementsByPosition, ungroupElementsByPosition } from './view/group';
|
||||
export { calcPointMoveElementInGroup } from './view/point-move-element';
|
||||
export { mergeLayout } from './view/handle-layout';
|
||||
export { mergeGlobal } from './view/handle-global';
|
||||
export { calcRevertMovePosition, calcResultMovePosition } from './view/position';
|
||||
|
||||
export { merge } from './tool/merge';
|
||||
export { omit } from './tool/omit';
|
||||
export { elementToBoxInfo } from './view/box';
|
||||
export { get, set, toPath } from './tool/get-set-del';
|
||||
export { flatObject } from './tool/flat-object';
|
||||
export { unflatObject } from './tool/unflat-object';
|
||||
|
|
|
|||
|
|
@ -1,179 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,98 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
export function createUUID(): string {
|
||||
function _createStr() {
|
||||
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
|
||||
}
|
||||
return `${_createStr()}${_createStr()}-${_createStr()}-${_createStr()}-${_createStr()}-${_createStr()}${_createStr()}${_createStr()}`;
|
||||
}
|
||||
|
||||
function limitHexStr(str: string) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
count += str.charCodeAt(i) * str.charCodeAt(i) * i * i;
|
||||
}
|
||||
return count.toString(16).substring(0, 4);
|
||||
}
|
||||
|
||||
export function createAssetId(assetStr: string): string {
|
||||
const len = assetStr.length;
|
||||
const mid = Math.floor(len / 2);
|
||||
const start4 = assetStr.substring(0, 4).padEnd(4, '0');
|
||||
const end4 = assetStr.substring(0, 4).padEnd(4, '0');
|
||||
const str1 = limitHexStr(len.toString(16).padEnd(4, start4));
|
||||
const str2 = limitHexStr(assetStr.substring(mid - 4, mid).padEnd(4, start4)).padEnd(4, 'f');
|
||||
const str3 = limitHexStr(assetStr.substring(mid - 8, mid - 4).padEnd(4, start4)).padEnd(4, 'f');
|
||||
const str4 = limitHexStr(assetStr.substring(mid - 12, mid - 8).padEnd(4, start4)).padEnd(4, 'f');
|
||||
const str5 = limitHexStr(assetStr.substring(mid - 16, mid - 12).padEnd(4, end4)).padEnd(4, 'f');
|
||||
const str6 = limitHexStr(assetStr.substring(mid, mid + 4).padEnd(4, end4)).padEnd(4, 'f');
|
||||
const str7 = limitHexStr(assetStr.substring(mid + 4, mid + 8).padEnd(4, end4)).padEnd(4, 'f');
|
||||
const str8 = limitHexStr(end4.padEnd(4, start4).padEnd(4, end4));
|
||||
return `@assets/${str1}${str2}-${str3}-${str4}-${str5}-${str6}${str7}${str8}`;
|
||||
}
|
||||
|
||||
export function isAssetId(id: any | string): boolean {
|
||||
return /^@assets\/[0-9a-z]{8,8}\-[0-9a-z]{4,4}\-[0-9a-z]{4,4}\-[0-9a-z]{4,4}\-[0-9a-z]{12,12}$/.test(`${id}`);
|
||||
}
|
||||
59
packages/util/src/tool/flat-object.ts
Normal file
59
packages/util/src/tool/flat-object.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
type FlattenedObject = Record<string, any>;
|
||||
|
||||
type FlatObjectOptions = {
|
||||
ignorePaths?: string[]; // eg. ["detail.children"]
|
||||
};
|
||||
|
||||
/**
|
||||
* Flattens a nested object/array into a path-based object
|
||||
* @param obj The input object
|
||||
* @param parentKey The parent path (used internally for recursion)
|
||||
* @param result The result object (used internally for recursion)
|
||||
*/
|
||||
function flattenObject<T extends Record<string, any>>(
|
||||
obj: T,
|
||||
parentKey: string = '',
|
||||
result: FlattenedObject = {},
|
||||
opts?: FlatObjectOptions
|
||||
): FlattenedObject {
|
||||
// Iterate over each key in the object
|
||||
Object.keys(obj).forEach((key) => {
|
||||
// Compute the current full path
|
||||
const currentKey = parentKey ? `${parentKey}${isArrayIndex(key) ? `[${key}]` : `.${key}`}` : key;
|
||||
if (!opts?.ignorePaths?.includes(currentKey)) {
|
||||
// Get the current value
|
||||
const value = obj[key];
|
||||
|
||||
// Handle flattenable types (objects/arrays)
|
||||
if (isFlattenable(value)) {
|
||||
flattenObject(value, Array.isArray(value) ? currentKey : currentKey, result, opts);
|
||||
}
|
||||
// Handle primitive values that cannot be flattened further
|
||||
else {
|
||||
result[currentKey] = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Type guard to check if the value can be flattened
|
||||
function isFlattenable(value: unknown): value is object | Array<unknown> {
|
||||
return (typeof value === 'object' && value !== null && !(value instanceof Date)) || Array.isArray(value);
|
||||
}
|
||||
|
||||
// Check if the key is an array index (for path formatting)
|
||||
function isArrayIndex(key: string): boolean {
|
||||
return /^\d+$/.test(key) && !isNaN(Number(key));
|
||||
}
|
||||
|
||||
// Main entry function to flatten an object with enhanced type support
|
||||
export function flatObject<T extends Record<string, any>>(obj: T, opts?: FlatObjectOptions): FlattenedObject {
|
||||
// Defensive check for non-object input
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
return { '': obj };
|
||||
}
|
||||
|
||||
return flattenObject(obj, '', {}, opts);
|
||||
}
|
||||
101
packages/util/src/tool/get-set-del.ts
Normal file
101
packages/util/src/tool/get-set-del.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* Convert path to array format
|
||||
* @example 'a[0].b.c' => ['a', '0', 'b', 'c']
|
||||
*/
|
||||
export function toPath(path: string | string[]): string[] {
|
||||
if (Array.isArray(path)) return [...path];
|
||||
return path.split(/\.|\[|\]/).filter((key) => key !== '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Lodash-style get method to retrieve nested object values
|
||||
* @param obj Target object
|
||||
* @param path Path string or array
|
||||
* @param defaultValue Fallback value if path not found
|
||||
*/
|
||||
export function get<T = any, D = any>(obj: T, path: string | string[], defaultValue?: D): D | undefined {
|
||||
if (!path) {
|
||||
return undefined;
|
||||
}
|
||||
const pathArray = toPath(path);
|
||||
let current: any = obj;
|
||||
|
||||
for (const key of pathArray) {
|
||||
if (current === null || current === undefined) {
|
||||
return defaultValue as D;
|
||||
}
|
||||
current = current[key];
|
||||
}
|
||||
|
||||
return current !== undefined ? current : defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lodash-style set method to assign values to nested paths (mutates original object)
|
||||
* @param obj Target object
|
||||
* @param path Path string or array
|
||||
* @param value Value to assign
|
||||
*/
|
||||
export function set<T = any>(obj: T, path: string | string[], value: any): T {
|
||||
const pathArray = toPath(path);
|
||||
if (pathArray.length === 0) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
let current: any = obj;
|
||||
|
||||
if (current) {
|
||||
for (let i = 0; i < pathArray.length; i++) {
|
||||
const key = pathArray[i];
|
||||
|
||||
if (i === pathArray.length - 1) {
|
||||
// Final path segment - assign value
|
||||
current[key] = value;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create missing path structures
|
||||
if (current && (current?.[key] === undefined || typeof current?.[key] !== 'object' || current?.[key] === null)) {
|
||||
const nextKey = pathArray[i + 1];
|
||||
const isNextNumeric = /^\d+$/.test(nextKey);
|
||||
current[key] = isNextNumeric ? [] : {};
|
||||
}
|
||||
|
||||
current = current?.[key];
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
export function del<T = any>(obj: T, path: string | string[]): T {
|
||||
const pathArray = toPath(path);
|
||||
if (pathArray.length === 0) {
|
||||
return obj;
|
||||
}
|
||||
|
||||
let current: any = obj;
|
||||
|
||||
if (current) {
|
||||
for (let i = 0; i < pathArray.length; i++) {
|
||||
const key = pathArray[i];
|
||||
|
||||
if (i === pathArray.length - 1) {
|
||||
// Final path segment - delete value
|
||||
delete current[key];
|
||||
break;
|
||||
}
|
||||
|
||||
// Create missing path structures
|
||||
if (current && (current?.[key] === undefined || typeof current?.[key] !== 'object' || current?.[key] === null)) {
|
||||
const nextKey = pathArray[i + 1];
|
||||
const isNextNumeric = /^\d+$/.test(nextKey);
|
||||
current[key] = isNextNumeric ? [] : {};
|
||||
}
|
||||
|
||||
current = current?.[key];
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
52
packages/util/src/tool/hash.ts
Normal file
52
packages/util/src/tool/hash.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/**
|
||||
* Synchronously generates 32-character Base36 encoded hash (enhanced 256-bit algorithm)
|
||||
* @param str - Input string (any length)
|
||||
* @returns 32-character lowercase Base36 string (0-9a-z)
|
||||
*/
|
||||
export function generate32Base36Hash(str: string): string {
|
||||
// Generate 256-bit hash (4x64-bit FNV hashes)
|
||||
const hash256 = generate256BitHash(str);
|
||||
// Convert to Base36 and format to 32 characters
|
||||
return bigIntToBase36(hash256).padStart(32, '0').slice(0, 32);
|
||||
}
|
||||
// // Usage example
|
||||
// console.log(generate32Base36Hash('hello world'));
|
||||
// // Sample output: 2yj8q4z7kpr6s9d5m2w3x1g6h8j4n0q
|
||||
|
||||
// Core algorithm for generating 256-bit hash
|
||||
function generate256BitHash(str: string): bigint {
|
||||
let h1 = 0xcbf29ce484222325n,
|
||||
h2 = 0x84222325cbf29ce4n;
|
||||
let h3 = 0x1b3n * h1,
|
||||
h4 = 0x1000000n * h2; // Different initial values
|
||||
const prime = 0x100000001b3n;
|
||||
|
||||
// Chunk processing for large texts (per 4096 characters)
|
||||
const chunkSize = 4096;
|
||||
for (let i = 0; i < str.length; i += chunkSize) {
|
||||
const chunk = str.slice(i, i + chunkSize);
|
||||
for (let j = 0; j < chunk.length; j++) {
|
||||
const code = BigInt(chunk.charCodeAt(j) + i + j); // Position-sensitive
|
||||
h1 = (h1 ^ code) * prime;
|
||||
h2 = ((h2 ^ h1) * prime) ^ h3;
|
||||
h3 = (h3 ^ h2) * prime + h4;
|
||||
h4 = ((h4 ^ h3) * prime) | 0x1234567890abcdefn;
|
||||
}
|
||||
}
|
||||
|
||||
// Combine 4x64-bit hashes into 256-bit
|
||||
return (h1 << 192n) | (h2 << 128n) | (h3 << 64n) | h4;
|
||||
}
|
||||
|
||||
// Utility function for BigInt to Base36 conversion
|
||||
function bigIntToBase36(num: bigint): string {
|
||||
const chars = '0123456789abcdefghijklmnopqrstuvwxyz';
|
||||
if (num === 0n) return '0';
|
||||
let result = '';
|
||||
while (num > 0n) {
|
||||
const rem = num % 36n;
|
||||
result = chars[Number(rem)] + result;
|
||||
num = num / 36n;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
import { HTMLNode } from '@idraw/types';
|
||||
|
||||
const attrRegExp = /\s([^'"/\s><]+?)[\s/>]|([^\s=]+)=\s?(".*?"|'.*?')/g;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
const elemRegExp = /<[a-zA-Z0-9\-\!\/](?:"[^"]*"|'[^']*'|[^'">])*>/g;
|
||||
const whitespaceReg = /^\s*$/;
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ export function parseHTML(html: string) {
|
|||
|
||||
function attrString(attrs: HTMLNode['attributes']) {
|
||||
const buff = [];
|
||||
for (let key in attrs) {
|
||||
for (const key in attrs) {
|
||||
buff.push(key + '="' + attrs[key] + '"');
|
||||
}
|
||||
if (!buff.length) {
|
||||
|
|
@ -177,7 +178,11 @@ function stringify(buff: string, htmlNode: HTMLNode): string {
|
|||
case 'text':
|
||||
return buff + htmlNode.textContent;
|
||||
case 'element':
|
||||
buff += '<' + htmlNode.name + (htmlNode.attributes ? attrString(htmlNode.attributes) : '') + (htmlNode.isVoid ? '/>' : '>');
|
||||
buff +=
|
||||
'<' +
|
||||
htmlNode.name +
|
||||
(htmlNode.attributes ? attrString(htmlNode.attributes) : '') +
|
||||
(htmlNode.isVoid ? '/>' : '>');
|
||||
if (htmlNode.isVoid) {
|
||||
return buff;
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import type { RecursivePartial } from '@idraw/types';
|
||||
import { deepClone } from './data';
|
||||
import { deepClone } from '../view/data';
|
||||
|
||||
export class Store<
|
||||
T extends Record<string | symbol, any> = Record<string | symbol, any>,
|
||||
121
packages/util/src/tool/unflat-object.ts
Normal file
121
packages/util/src/tool/unflat-object.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
type NestedStructure = object | unknown[];
|
||||
|
||||
/**
|
||||
* Restores a flattened object to its original nested structure
|
||||
* @param flatObj Flattened object (e.g., { 'a.b.c': 1, 'a.d[0]': 2 })
|
||||
* @returns Nested object structure (e.g., { a: { b: { c: 1 }, d: [2] } })
|
||||
*/
|
||||
export function unflatObject<T extends Record<string, unknown>>(flatObj: T): NestedStructure {
|
||||
const result: NestedStructure = {};
|
||||
|
||||
for (const [flatKey, value] of Object.entries(flatObj)) {
|
||||
const pathParts = parseKeyToPath(flatKey);
|
||||
buildNestedStructure(result, pathParts, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improved path parser with better array handling
|
||||
* @example 'a.b[0].c' => ['a', 'b', '0', 'c']
|
||||
*/
|
||||
function parseKeyToPath(flatKey: string): string[] {
|
||||
const regex = /([\w-]+)|\[(\d+)\]/g;
|
||||
const pathParts: string[] = [];
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(flatKey)) !== null) {
|
||||
const prop = match[1] || match[2];
|
||||
if (prop) {
|
||||
pathParts.push(prop);
|
||||
}
|
||||
}
|
||||
|
||||
return pathParts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhanced structure builder with array fixes
|
||||
*/
|
||||
function buildNestedStructure(currentObj: NestedStructure, pathParts: string[], value: unknown): void {
|
||||
let currentLevel: any = currentObj;
|
||||
|
||||
for (let i = 0; i < pathParts.length; i++) {
|
||||
const part = pathParts[i];
|
||||
const isArrayPart = isArrayIndex(part);
|
||||
const isLast = i === pathParts.length - 1;
|
||||
|
||||
try {
|
||||
if (isArrayPart) {
|
||||
validateArrayPath(currentLevel, part);
|
||||
} else {
|
||||
validateObjectPath(currentLevel, part);
|
||||
}
|
||||
} catch (e: any) {
|
||||
throw new Error(`Structure conflict at path '${pathParts.slice(0, i + 1).join('.')}': ${e.message}`);
|
||||
}
|
||||
|
||||
if (isLast) {
|
||||
assignValue(currentLevel, part, value);
|
||||
} else {
|
||||
currentLevel = prepareNextLevel(currentLevel, part, pathParts[i + 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Utility functions ===============================================
|
||||
|
||||
function isArrayIndex(key: string): boolean {
|
||||
return /^\d+$/.test(key);
|
||||
}
|
||||
|
||||
function validateArrayPath(obj: any, index: string): void {
|
||||
if (!Array.isArray(obj)) {
|
||||
throw new Error(`Expected array but found ${typeof obj}`);
|
||||
}
|
||||
|
||||
const idx = Number(index);
|
||||
if (idx > obj.length) {
|
||||
obj.length = idx + 1;
|
||||
}
|
||||
}
|
||||
|
||||
function validateObjectPath(obj: any, key: string): void {
|
||||
if (Array.isArray(obj)) {
|
||||
throw new Error(`Cannot create object property '${key}' on array`);
|
||||
}
|
||||
|
||||
if (typeof obj !== 'object' || obj === null) {
|
||||
throw new Error(`Invalid structure for property '${key}'`);
|
||||
}
|
||||
}
|
||||
|
||||
function prepareNextLevel(current: any, part: string, nextPart?: string): any {
|
||||
const isNextArray = nextPart ? isArrayIndex(nextPart) : false;
|
||||
|
||||
if (Array.isArray(current)) {
|
||||
const index = Number(part);
|
||||
if (!current[index]) {
|
||||
current[index] = isNextArray ? [] : {};
|
||||
}
|
||||
return current[index];
|
||||
}
|
||||
|
||||
if (!current[part]) {
|
||||
current[part] = isNextArray ? [] : {};
|
||||
}
|
||||
return current[part];
|
||||
}
|
||||
|
||||
function assignValue(target: any, key: string, value: unknown): void {
|
||||
if (Array.isArray(target)) {
|
||||
const index = Number(key);
|
||||
if (index >= target.length) {
|
||||
target.length = index + 1;
|
||||
}
|
||||
target[index] = value;
|
||||
} else {
|
||||
target[key] = value;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue