feat: add middleware texteditor and enhance middlewares

This commit is contained in:
chenshenhai 2023-11-25 10:28:08 +08:00
parent 69e49f2dbe
commit 532f604389
15 changed files with 324 additions and 61 deletions

View file

@ -307,9 +307,9 @@ export class Board<T extends BoardExtendEvent = BoardExtendEvent> {
}
use(middleware: BoardMiddleware<any, any>) {
const { viewContent } = this._opts;
const { viewContent, container } = this._opts;
const { _sharer: sharer, _viewer: viewer, _calculator: calculator, _eventHub: eventHub } = this;
const obj = middleware({ viewContent, sharer, viewer, calculator, eventHub: eventHub as UtilEventEmitter<any> });
const obj = middleware({ viewContent, sharer, viewer, calculator, eventHub: eventHub as UtilEventEmitter<any>, container });
this._middlewares.push(middleware);
this._activeMiddlewareObjs.push(obj);
}

View file

@ -8,24 +8,26 @@ export { MiddlewareSelector, middlewareEventSelect } from './middleware/selector
export { MiddlewareScroller } from './middleware/scroller';
export { MiddlewareScaler, middlewareEventScale } from './middleware/scaler';
export { MiddlewareRuler, middlewareEventRuler } from './middleware/ruler';
export { MiddlewareTextEditor, middlewareEventTextEdit } from './middleware/text-editor';
export class Core {
private _board: Board<CoreEvent>;
private _opts: CoreOptions;
private _container: HTMLDivElement;
private _canvas: HTMLCanvasElement;
#board: Board<CoreEvent>;
// #opts: CoreOptions;
// #canvas: HTMLCanvasElement;
#container: HTMLDivElement;
constructor(container: HTMLDivElement, opts: CoreOptions) {
const { devicePixelRatio = 1, width, height } = opts;
this._opts = opts;
this._container = container;
// this.#opts = opts;
// this.#canvas = canvas;
this.#container = container;
const canvas = document.createElement('canvas');
this._canvas = canvas;
this.#initContainer();
container.appendChild(canvas);
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
const viewContent = createBoardContexts(ctx, { devicePixelRatio });
const board = new Board<CoreEvent>({ viewContent });
const board = new Board<CoreEvent>({ viewContent, container });
const sharer = board.getSharer();
sharer.setActiveViewSizeInfo({
width,
@ -34,7 +36,7 @@ export class Core {
contextWidth: width,
contextHeight: height
});
this._board = board;
this.#board = board;
this.resize(sharer.getActiveViewSizeInfo());
const eventHub = board.getEventHub();
new Cursor(container, {
@ -42,27 +44,32 @@ export class Core {
});
}
#initContainer() {
const container = this.#container;
container.style.position = 'relative';
}
use(middleware: BoardMiddleware<any, any>) {
this._board.use(middleware);
this.#board.use(middleware);
}
setData(data: Data) {
validateElements(data?.elements || []);
this._board.setData(data);
this.#board.setData(data);
}
getData(): Data | null {
return this._board.getData();
return this.#board.getData();
}
scale(opts: { scale: number; point: PointSize }) {
this._board.scale(opts);
const viewer = this._board.getViewer();
this.#board.scale(opts);
const viewer = this.#board.getViewer();
viewer.drawFrame();
}
resize(newViewSize: Partial<ViewSizeInfo>) {
const { _board: board } = this;
const board = this.#board;
const sharer = board.getSharer();
const viewSizeInfo = sharer.getActiveViewSizeInfo();
board.resize({
@ -72,21 +79,21 @@ export class Core {
}
clear() {
this._board.clear();
this.#board.clear();
}
on<T extends keyof CoreEvent>(name: T, callback: (e: CoreEvent[T]) => void) {
const eventHub = this._board.getEventHub();
const eventHub = this.#board.getEventHub();
eventHub.on(name, callback);
}
off<T extends keyof CoreEvent>(name: T, callback: (e: CoreEvent[T]) => void) {
const eventHub = this._board.getEventHub();
const eventHub = this.#board.getEventHub();
eventHub.off(name, callback);
}
trigger<T extends keyof CoreEvent>(name: T, e: CoreEvent[T]) {
const eventHub = this._board.getEventHub();
const eventHub = this.#board.getEventHub();
eventHub.trigger(name, e);
}
}

View file

@ -27,7 +27,6 @@ export class Cursor {
const { _eventHub: eventHub } = this;
this._resetCursor('auto');
eventHub.on('cursor', (e) => {
// console.log('e ======= ', e);
if (e.type === 'over-element' || !e.type) {
this._resetCursor('auto');
} else if (typeof e.type === 'string' && e.type?.startsWith('resize-')) {

View file

@ -8,11 +8,18 @@ export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEvent> =
const key = 'RULE';
const { viewContent, viewer, eventHub } = opts;
const { helperContext, underContext } = viewContent;
let showRuler: boolean = true;
let show: boolean = true;
let showGrid: boolean = true;
eventHub.on(middlewareEventRuler, (e: { show: boolean }) => {
eventHub.on(middlewareEventRuler, (e: { show: boolean; showGrid: boolean }) => {
if (typeof e?.show === 'boolean') {
showRuler = e.show;
show = e.show;
}
if (typeof e?.showGrid === 'boolean') {
showGrid = e.showGrid;
}
if (typeof e?.show === 'boolean' || typeof e?.showGrid === 'boolean') {
viewer.drawFrame();
}
});
@ -20,7 +27,7 @@ export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEvent> =
mode: key,
isDefault: true,
beforeDrawFrame: ({ snapshot }) => {
if (showRuler === true) {
if (show === true) {
const viewScaleInfo = getViewScaleInfoFromSnapshot(snapshot);
const viewSizeInfo = getViewSizeInfoFromSnapshot(snapshot);
drawRulerBackground(helperContext, { viewScaleInfo, viewSizeInfo });
@ -31,12 +38,14 @@ export const MiddlewareRuler: BoardMiddleware<Record<string, any>, CoreEvent> =
const yList = calcYRulerScaleList({ viewScaleInfo, viewSizeInfo });
drawYRuler(helperContext, { scaleList: yList });
drawUnderGrid(underContext, {
xList,
yList,
viewScaleInfo,
viewSizeInfo
});
if (showGrid === true) {
drawUnderGrid(underContext, {
xList,
yList,
viewScaleInfo,
viewSizeInfo
});
}
}
}
};

View file

@ -1,5 +1,5 @@
import type { ViewScaleInfo, ViewSizeInfo, ViewContext2D } from '@idraw/types';
import { Context2D, formatNumber, rotateByCenter } from '@idraw/util';
import { formatNumber, rotateByCenter } from '@idraw/util';
const rulerSize = 16;
const background = '#FFFFFFA8';
@ -11,6 +11,7 @@ const fontSize = 10;
const fontWeight = 100;
const gridColor = '#AAAAAA30';
const gridKeyColor = '#AAAAAA70';
const lineSize = 1;
// const rulerUnit = 10;
// const rulerKeyUnit = 100;
@ -30,7 +31,7 @@ function calcRulerScaleList(opts: { axis: 'X' | 'Y'; scale: number; viewLength:
let rulerUnit = 10;
rulerUnit = formatNumber(rulerUnit / scale, { decimalPlaces: 0 });
rulerUnit = Math.max(1, Math.min(rulerUnit, 1000));
rulerUnit = Math.max(10, Math.min(rulerUnit, 1000));
const rulerKeyUnit = rulerUnit * 10;
const rulerSubKeyUnit = rulerUnit * 5;
@ -104,6 +105,8 @@ export function drawXRuler(
ctx.moveTo(item.position, scaleDrawStart);
ctx.lineTo(item.position, item.isKeyNum ? keyScaleDrawEnd : item.isSubKeyNum ? subKeyScaleDrawEnd : scaleDrawEnd);
ctx.closePath();
ctx.lineWidth = lineSize;
ctx.setLineDash([]);
ctx.fillStyle = scaleColor;
ctx.stroke();
if (item.isKeyNum) {
@ -141,6 +144,8 @@ export function drawYRuler(
ctx.lineTo(item.isKeyNum ? keyScaleDrawEnd : item.isSubKeyNum ? subKeyScaleDrawEnd : scaleDrawEnd, item.position);
ctx.closePath();
ctx.fillStyle = scaleColor;
ctx.lineWidth = lineSize;
ctx.setLineDash([]);
ctx.stroke();
if (item.showNum === true) {
const textX = fontStart;
@ -181,6 +186,8 @@ export function drawRulerBackground(
ctx.closePath();
ctx.fillStyle = background;
ctx.fill();
ctx.lineWidth = lineSize;
ctx.setLineDash([]);
ctx.strokeStyle = borderColor;
ctx.stroke();
}
@ -206,9 +213,9 @@ export function drawUnderGrid(
} else {
ctx.strokeStyle = gridColor;
}
ctx.lineWidth = 1;
ctx.closePath();
ctx.lineWidth = lineSize;
ctx.setLineDash([]);
ctx.stroke();
}

View file

@ -6,6 +6,8 @@ export const middlewareEventScale = '@middleware/scale';
export const MiddlewareScaler: BoardMiddleware<Record<string, any>, CoreEvent> = (opts) => {
const key = 'SCALE';
const { viewer, sharer, eventHub } = opts;
const maxScale = 50;
const minScale = 0.05;
return {
mode: key,
@ -19,6 +21,10 @@ export const MiddlewareScaler: BoardMiddleware<Record<string, any>, CoreEvent> =
} else if (deltaY > 0) {
newScaleNum = scale * 0.9;
}
if (newScaleNum < minScale || newScaleNum > maxScale) {
return;
}
const { moveX, moveY } = viewer.scale({ scale: newScaleNum, point });
viewer.scroll({ moveX, moveY });
viewer.drawFrame();

View file

@ -72,11 +72,11 @@ function calcScrollerInfo(viewScaleInfo: ViewScaleInfo, viewSizeInfo: ViewSizeIn
let xSize = 0;
let ySize = 0;
xSize = Math.max(sliderMinSize, width - (Math.abs(offsetLeft) + Math.abs(offsetRight)));
xSize = Math.max(sliderMinSize, width - lineSize * 2 - (Math.abs(offsetLeft) + Math.abs(offsetRight)));
if (xSize >= width) {
xSize = width;
}
ySize = Math.max(sliderMinSize, height - (Math.abs(offsetTop) + Math.abs(offsetBottom)));
ySize = Math.max(sliderMinSize, height - lineSize * 2 - (Math.abs(offsetTop) + Math.abs(offsetBottom)));
if (ySize >= height) {
ySize = height;
}
@ -246,6 +246,7 @@ function drawScrollerInfo(helperContext: ViewContext2D, opts: { viewScaleInfo: V
});
ctx.globalAlpha = 1;
return {
xThumbRect,
yThumbRect

View file

@ -51,6 +51,7 @@ import {
// keyDebugStartHorizontal,
// keyDebugStartVertical
} from './config';
import { middlewareEventTextEdit } from '../text-editor';
export const middlewareEventSelect: string = '@middleware/select';
@ -486,6 +487,12 @@ export const MiddlewareSelector: BoardMiddleware<DeepSelectorSharedStorage, Core
viewer.drawFrame();
return;
}
} else if (target.elements.length === 1 && target.elements[0]?.type === 'text') {
eventHub.trigger(middlewareEventTextEdit, {
element: target.elements[0],
groupQueue: sharer.getSharedStorage(keyGroupQueue) || [],
viewScaleInfo: sharer.getActiveViewScaleInfo()
});
}
sharer.setSharedStorage(keyActionType, null);
},

View file

@ -0,0 +1,160 @@
import type { BoardMiddleware, CoreEvent, Element, ElementSize, ViewScaleInfo } from '@idraw/types';
import { limitAngle, getDefaultElementDetailConfig } from '@idraw/util';
export const middlewareEventTextEdit = '@middleware/text-edit';
type TextEditEvent = {
element: Element<'text'>;
groupQueue: Element<'group'>[];
viewScaleInfo: ViewScaleInfo;
};
const defaultElementDetail = getDefaultElementDetailConfig();
export const MiddlewareTextEditor: BoardMiddleware<Record<string, any>, CoreEvent> = (opts) => {
const key = 'SELECT';
const { eventHub, viewContent, viewer } = opts;
const canvas = viewContent.boardContext.canvas;
const textarea = document.createElement('textarea');
const canvasWrapper = document.createElement('div');
const container = opts.container || document.body;
const mask = document.createElement('div');
let activeElem: Element<'text'> | null = null;
canvasWrapper.appendChild(textarea);
canvasWrapper.style.position = 'absolute';
mask.appendChild(canvasWrapper);
mask.style.position = 'fixed';
mask.style.top = '0';
mask.style.bottom = '0';
mask.style.left = '0';
mask.style.right = '0';
mask.style.display = 'none';
container.appendChild(mask);
const showTextArea = (e: TextEditEvent) => {
resetCanvasWrapper();
resetTextArea(e);
mask.style.display = 'block';
};
const hideTextArea = () => {
mask.style.display = 'none';
activeElem = null;
};
const getCanvasRect = () => {
const clientRect = canvas.getBoundingClientRect() as DOMRect;
const { left, top, width, height } = clientRect;
return { left, top, width, height };
};
const createBox = (opts: { size: ElementSize; parent: HTMLDivElement }) => {
const { size, parent } = opts;
const div = document.createElement('div');
const { x, y, w, h } = size;
const angle = limitAngle(size.angle || 0);
div.style.position = 'absolute';
div.style.left = `${x}px`;
div.style.top = `${y}px`;
div.style.width = `${w}px`;
div.style.height = `${h}px`;
div.style.transform = `rotate(${angle}deg)`;
parent.appendChild(div);
return div;
};
const resetTextArea = (e: TextEditEvent) => {
const { viewScaleInfo, element, groupQueue } = e;
const { scale, offsetTop, offsetLeft } = viewScaleInfo;
if (canvasWrapper.children) {
Array.from(canvasWrapper.children).forEach((child) => {
child.remove();
});
}
let parent = canvasWrapper;
for (let i = 0; i < groupQueue.length; i++) {
const group = groupQueue[i];
const { x, y, w, h } = group;
const angle = limitAngle(group.angle || 0);
const size = {
x: x * scale,
y: y * scale,
w: w * scale,
h: h * scale,
angle
};
if (i === 0) {
size.x += offsetLeft;
size.y += offsetTop;
}
parent = createBox({ size, parent });
}
const detail = {
...defaultElementDetail,
...element.detail
};
textarea.style.position = 'absolute';
textarea.style.left = `${element.x * scale}px`;
textarea.style.top = `${element.y * scale}px`;
textarea.style.width = `${element.w * scale}px`;
textarea.style.height = `${element.h * scale}px`;
textarea.style.transform = `rotate(${limitAngle(element.angle || 0)}deg)`;
textarea.style.border = 'none';
textarea.style.resize = 'none';
textarea.style.overflow = 'hidden';
textarea.style.wordBreak = 'break-all';
textarea.style.background = '#FFFFFF';
textarea.style.color = '#333333';
textarea.style.fontSize = `${detail.fontSize * scale}px`;
textarea.style.lineHeight = `${detail.lineHeight * scale}px`;
textarea.style.fontFamily = detail.fontFamily;
textarea.style.fontWeight = `${detail.fontWeight}`;
textarea.value = detail.text || '';
parent.appendChild(textarea);
};
const resetCanvasWrapper = () => {
const { left, top, width, height } = getCanvasRect();
canvasWrapper.style.position = 'absolute';
canvasWrapper.style.overflow = 'hidden';
canvasWrapper.style.top = `${top}px`;
canvasWrapper.style.left = `${left}px`;
canvasWrapper.style.width = `${width}px`;
canvasWrapper.style.height = `${height}px`;
// canvasWrapper.style.background = '#000000';
};
mask.addEventListener('click', () => {
hideTextArea();
});
textarea.addEventListener('click', (e) => {
e.stopPropagation();
});
textarea.addEventListener('input', (e) => {
if (activeElem) {
activeElem.detail.text = (e.target as any).value || '';
viewer.drawFrame();
}
});
textarea.addEventListener('blur', () => {
hideTextArea();
});
eventHub.on(middlewareEventTextEdit, (e: TextEditEvent) => {
if (e?.element && e?.element?.type === 'text') {
activeElem = e.element;
}
showTextArea(e);
});
return {
mode: key,
isDefault: true
};
};

View file

@ -203,6 +203,28 @@ const data: Data = {
detail: {
background: '#cddc39'
}
},
{
uuid: 'text-002',
name: 'text-002',
x: 50,
y: 200,
w: 100,
h: 50,
// angle: 30,
type: 'text',
detail: {
fontSize: 16,
// text: 'Hello Text Hello Text Hello Text Hello Text Hello Text Hello Text',
text: 'Hello Text',
fontWeight: 'bold',
color: '#000000',
borderRadius: 30,
borderWidth: 2,
borderColor: '#ff5722',
textAlign: 'center',
verticalAlign: 'middle'
}
}
]
};

View file

@ -0,0 +1,29 @@
import { middlewareEventScale, middlewareEventSelect } from '@idraw/core';
import type { CoreEvent } from '@idraw/types';
export interface IDrawEventKeys {
select: typeof middlewareEventSelect;
scale: typeof middlewareEventScale;
change: 'change';
}
export type IDrawEvent = CoreEvent & {
[key: string]: any;
};
// TODO
const EventKeys = {} as {
select: typeof middlewareEventSelect;
scale: typeof middlewareEventScale;
change: 'change';
};
Object.defineProperty(EventKeys, 'select', {
value: middlewareEventSelect,
writable: false
});
Object.defineProperty(EventKeys, 'scale', {
value: middlewareEventScale,
writable: false
});
export { EventKeys };

View file

@ -1,43 +1,45 @@
import { Core, MiddlewareSelector, MiddlewareScroller, MiddlewareScaler, MiddlewareRuler } from '@idraw/core';
import type { PointSize, IDrawOptions, Data, ViewSizeInfo, IDrawEvent } from '@idraw/types';
import { Core, MiddlewareSelector, MiddlewareScroller, MiddlewareScaler, MiddlewareRuler, MiddlewareTextEditor, middlewareEventSelect } from '@idraw/core';
import type { PointSize, IDrawOptions, Data, ViewSizeInfo } from '@idraw/types';
import type { IDrawEvent } from './event';
export class iDraw {
private _core: Core;
private _opts: IDrawOptions;
#core: Core;
// private #opts: IDrawOptions;
constructor(mount: HTMLDivElement, opts: IDrawOptions) {
const core = new Core(mount, opts);
this._core = core;
this._opts = opts;
this.#core = core;
// this.#opts = opts;
core.use(MiddlewareScroller);
core.use(MiddlewareSelector);
core.use(MiddlewareScaler);
core.use(MiddlewareRuler);
core.use(MiddlewareTextEditor);
}
setData(data: Data) {
this._core.setData(data);
this.#core.setData(data);
}
getData(): Data | null {
return this._core.getData();
return this.#core.getData();
}
selectElement() {
// TODO
selectElements(uuids: string[]) {
this.trigger(middlewareEventSelect, { uuids });
}
selectElementByIndex() {
// TODO
}
// selectElementByIndex() {
// // TODO
// }
cancelElement() {
// TODO
}
cancelElementByIndex() {
// TODO
}
// cancelElementByIndex() {
// // TODO
// }
updateElement() {
// TODO
@ -76,23 +78,23 @@ export class iDraw {
}
scale(opts: { scale: number; point: PointSize }) {
this._core.scale(opts);
this.#core.scale(opts);
}
resize(opts: Partial<ViewSizeInfo>) {
this._core.resize(opts);
this.#core.resize(opts);
}
on<T extends keyof IDrawEvent>(name: T, callback: (e: IDrawEvent[T]) => void) {
this._core.on(name, callback);
this.#core.on(name, callback);
}
off<T extends keyof IDrawEvent>(name: T, callback: (e: IDrawEvent[T]) => void) {
this._core.off(name, callback);
this.#core.off(name, callback);
}
trigger<T extends keyof IDrawEvent>(name: T, e: IDrawEvent[T]) {
this._core.trigger(name, e);
this.#core.trigger(name, e);
}
// scrollLeft() {

View file

@ -1,6 +1,17 @@
export { iDraw } from './idraw';
export type { IDrawEvent, IDrawEventKeys } from './event';
export type * from '@idraw/types';
export { Core, MiddlewareSelector, MiddlewareScroller, MiddlewareScaler } from '@idraw/core';
export {
Core,
MiddlewareSelector,
middlewareEventSelect,
MiddlewareScroller,
MiddlewareScaler,
middlewareEventScale,
MiddlewareRuler,
middlewareEventRuler,
MiddlewareTextEditor
} from '@idraw/core';
export { Renderer } from '@idraw/renderer';
export {
delay,

View file

@ -85,6 +85,8 @@ export interface BoardMiddlewareOptions<S extends Record<any | symbol, any> = Re
viewer: BoardViewer;
calculator: ViewCalculator;
eventHub: UtilEventEmitter<E>;
container?: HTMLDivElement;
canvas?: HTMLCanvasElement;
}
export type BoardMiddleware<S extends Record<any | symbol, any> = any, E extends BoardExtendEvent = Record<string, any>> = (
@ -93,6 +95,7 @@ export type BoardMiddleware<S extends Record<any | symbol, any> = any, E extends
export interface BoardOptions {
viewContent: ViewContent;
container?: HTMLDivElement;
}
export interface BoardViewerFrameSnapshot<S extends Record<any | symbol, any> = any> {

View file

@ -2,7 +2,7 @@ import type { DefaultElementDetailConfig } from '@idraw/types';
export function getDefaultElementDetailConfig(): DefaultElementDetailConfig {
const config: DefaultElementDetailConfig = {
boxSizing: 'border-box',
boxSizing: 'center-line',
borderWidth: 0,
borderColor: '#000000',
shadowColor: '#000000',