feat: improve text-editor, fix getData and fix type error

This commit is contained in:
chenshenhai 2024-02-03 13:18:18 +08:00
parent a3ed9dd744
commit bf3629a977
22 changed files with 438 additions and 94 deletions

View file

@ -34,7 +34,7 @@ export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
#viewer: Viewer;
#calculator: Calculator;
#eventHub: EventEmitter<T> = new EventEmitter<T>();
#hasDestroyed: boolean = false;
constructor(opts: BoardOptions) {
const { boardContent } = opts;
const sharer = new Sharer();
@ -70,6 +70,10 @@ export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
this.#resetActiveMiddlewareObjs();
}
isDestroyed() {
return this.#hasDestroyed;
}
destroy() {
// #opts
// #middlewareMap
@ -81,6 +85,7 @@ export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
// #viewer: Viewer;
this.#calculator.destroy();
this.#eventHub.destroy();
this.#hasDestroyed = true;
}
#init() {
@ -376,6 +381,14 @@ export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
getEventHub(): EventEmitter<T> {
return this.#eventHub;
}
onWatcherEvents() {
this.#watcher.onEvents();
}
offWatcherEvents() {
this.#watcher.offEvents();
}
}
export { Sharer, Calculator };

View file

@ -8,6 +8,7 @@ function isBoardAvailableNum(num: any): boolean {
export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
#opts: BoardWatcherOptions;
#store: Store<BoardWatcherStore>;
#hasDestroyed: boolean = false;
constructor(opts: BoardWatcherOptions) {
super();
const store = new Store<BoardWatcherStore>({ defaultStorage: { hasPointDown: false, prevClickPoint: null } });
@ -17,6 +18,13 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
}
#init() {
this.onEvents();
}
onEvents() {
if (this.#hasDestroyed) {
return;
}
const container = window;
container.addEventListener('mousemove', this.#onHover);
container.addEventListener('mousedown', this.#onPointStart);
@ -28,7 +36,7 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
container.addEventListener('contextmenu', this.#onContextMenu);
}
destroy() {
offEvents() {
const container = window;
container.removeEventListener('mousemove', this.#onHover);
container.removeEventListener('mousedown', this.#onPointStart);
@ -40,6 +48,12 @@ export class BoardWatcher extends EventEmitter<BoardWatcherEventMap> {
container.removeEventListener('contextmenu', this.#onContextMenu);
}
destroy() {
this.offEvents();
this.#store.destroy();
this.#hasDestroyed = true;
}
#onWheel = (e: WheelEvent) => {
if (!this.#isInTarget(e)) {
return;

View file

@ -45,6 +45,10 @@ export class Core<E extends CoreEvent = CoreEvent> {
});
}
isDestroyed() {
return this.#board.isDestroyed();
}
destroy() {
this.#board.destroy();
this.#canvas.remove();
@ -129,4 +133,12 @@ export class Core<E extends CoreEvent = CoreEvent> {
getLoadItemMap(): LoadItemMap {
return this.#board.getRenderer().getLoadItemMap();
}
onBoardWatcherEvents() {
this.#board.onWatcherEvents();
}
offBoardWatcherEvents() {
this.#board.offWatcherEvents();
}
}

View file

@ -3,11 +3,11 @@ import { limitAngle, loadImage, parseAngleToRadian } from '@idraw/util';
import { CURSOR, CURSOR_RESIZE, CURSOR_DRAG_DEFAULT, CURSOR_DRAG_ACTIVE } from './cursor-image';
export class Cursor {
private _eventHub: UtilEventEmitter<CoreEvent>;
private _container: HTMLDivElement;
private _cursorType: 'auto' | string | null = null;
private _resizeCursorBaseImage: HTMLImageElement | null = null;
private _cursorImageMap: Record<string, string> = {
#eventHub: UtilEventEmitter<CoreEvent>;
#container: HTMLDivElement;
#cursorType: 'default' | string | null = null;
#resizeCursorBaseImage: HTMLImageElement | null = null;
#cursorImageMap: Record<string, string> = {
auto: CURSOR,
'drag-default': CURSOR_DRAG_DEFAULT,
'drag-active': CURSOR_DRAG_ACTIVE,
@ -19,56 +19,60 @@ export class Cursor {
eventHub: UtilEventEmitter<CoreEvent>;
}
) {
this._container = container;
this._eventHub = opts.eventHub;
this._init();
this._loadResizeCursorBaseImage();
this.#container = container;
this.#eventHub = opts.eventHub;
this.#init();
this.#loadResizeCursorBaseImage();
}
private _init() {
const { _eventHub: eventHub } = this;
this._resetCursor('auto');
#init() {
const eventHub = this.#eventHub;
this.#resetCursor('default');
eventHub.on('cursor', (e) => {
if (e.type === 'over-element' || !e.type) {
this._resetCursor('auto');
this.#resetCursor('auto');
} else if (typeof e.type === 'string' && e.type?.startsWith('resize-')) {
this._setCursorResize(e);
this.#setCursorResize(e);
} else if (e.type === 'drag-default') {
this._resetCursor('drag-default');
this.#resetCursor('drag-default');
} else if (e.type === 'drag-active') {
this._resetCursor('drag-active');
this.#resetCursor('drag-active');
} else {
this._resetCursor('auto');
this.#resetCursor('auto');
}
});
}
private _loadResizeCursorBaseImage() {
#loadResizeCursorBaseImage() {
loadImage(CURSOR_RESIZE)
.then((img) => {
this._resizeCursorBaseImage = img;
this.#resizeCursorBaseImage = img;
})
.catch((err) => {
console.error(err);
});
}
private _resetCursor(cursorKey: string) {
if (this._cursorType === cursorKey) {
#resetCursor(cursorKey: string) {
if (this.#cursorType === cursorKey) {
return;
}
this._cursorType = cursorKey;
const image = this._cursorImageMap[this._cursorType] || this._cursorImageMap['auto'];
this.#cursorType = cursorKey;
const image = this.#cursorImageMap[this.#cursorType] || this.#cursorImageMap['auto'];
let offsetX = 0;
let offsetY = 0;
if (cursorKey.startsWith('rotate-') && this._cursorImageMap[this._cursorType]) {
if (cursorKey.startsWith('rotate-') && this.#cursorImageMap[this.#cursorType]) {
offsetX = 10;
offsetY = 10;
}
this._container.style.cursor = `image-set(url(${image})2x) ${offsetX} ${offsetY}, auto`;
if (cursorKey === 'default') {
this.#container.style.cursor = 'default';
} else {
this.#container.style.cursor = `image-set(url(${image})2x) ${offsetX} ${offsetY}, auto`;
}
}
private _setCursorResize(e: CoreEvent['cursor']) {
#setCursorResize(e: CoreEvent['cursor']) {
let totalAngle = 0;
if (e.type === 'resize-top') {
totalAngle += 0;
@ -94,14 +98,14 @@ export class Cursor {
});
}
totalAngle = limitAngle(totalAngle);
const cursorKey = this._appendRotateResizeImage(totalAngle);
this._resetCursor(cursorKey);
const cursorKey = this.#appendRotateResizeImage(totalAngle);
this.#resetCursor(cursorKey);
}
private _appendRotateResizeImage(angle: number): string {
#appendRotateResizeImage(angle: number): string {
const key = `rotate-${angle}`;
if (!this._cursorImageMap[key]) {
const baseImage = this._resizeCursorBaseImage;
if (!this.#cursorImageMap[key]) {
const baseImage = this.#resizeCursorBaseImage;
if (baseImage) {
const canvas = document.createElement('canvas');
const w = baseImage.width;
@ -126,7 +130,7 @@ export class Cursor {
ctx.translate(-center.x, -center.y);
const base = canvas.toDataURL('image/png');
this._cursorImageMap[key] = base;
this.#cursorImageMap[key] = base;
}
}
return key;

View file

@ -110,10 +110,10 @@ export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEven
}
textarea.style.position = 'absolute';
textarea.style.left = `${elemX}px`;
textarea.style.top = `${elemY}px`;
textarea.style.width = `${elemW}px`;
textarea.style.height = `${elemH}px`;
textarea.style.left = `${elemX - 1}px`;
textarea.style.top = `${elemY - 1}px`;
textarea.style.width = `${elemW + 2}px`;
textarea.style.height = `${elemH + 2}px`;
textarea.style.transform = `rotate(${limitAngle(element.angle || 0)}deg)`;
// textarea.style.border = 'none';
textarea.style.boxSizing = 'border-box';
@ -127,6 +127,10 @@ export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEven
textarea.style.lineHeight = `${detail.lineHeight * scale}px`;
textarea.style.fontFamily = detail.fontFamily;
textarea.style.fontWeight = `${detail.fontWeight}`;
textarea.style.padding = '0';
textarea.style.margin = '0';
textarea.style.outline = 'none';
textarea.value = detail.text || '';
parent.appendChild(textarea);
};

View file

@ -30,7 +30,8 @@ import {
getElementPositionFromList,
calcElementListSize,
filterCompactData,
calcViewCenterContent
calcViewCenterContent,
calcViewCenter
} from '@idraw/util';
import { defaultSettings } from './config';
import { exportImageFileBlobURL } from './file';
@ -143,7 +144,7 @@ export class iDraw {
centerContent(opts?: { data?: Data }) {
const data = opts?.data || this.#core.getData();
const { viewSizeInfo } = this.getViewInfo();
if (data) {
if (Array.isArray(data?.elements) && data?.elements.length > 0) {
const result = calcViewCenterContent(data, { viewSizeInfo });
this.setViewScale(result);
}
@ -274,8 +275,25 @@ export class iDraw {
});
}
isDestroyed() {
return this.#core.isDestroyed();
}
destroy() {
const core = this.#core;
core.destroy();
}
getViewCenter() {
const { viewScaleInfo, viewSizeInfo } = this.getViewInfo();
return calcViewCenter({ viewScaleInfo, viewSizeInfo });
}
$onBoardWatcherEvents() {
this.#core.onBoardWatcherEvents();
}
$offBoardWatcherEvents() {
this.#core.offBoardWatcherEvents();
}
}

View file

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

View file

@ -7,7 +7,7 @@ export function drawHTML(ctx: ViewContext2D, elem: Element<'html'>, opts: Render
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
rotateElement(ctx, { x, y, w, h, angle }, () => {
if (!content) {
if (!content && !opts.loader.isDestroyed()) {
opts.loader.load(elem as Element<'html'>, opts.elementAssets || {});
}
if (elem.type === 'html' && content) {

View file

@ -20,7 +20,7 @@ export function drawImage(ctx: ViewContext2D, elem: Element<'image'>, opts: Rend
viewSizeInfo,
parentOpacity,
renderContent: () => {
if (!content) {
if (!content && !opts.loader.isDestroyed()) {
opts.loader.load(elem as Element<'image'>, opts.elementAssets || {});
}
if (elem.type === 'image' && content) {

View file

@ -7,7 +7,7 @@ export function drawSVG(ctx: ViewContext2D, elem: Element<'svg'>, opts: Renderer
const { calculator, viewScaleInfo, viewSizeInfo, parentOpacity } = opts;
const { x, y, w, h, angle } = calculator?.elementSize(elem, viewScaleInfo, viewSizeInfo) || elem;
rotateElement(ctx, { x, y, w, h, angle }, () => {
if (!content) {
if (!content && !opts.loader.isDestroyed()) {
opts.loader.load(elem as Element<'svg'>, opts.elementAssets || {});
}
if (elem.type === 'svg' && content) {

View file

@ -7,6 +7,7 @@ import type { Data, BoardRenderer, RendererOptions, RendererEventMap, RendererDr
export class Renderer extends EventEmitter<RendererEventMap> implements BoardRenderer {
#opts: RendererOptions;
#loader: Loader = new Loader();
#hasDestroyed: boolean = false;
constructor(opts: RendererOptions) {
super();
@ -14,10 +15,16 @@ export class Renderer extends EventEmitter<RendererEventMap> implements BoardRen
this.#init();
}
isDestroyed() {
return this.#hasDestroyed;
}
destroy() {
this.clear();
this.#opts = null as any;
this.#loader.destroy();
this.#loader = null as any;
this.#hasDestroyed = true;
}
#init() {

View file

@ -25,6 +25,7 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
#loadFuncMap: Record<LoadElementType | string, LoadFunc<LoadElementType, LoadContent>> = {};
#currentLoadItemMap: LoadItemMap = {};
#storageLoadItemMap: LoadItemMap = {};
#hasDestroyed: boolean = false;
constructor() {
super();
@ -60,7 +61,13 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
});
}
isDestroyed() {
return this.#hasDestroyed;
}
destroy() {
this.#hasDestroyed = true;
this.clear();
this.#loadFuncMap = null as any;
this.#currentLoadItemMap = null as any;
this.#storageLoadItemMap = null as any;
@ -99,28 +106,32 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
#emitLoad(item: LoadItem) {
const assetId = getAssetIdFromElement(item.element);
const storageItem = this.#storageLoadItemMap[assetId];
if (storageItem) {
if (storageItem.startTime < item.startTime) {
if (!this.#hasDestroyed) {
if (storageItem) {
if (storageItem.startTime < item.startTime) {
this.#storageLoadItemMap[assetId] = item;
this.trigger('load', { ...item, countTime: item.endTime - item.startTime });
}
} else {
this.#storageLoadItemMap[assetId] = item;
this.trigger('load', { ...item, countTime: item.endTime - item.startTime });
}
} else {
this.#storageLoadItemMap[assetId] = item;
this.trigger('load', { ...item, countTime: item.endTime - item.startTime });
}
}
#emitError(item: LoadItem) {
const assetId = getAssetIdFromElement(item.element);
const storageItem = this.#storageLoadItemMap?.[assetId];
if (storageItem) {
if (storageItem.startTime < item.startTime) {
if (!this.#hasDestroyed) {
if (storageItem) {
if (storageItem.startTime < item.startTime) {
this.#storageLoadItemMap[assetId] = item;
this.trigger('error', { ...item, countTime: item.endTime - item.startTime });
}
} else {
this.#storageLoadItemMap[assetId] = item;
this.trigger('error', { ...item, countTime: item.endTime - item.startTime });
}
} else {
this.#storageLoadItemMap[assetId] = item;
this.trigger('error', { ...item, countTime: item.endTime - item.startTime });
}
}
@ -130,16 +141,19 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
this.#currentLoadItemMap[assetId] = item;
const loadFunc = this.#loadFuncMap[element.type];
if (typeof loadFunc === 'function') {
if (typeof loadFunc === 'function' && !this.#hasDestroyed) {
item.startTime = Date.now();
loadFunc(element, assets)
.then((result) => {
item.content = result.content;
item.endTime = Date.now();
item.status = 'load';
this.#emitLoad(item);
if (!this.#hasDestroyed) {
item.content = result.content;
item.endTime = Date.now();
item.status = 'load';
this.#emitLoad(item);
}
})
.catch((err: Error) => {
// eslint-disable-next-line no-console
console.warn(`Load element source "${item.source}" fail`, err, element);
item.endTime = Date.now();
item.status = 'error';
@ -159,6 +173,9 @@ export class Loader extends EventEmitter<LoaderEventMap> implements RendererLoad
}
load(element: Element<LoadElementType>, assets: ElementAssets) {
if (this.#hasDestroyed === true) {
return;
}
if (this.#isExistingErrorStorage(element)) {
return;
}

View file

@ -134,6 +134,7 @@ export interface BoardRenderer extends UtilEventEmitter<RendererEventMap> {
drawData(data: Data, opts: RendererDrawOptions): void;
scale(num: number): void;
destroy(): void;
isDestroyed(): boolean;
getLoader(): RendererLoader;
}

View file

@ -19,7 +19,8 @@ export type CursorType =
| 'resize-bottom-left'
| 'resize-bottom-right'
| 'drag-default'
| 'drag-active';
| 'drag-active'
| 'default';
export interface CoreEventCursor {
type: CursorType | string | null;

View file

@ -26,6 +26,7 @@ export interface RendererLoader extends UtilEventEmitter<LoaderEventMap> {
getLoadItemMap(): LoadItemMap;
setLoadItemMap(itemMap: LoadItemMap): void;
destroy(): void;
isDestroyed(): boolean;
}
export interface RendererDrawOptions {

View file

@ -0,0 +1,50 @@
export const imageBase64 = `data:image/png;base64,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`;
export const svg = `<svg viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M336 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" fill="#1296db"></path><path d="M688 421m-48 0a48 48 0 1 0 96 0 48 48 0 1 0-96 0Z" fill="#1296db"></path><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m263 711c-34.2 34.2-74 61-118.3 79.8C611 874.2 562.3 884 512 884c-50.3 0-99-9.8-144.8-29.2-44.3-18.7-84.1-45.6-118.3-79.8-34.2-34.2-61-74-79.8-118.3C149.8 611 140 562.3 140 512s9.8-99 29.2-144.8c18.7-44.3 45.6-84.1 79.8-118.3 34.2-34.2 74-61 118.3-79.8C413 149.8 461.7 140 512 140c50.3 0 99 9.8 144.8 29.2 44.3 18.7 84.1 45.6 118.3 79.8 34.2 34.2 61 74 79.8 118.3C874.2 413 884 461.7 884 512s-9.8 99-29.2 144.8c-18.7 44.3-45.6 84.1-79.8 118.2z" fill="#1296db"></path><path d="M664 533h-48.1c-4.2 0-7.8 3.2-8.1 7.4C604 589.9 562.5 629 512 629s-92.1-39.1-95.8-88.6c-0.3-4.2-3.9-7.4-8.1-7.4H360c-4.6 0-8.2 3.8-8 8.4 4.4 84.3 74.5 151.6 160 151.6s155.6-67.3 160-151.6c0.2-4.6-3.4-8.4-8-8.4z" fill="#1296db"></path></svg>`;
export const html = `
<style>
.btn-box {
margin: 10px 0;
}
.btn {
line-height: 1.5715;
position: relative;
display: inline-block;
font-weight: 400;
white-space: nowrap;
text-align: center;
background-image: none;
border: 1px solid transparent;
box-shadow: 0 2px #00000004;
cursor: pointer;
user-select: none;
height: 32px;
padding: 4px 15px;
font-size: 14px;
border-radius: 2px;
color: #000000d9;
background: #fff;
border-color: #d9d9d9;
}
.btn-primary {
color: #fff;
background: #1890ff;
border-color: #1890ff;
text-shadow: 0 -1px 0 rgb(0 0 0 / 12%);
box-shadow: 0 2px #0000000b;
}
</style>
<div>
<div class="btn-box">
<button class="btn">
<span>Button</span>
</button>
</div>
<div class="btn-box">
<button class="btn btn-primary">
<span>Button Primary</span>
</button>
</div>
</div>
`;

View file

@ -1,7 +1,6 @@
import {
deepClone
} from '../../src/lib/data';
import { deepClone, filterCompactData } from '../../src/lib/data';
import { imageBase64, html, svg } from '../_assets/base';
import type { Data } from '@idraw/types';
describe('@idraw/util: lib/data', () => {
const json = {
@ -12,12 +11,12 @@ describe('@idraw/util: lib/data', () => {
{
num: 1,
str: 'a',
bool: false,
bool: false
},
{
num: 2,
str: 'b',
bool: false,
bool: false
}
],
json: {
@ -27,10 +26,10 @@ describe('@idraw/util: lib/data', () => {
json: {
num: 11,
str: 'bbbb',
bool: false,
bool: false
}
}
}
};
const json2 = deepClone(json);
json2.json.json.num *= 2;
@ -41,7 +40,180 @@ describe('@idraw/util: lib/data', () => {
expect(result).toStrictEqual(json2);
});
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);
const expectData: Data = {
elements: [
{
uuid: 'b37213ce-d711-cbb3-51ac-d8081c19f127',
type: 'image',
x: 0,
y: 0,
w: 100,
h: 100,
detail: { src: '@assets/1919ff71-124e-2766-23bb-9a251bf3241c' }
},
{
uuid: '39308517-e10f-76df-43a9-50ed7295e61e',
type: 'svg',
x: 0,
y: 0,
w: 100,
h: 100,
detail: { svg: '@assets/b9b92016-5290-54e8-9668-807574952823' }
},
{
uuid: 'ef934ab7-a32e-040c-9ac0-ed193405e6e4',
type: 'html',
x: 0,
y: 0,
w: 100,
h: 100,
detail: { html: '@assets/34017fa0-2d48-2506-3464-238f34642b5c' }
},
{
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: '@assets/1919ff71-124e-2766-23bb-9a251bf3241c' }
},
{
uuid: 'b60e64e8-833e-e112-d7eb-1ab6e7d6870c',
type: 'svg',
x: 0,
y: 0,
w: 100,
h: 100,
detail: { svg: '@assets/b9b92016-5290-54e8-9668-807574952823' }
},
{
uuid: '61f2a61e-cdd5-ae36-983f-686ba8e35973',
type: 'html',
x: 0,
y: 0,
w: 100,
h: 100,
detail: { html: '@assets/34017fa0-2d48-2506-3464-238f34642b5c' }
}
]
}
}
],
assets: {
'@assets/1919ff71-124e-2766-23bb-9a251bf3241c': {
type: 'image',
value: imageBase64
},
'@assets/b9b92016-5290-54e8-9668-807574952823': {
type: 'svg',
value: svg
},
'@assets/34017fa0-2d48-2506-3464-238f34642b5c': {
type: 'html',
value: html
}
}
};
expect(compactData).toStrictEqual(expectData);
const data2: Data = deepClone<Data>(expectData);
const compactData2 = filterCompactData(data2);
expect(compactData2).toStrictEqual(expectData);
});
});

View file

@ -74,4 +74,4 @@ export {
updateElementInList
} from './lib/handle-element';
export { deepResizeGroupElement } from './lib/resize-element';
export { calcViewCenterContent } from './lib/view-content';
export { calcViewCenterContent, calcViewCenter } from './lib/view-content';

View file

@ -119,7 +119,7 @@ export function filterCompactData(data: Data, opts?: { loadItemMap?: LoadItemMap
type: 'image',
value: loadItemMap[src].source as string
};
} else {
} else if (!assets[src]) {
const assetUUID = createAssetId(src);
if (!assets[assetUUID]) {
assets[assetUUID] = {
@ -131,34 +131,40 @@ export function filterCompactData(data: Data, opts?: { loadItemMap?: LoadItemMap
}
} else if (elem.type === 'svg') {
const svg = (elem as Element<'svg'>).detail.svg;
const assetUUID = createAssetId(svg);
if (isAssetId(svg) && !assets[svg] && loadItemMap[svg] && typeof loadItemMap[svg]?.source === 'string') {
assets[svg] = {
type: 'svg',
value: loadItemMap[svg].source as string
};
} else if (!assets[assetUUID]) {
assets[assetUUID] = {
type: 'svg',
value: svg
};
} else if (!assets[svg]) {
const assetUUID = createAssetId(svg);
if (!assets[assetUUID]) {
assets[assetUUID] = {
type: 'svg',
value: svg
};
}
(elem as Element<'svg'>).detail.svg = assetUUID;
}
(elem as Element<'svg'>).detail.svg = assetUUID;
} else if (elem.type === 'html') {
const html = (elem as Element<'html'>).detail.html;
const assetUUID = createAssetId(html);
if (isAssetId(html) && !assets[html] && loadItemMap[html] && typeof loadItemMap[html]?.source === 'string') {
assets[html] = {
type: 'html',
value: loadItemMap[html].source as string
};
} else if (!assets[assetUUID]) {
assets[assetUUID] = {
type: 'html',
value: html
};
} else if (!assets[html]) {
const assetUUID = createAssetId(html);
if (!assets[assetUUID]) {
assets[assetUUID] = {
type: 'html',
value: html
};
}
(elem as Element<'html'>).detail.html = assetUUID;
}
(elem as Element<'html'>).detail.html = assetUUID;
} else if (elem.type === 'group' && Array.isArray((elem as Element<'group'>).detail.children)) {
const groupAssets = (elem as Element<'group'>).detail.assets || {};
Object.keys(groupAssets).forEach((assetId) => {

View file

@ -55,6 +55,10 @@ export class EventEmitter<T extends Record<string, any>> implements UtilEventEmi
}
destroy() {
this.clear();
}
clear() {
this.#listeners.clear();
}
}

View file

@ -1,4 +1,4 @@
import type { Data, ViewSizeInfo, Element, ElementSize } from 'idraw';
import type { Data, ViewSizeInfo, Element, ElementSize, ViewScaleInfo, PointSize } from '@idraw/types';
import { rotateElementVertexes } from './rotate';
import {} from './view-calc';
import { formatNumber } from './number';
@ -12,12 +12,12 @@ interface ViewCenterContentResult {
export function calcViewCenterContent(data: Data, opts: { viewSizeInfo: ViewSizeInfo }): ViewCenterContentResult {
let offsetX: number = 0;
let offsetY: number = 0;
let scale: number = 0;
let scale: number = 1;
let contentX: number = 0;
let contentY: number = 0;
let contentW: number = 0;
let contentH: number = 0;
let contentX: number = data?.elements?.[0]?.x || 0;
let contentY: number = data?.elements?.[0]?.y || 0;
let contentW: number = data?.elements?.[0]?.w || 0;
let contentH: number = data?.elements?.[0]?.h || 0;
const { width, height } = opts.viewSizeInfo;
@ -56,8 +56,8 @@ export function calcViewCenterContent(data: Data, opts: { viewSizeInfo: ViewSize
const scaleW = formatNumber(width / contentW, { decimalPlaces: 4 });
const scaleH = formatNumber(height / contentH, { decimalPlaces: 4 });
scale = Math.min(scaleW, scaleH, 1);
offsetX = (contentW * scale - width) / 2 / scale;
offsetY = (contentH * scale - height) / 2 / scale;
offsetX = (contentW * scale - width) / 2 / scale + contentX;
offsetY = (contentH * scale - height) / 2 / scale + contentY;
}
const result: ViewCenterContentResult = {
@ -65,5 +65,24 @@ export function calcViewCenterContent(data: Data, opts: { viewSizeInfo: ViewSize
offsetY: formatNumber(offsetY, { decimalPlaces: 0 }),
scale
};
return result;
}
export function calcViewCenter(opts?: { viewScaleInfo: ViewScaleInfo; viewSizeInfo: ViewSizeInfo }): PointSize {
let x = 0;
let y = 0;
if (opts) {
const { viewScaleInfo, viewSizeInfo } = opts;
const { offsetLeft, offsetTop, scale } = viewScaleInfo;
const { width, height } = viewSizeInfo;
x = 0 - offsetLeft + width / scale / 2;
y = 0 - offsetTop + height / scale / 2;
}
const p: PointSize = {
x,
y
};
return p;
}