From 532f6043898cfce73090cf86f6a7db3b1deeea02 Mon Sep 17 00:00:00 2001 From: chenshenhai Date: Sat, 25 Nov 2023 10:28:08 +0800 Subject: [PATCH] feat: add middleware texteditor and enhance middlewares --- packages/board/src/index.ts | 4 +- packages/core/src/index.ts | 45 ++--- packages/core/src/lib/cursor.ts | 1 - packages/core/src/middleware/ruler/index.ts | 29 ++-- packages/core/src/middleware/ruler/util.ts | 15 +- packages/core/src/middleware/scaler/index.ts | 6 + packages/core/src/middleware/scroller/util.ts | 5 +- .../core/src/middleware/selector/index.ts | 7 + .../core/src/middleware/text-editor/index.ts | 160 ++++++++++++++++++ packages/idraw/dev/data.ts | 22 +++ packages/idraw/src/event.ts | 29 ++++ packages/idraw/src/idraw.ts | 44 ++--- packages/idraw/src/index.ts | 13 +- packages/types/src/lib/board.ts | 3 + packages/util/src/lib/config.ts | 2 +- 15 files changed, 324 insertions(+), 61 deletions(-) create mode 100644 packages/core/src/middleware/text-editor/index.ts create mode 100644 packages/idraw/src/event.ts diff --git a/packages/board/src/index.ts b/packages/board/src/index.ts index 817f34a..12b9230 100644 --- a/packages/board/src/index.ts +++ b/packages/board/src/index.ts @@ -307,9 +307,9 @@ export class Board { } use(middleware: BoardMiddleware) { - 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 }); + const obj = middleware({ viewContent, sharer, viewer, calculator, eventHub: eventHub as UtilEventEmitter, container }); this._middlewares.push(middleware); this._activeMiddlewareObjs.push(obj); } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ec2d2c2..0cadf05 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -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; - private _opts: CoreOptions; - private _container: HTMLDivElement; - private _canvas: HTMLCanvasElement; + #board: Board; + // #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({ viewContent }); + const board = new Board({ 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) { - 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) { - 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(name: T, callback: (e: CoreEvent[T]) => void) { - const eventHub = this._board.getEventHub(); + const eventHub = this.#board.getEventHub(); eventHub.on(name, callback); } off(name: T, callback: (e: CoreEvent[T]) => void) { - const eventHub = this._board.getEventHub(); + const eventHub = this.#board.getEventHub(); eventHub.off(name, callback); } trigger(name: T, e: CoreEvent[T]) { - const eventHub = this._board.getEventHub(); + const eventHub = this.#board.getEventHub(); eventHub.trigger(name, e); } } diff --git a/packages/core/src/lib/cursor.ts b/packages/core/src/lib/cursor.ts index 48dd105..fe1597f 100644 --- a/packages/core/src/lib/cursor.ts +++ b/packages/core/src/lib/cursor.ts @@ -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-')) { diff --git a/packages/core/src/middleware/ruler/index.ts b/packages/core/src/middleware/ruler/index.ts index 8deb7da..66b86ef 100644 --- a/packages/core/src/middleware/ruler/index.ts +++ b/packages/core/src/middleware/ruler/index.ts @@ -8,11 +8,18 @@ export const MiddlewareRuler: BoardMiddleware, 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, 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, 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 + }); + } } } }; diff --git a/packages/core/src/middleware/ruler/util.ts b/packages/core/src/middleware/ruler/util.ts index 8253c4e..00da596 100644 --- a/packages/core/src/middleware/ruler/util.ts +++ b/packages/core/src/middleware/ruler/util.ts @@ -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(); } diff --git a/packages/core/src/middleware/scaler/index.ts b/packages/core/src/middleware/scaler/index.ts index 59bad76..c3e84b7 100644 --- a/packages/core/src/middleware/scaler/index.ts +++ b/packages/core/src/middleware/scaler/index.ts @@ -6,6 +6,8 @@ export const middlewareEventScale = '@middleware/scale'; export const MiddlewareScaler: BoardMiddleware, 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, 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(); diff --git a/packages/core/src/middleware/scroller/util.ts b/packages/core/src/middleware/scroller/util.ts index ea27464..d4a5f4a 100644 --- a/packages/core/src/middleware/scroller/util.ts +++ b/packages/core/src/middleware/scroller/util.ts @@ -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 diff --git a/packages/core/src/middleware/selector/index.ts b/packages/core/src/middleware/selector/index.ts index 7f727ae..0e153b4 100644 --- a/packages/core/src/middleware/selector/index.ts +++ b/packages/core/src/middleware/selector/index.ts @@ -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; + groupQueue: Element<'group'>[]; + viewScaleInfo: ViewScaleInfo; +}; + +const defaultElementDetail = getDefaultElementDetailConfig(); + +export const MiddlewareTextEditor: BoardMiddleware, 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 + }; +}; diff --git a/packages/idraw/dev/data.ts b/packages/idraw/dev/data.ts index 30944b8..2e34cfe 100644 --- a/packages/idraw/dev/data.ts +++ b/packages/idraw/dev/data.ts @@ -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' + } } ] }; diff --git a/packages/idraw/src/event.ts b/packages/idraw/src/event.ts new file mode 100644 index 0000000..c1d4178 --- /dev/null +++ b/packages/idraw/src/event.ts @@ -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 }; diff --git a/packages/idraw/src/idraw.ts b/packages/idraw/src/idraw.ts index 04794de..d48ac5f 100644 --- a/packages/idraw/src/idraw.ts +++ b/packages/idraw/src/idraw.ts @@ -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) { - this._core.resize(opts); + this.#core.resize(opts); } on(name: T, callback: (e: IDrawEvent[T]) => void) { - this._core.on(name, callback); + this.#core.on(name, callback); } off(name: T, callback: (e: IDrawEvent[T]) => void) { - this._core.off(name, callback); + this.#core.off(name, callback); } trigger(name: T, e: IDrawEvent[T]) { - this._core.trigger(name, e); + this.#core.trigger(name, e); } // scrollLeft() { diff --git a/packages/idraw/src/index.ts b/packages/idraw/src/index.ts index ed791c3..aff5b2e 100644 --- a/packages/idraw/src/index.ts +++ b/packages/idraw/src/index.ts @@ -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, diff --git a/packages/types/src/lib/board.ts b/packages/types/src/lib/board.ts index ee26743..625751e 100644 --- a/packages/types/src/lib/board.ts +++ b/packages/types/src/lib/board.ts @@ -85,6 +85,8 @@ export interface BoardMiddlewareOptions = Re viewer: BoardViewer; calculator: ViewCalculator; eventHub: UtilEventEmitter; + container?: HTMLDivElement; + canvas?: HTMLCanvasElement; } export type BoardMiddleware = any, E extends BoardExtendEvent = Record> = ( @@ -93,6 +95,7 @@ export type BoardMiddleware = any, E extends export interface BoardOptions { viewContent: ViewContent; + container?: HTMLDivElement; } export interface BoardViewerFrameSnapshot = any> { diff --git a/packages/util/src/lib/config.ts b/packages/util/src/lib/config.ts index 8515c0a..5d07da8 100644 --- a/packages/util/src/lib/config.ts +++ b/packages/util/src/lib/config.ts @@ -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',