UNPKG

@leafer-in/editor

Version:
531 lines (422 loc) 22.3 kB
import { IRect, IEventListenerId, IBoundsData, IPointData, IKeyEvent, IGroup, IBox, IBoxInputData, IAlign, IUI, IEditorConfig, IEditorDragStartData, ITransformTool, IUIEvent, IEditPointInputData } from '@leafer-ui/interface' import { Group, Text, AroundHelper, Direction9, ResizeEvent, BoundsHelper, DataHelper, isArray, isString, isNumber, isNull, getPointData, isUndefined } from '@leafer-ui/draw' import { DragEvent, PointerEvent, KeyEvent, RotateEvent, ZoomEvent, MoveEvent } from '@leafer-ui/core' import { IEditBox, IEditor, IEditPoint, IEditPointType } from '@leafer-in/interface' import { updatePointCursor, updateMoveCursor } from '../editor/cursor' import { EditPoint } from './EditPoint' import { EditDataHelper } from '../helper/EditDataHelper' const fourDirection = ['top', 'right', 'bottom', 'left'], editConfig: IEditorConfig = undefined export class EditBox extends Group implements IEditBox { public editor: IEditor public dragging: boolean public gesturing: boolean public moving: boolean public resizing: boolean public rotating: boolean public skewing: boolean public view: IGroup = new Group() // 放置默认编辑工具控制点 public rect: IEditPoint = new EditPoint({ name: 'rect', hitFill: 'all', hitStroke: 'none', strokeAlign: 'center', hitRadius: 5 }) // target rect public circle: IEditPoint = new EditPoint({ name: 'circle', strokeAlign: 'center', around: 'center', cursor: 'crosshair', hitRadius: 5 }) // rotate point public buttons: IGroup = new Group({ around: 'center', hitSelf: false, visible: 0 }) public resizePoints: IEditPoint[] = [] // topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left public rotatePoints: IEditPoint[] = [] // topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left public resizeLines: IEditPoint[] = [] // top, right, bottom, left public enterPoint: IEditPoint public dragPoint: IEditPoint // 正在拖拽的控制点 public dragStartData = {} as IEditorDragStartData public config: IEditorConfig public mergedConfig: IEditorConfig public get mergeConfig(): IEditorConfig { const { config } = this, { mergeConfig, editBox } = this.editor return this.mergedConfig = config && (editBox !== this) ? { ...mergeConfig, ...config } : mergeConfig // 可能会出现多个editBox的情况 } protected _target: IUI public get target(): IUI { return this._target || this.editor.element } // 操作的元素,默认为editor.element public set target(target: IUI) { this._target = target } public get single(): boolean { return !!this._target || this.editor.single } protected _transformTool: ITransformTool public get transformTool(): ITransformTool { return this._transformTool || this.editor } public set transformTool(tool: ITransformTool) { this._transformTool = tool } // fliped public get flipped(): boolean { return this.flippedX || this.flippedY } public get flippedX(): boolean { return this.scaleX < 0 } public get flippedY(): boolean { return this.scaleY < 0 } public get flippedOne(): boolean { return this.scaleX * this.scaleY < 0 } public get canUse(): boolean { return this.app && this.editor.editing } // 编辑框是否处于激活状态 public get canGesture(): boolean { // 是否支持手势 if (!this.canUse) return false const { moveable, resizeable, rotateable } = this.mergeConfig return isString(moveable) || isString(resizeable) || isString(rotateable) } public get canDragLimitAnimate(): boolean { return (this.moving && this.mergeConfig.dragLimitAnimate && this.target.dragBounds) as any as boolean } protected __eventIds: IEventListenerId[] = [] constructor(editor: IEditor) { super() this.editor = editor this.visible = false this.create() this.__listenEvents() } public create() { let rotatePoint: IEditPoint, resizeLine: IEditPoint, resizePoint: IEditPoint const { view, resizePoints, rotatePoints, resizeLines, rect, circle, buttons } = this const arounds: IAlign[] = ['bottom-right', 'bottom', 'bottom-left', 'left', 'top-left', 'top', 'top-right', 'right'] for (let i = 0; i < 8; i++) { rotatePoint = new EditPoint({ name: 'rotate-point', around: arounds[i], width: 15, height: 15, hitFill: "all" }) rotatePoints.push(rotatePoint) this.listenPointEvents(rotatePoint, 'rotate', i) if (i % 2) { resizeLine = new EditPoint({ name: 'resize-line', around: 'center', width: 10, height: 10, hitFill: "all" }) resizeLines.push(resizeLine) this.listenPointEvents(resizeLine, 'resize', i) } resizePoint = new EditPoint({ name: 'resize-point', hitRadius: 5 }) resizePoints.push(resizePoint) this.listenPointEvents(resizePoint, 'resize', i) } this.listenPointEvents(circle, 'rotate', 2) this.listenPointEvents(rect, 'move', 8) // center view.addMany(...rotatePoints, rect, circle, buttons, ...resizeLines, ...resizePoints) this.add(view) } public load(): void { const { target, mergeConfig, single, rect, circle, resizePoints, resizeLines } = this const { stroke, strokeWidth, ignorePixelSnap } = mergeConfig const pointsStyle = this.getPointsStyle() const middlePointsStyle = this.getMiddlePointsStyle() const resizeLinesStyle = this.getResizeLinesStyle() this.visible = !target.locked let resizeP: IRect for (let i = 0; i < 8; i++) { resizeP = resizePoints[i] resizeP.set(this.getPointStyle((i % 2) ? middlePointsStyle[((i - 1) / 2) % middlePointsStyle.length] : pointsStyle[(i / 2) % pointsStyle.length])) resizeP.rotation = ((i - (i % 2 ? 1 : 0)) / 2) * 90 if (i % 2) resizeLines[(i - 1) / 2].set({ pointType: 'resize', rotation: (i - 1) / 2 * 90, ...(resizeLinesStyle[((i - 1) / 2) % resizeLinesStyle.length] || {}) } as IEditPointInputData) } // rotate circle.set(this.getPointStyle(mergeConfig.circle || mergeConfig.rotatePoint || pointsStyle[0])) // rect rect.set({ stroke, strokeWidth, opacity: 1, editConfig, ...(mergeConfig.rect || {}) }) // 编辑框作为底部虚拟元素, 在 unload() 中重置 const rectThrough = isNull(mergeConfig.rectThrough) ? single : mergeConfig.rectThrough rect.hittable = !rectThrough if (rectThrough) { target.syncEventer = rect // 同步给 rect 冒泡,在 target 属性装饰器中重置 this.app.interaction.bottomList = [{ target: rect, proxy: target }] } // 忽略元素像素对齐,在 target 属性装饰器中重置 if (single) DataHelper.stintSet(target.__world, 'ignorePixelSnap', ignorePixelSnap) updateMoveCursor(this) } // 必须来自 editor.update(),需同步更新编辑工具 public update(): void { const { editor } = this const { x, y, scaleX, scaleY, rotation, skewX, skewY, width, height } = this.target.getLayoutBounds('box', editor, true) this.visible = !this.target.locked this.set({ x, y, scaleX, scaleY, rotation, skewX, skewY }) this.updateBounds({ x: 0, y: 0, width, height }) } public unload(): void { this.visible = false if (this.app) this.rect.syncEventer = this.app.interaction.bottomList = null } public updateBounds(bounds: IBoundsData): void { const { editor, mergeConfig, single, rect, circle, buttons, resizePoints, rotatePoints, resizeLines } = this const { editMask } = editor const { middlePoint, resizeable, rotateable, hideOnSmall, editBox, mask, dimOthers, bright, spread, hideRotatePoints, hideResizeLines } = mergeConfig editMask.visible = mask ? true : 0 if (!isUndefined(dimOthers) || !isUndefined(bright)) { // 没有配置时不强制bright editor.setDimOthers(dimOthers) editor.setBright(!!dimOthers || bright) editor.hasDimOthers = true } else if (editor.hasDimOthers) { editor.cancelDimOthers() } if (spread) BoundsHelper.spread(bounds, spread) if (this.view.worldOpacity) { const { width, height } = bounds const smallSize = isNumber(hideOnSmall) ? hideOnSmall : 10 const showPoints = editBox && !(hideOnSmall && width < smallSize && height < smallSize) let point = {} as IPointData, rotateP: IRect, resizeP: IRect, resizeL: IRect for (let i = 0; i < 8; i++) { AroundHelper.toPoint(AroundHelper.directionData[i], bounds, point) resizeP = resizePoints[i] rotateP = rotatePoints[i] resizeP.set(point) rotateP.set(point) // visible resizeP.visible = showPoints && !!(resizeable || rotateable) rotateP.visible = showPoints && rotateable && resizeable && !hideRotatePoints if (i % 2) { // top, right, bottom, left resizeL = resizeLines[(i - 1) / 2] resizeL.set(point) resizeL.visible = resizeP.visible && !hideResizeLines if (resizeP.visible) resizeP.visible = !!middlePoint if (rotateP.visible) rotateP.visible = !!middlePoint if (((i + 1) / 2) % 2) { // top, bottom resizeL.width = width + resizeL.height if (hideOnSmall && resizeP.width * 2 > width) resizeP.visible = false } else { resizeL.width = height + resizeL.height if (hideOnSmall && resizeP.width * 2 > height) resizeP.visible = false } } } // rotate circle.visible = showPoints && rotateable && !!(mergeConfig.circle || mergeConfig.rotatePoint) if (circle.visible) this.layoutCircle() // rect if (rect.path) rect.path = null // line可能会变成path优先模式 rect.set({ ...bounds, visible: single ? editBox : true }) // buttons buttons.visible = showPoints && buttons.children.length > 0 || 0 if (buttons.visible) this.layoutButtons() } else rect.set(bounds) // 需要更新大小 } protected layoutCircle(): void { const { circleDirection, circleMargin, buttonsMargin, buttonsDirection, middlePoint } = this.mergedConfig const direction = fourDirection.indexOf(circleDirection || ((this.buttons.children.length && buttonsDirection === 'bottom') ? 'top' : 'bottom')) this.setButtonPosition(this.circle, direction, circleMargin || buttonsMargin, !!middlePoint) } protected layoutButtons(): void { const { buttons } = this const { buttonsDirection, buttonsFixed, buttonsMargin, middlePoint } = this.mergedConfig const { flippedX, flippedY } = this let index = fourDirection.indexOf(buttonsDirection) if ((index % 2 && flippedX) || ((index + 1) % 2 && flippedY)) { if (buttonsFixed) index = (index + 2) % 4 // flip x / y } const direction = buttonsFixed ? EditDataHelper.getRotateDirection(index, this.flippedOne ? this.rotation : -this.rotation, 4) : index this.setButtonPosition(buttons, direction, buttonsMargin, !!middlePoint) if (buttonsFixed) buttons.rotation = (direction - index) * 90 buttons.scaleX = flippedX ? -1 : 1 buttons.scaleY = flippedY ? -1 : 1 } protected setButtonPosition(buttons: IUI, direction: number, buttonsMargin: number, useMiddlePoint: boolean): void { const point = this.resizePoints[direction * 2 + 1] // 4 map 8 direction const useX = direction % 2 // left / right const sign = (!direction || direction === 3) ? -1 : 1 // top / left = -1 const useWidth = direction % 2 // left / right origin direction const margin = (buttonsMargin + (useWidth ? ((useMiddlePoint ? point.width : 0) + buttons.boxBounds.width) : ((useMiddlePoint ? point.height : 0) + buttons.boxBounds.height)) / 2) * sign if (useX) { buttons.x = point.x + margin buttons.y = point.y } else { buttons.x = point.x buttons.y = point.y + margin } } public getPointStyle(userStyle?: IBoxInputData): IBoxInputData { const { stroke, strokeWidth, pointFill, pointSize, pointRadius } = this.mergedConfig const defaultStyle = { fill: pointFill, stroke, strokeWidth, around: 'center', strokeAlign: 'center', opacity: 1, width: pointSize, height: pointSize, cornerRadius: pointRadius, offsetX: 0, offsetY: 0, editConfig } as IBoxInputData return userStyle ? Object.assign(defaultStyle, userStyle) : defaultStyle } public getPointsStyle(): IBoxInputData[] { const { point } = this.mergedConfig return isArray(point) ? point : [point] } public getMiddlePointsStyle(): IBoxInputData[] { const { middlePoint } = this.mergedConfig return isArray(middlePoint) ? middlePoint : (middlePoint ? [middlePoint] : this.getPointsStyle()) } public getResizeLinesStyle(): IBoxInputData[] { const { resizeLine } = this.mergedConfig return isArray(resizeLine) ? resizeLine : [resizeLine] } // drag public onDragStart(e: DragEvent): void { this.dragging = true const point = this.dragPoint = e.current as IEditPoint, { pointType } = point const { moveable, resizeable, rotateable, skewable, onCopy } = this.mergeConfig // 确定模式 if (pointType === 'move') { // alt复制钩子 if (e.altKey && onCopy && onCopy() && this.editor.single) this.app.interaction.replaceDownTarget(this.target) moveable && (this.moving = true) } else { if (pointType.includes('rotate') || this.isHoldRotateKey(e) || !resizeable) { rotateable && (this.rotating = true) if (pointType === 'resize-rotate') resizeable && (this.resizing = true) else if (point.name === 'resize-line') skewable && (this.skewing = true), this.rotating = false } else if (pointType === 'resize') resizeable && (this.resizing = true) if (pointType === 'skew') skewable && (this.skewing = true) } this.onTransformStart(e) } public onDrag(e: DragEvent): void { const { transformTool, moving, resizing, rotating, skewing } = this if (moving) { transformTool.onMove(e) } else if (resizing || rotating || skewing) { const point = e.current as IEditPoint if (point.pointType) this.enterPoint = point// 防止变化 if (rotating) transformTool.onRotate(e) if (resizing) transformTool.onScale(e) if (skewing) transformTool.onSkew(e) } updatePointCursor(this, e) } public onDragEnd(e: DragEvent): void { this.onTransformEnd(e) this.dragPoint = null } // 操作事件共用 public onTransformStart(e: IUIEvent): void { if (this.moving || this.gesturing) this.editor.opacity = this.mergedConfig.hideOnMove ? 0 : 1 // move if (this.resizing) ResizeEvent.resizingKeys = this.editor.leafList.keys // 记录正在resize中的元素列表 const { dragStartData, target } = this dragStartData.x = e.x dragStartData.y = e.y dragStartData.totalOffset = getPointData() // 缩放、旋转造成的总偏移量,一般用于手势操作的move纠正 dragStartData.point = { x: target.x, y: target.y } // 用于移动 dragStartData.bounds = { ...target.getLayoutBounds('box', 'local') } // 用于resize dragStartData.rotation = target.rotation // 用于旋转 } public onTransformEnd(e: IUIEvent): void { if (this.canDragLimitAnimate && (e instanceof DragEvent || e instanceof MoveEvent)) this.transformTool.onMove(e) if (this.resizing) ResizeEvent.resizingKeys = null this.dragging = this.gesturing = this.moving = this.resizing = this.rotating = this.skewing = false this.editor.opacity = 1 this.editor.update() // 移动端手势操作hideOnMove移动需强制更新一次 } // 手势控制元素 public onMove(e: MoveEvent): void { if (this.canGesture && e.moveType !== 'drag') { e.stop() if (isString(this.mergedConfig.moveable)) { this.gesturing = this.moving = true switch (e.type) { case MoveEvent.START: this.onTransformStart(e); break case MoveEvent.END: this.onTransformEnd(e); break default: this.transformTool.onMove(e) } } } } public onScale(e: ZoomEvent): void { if (this.canGesture) { e.stop() if (isString(this.mergedConfig.resizeable)) { this.gesturing = this.resizing = true switch (e.type) { case ZoomEvent.START: this.onTransformStart(e); break case ZoomEvent.END: this.onTransformEnd(e); break default: this.transformTool.onScale(e) } } } } public onRotate(e: RotateEvent): void { if (this.canGesture) { e.stop() if (isString(this.mergedConfig.rotateable)) { this.gesturing = this.rotating = true switch (e.type) { case ZoomEvent.START: this.onTransformStart(e); break case ZoomEvent.END: this.onTransformEnd(e); break default: this.transformTool.onRotate(e) } } } } // 键盘 public isHoldRotateKey(e: IUIEvent): boolean { // 按住ctrl在控制点上变旋转功能 const { rotateKey } = this.mergedConfig if (rotateKey) return e.isHoldKeys(rotateKey) return e.metaKey || e.ctrlKey } protected onKey(e: KeyEvent): void { updatePointCursor(this, e) } public onArrow(e: IKeyEvent): void { if (this.canUse) { let x = 0, y = 0 switch (e.code) { case 'ArrowDown': y = 1 break case 'ArrowUp': y = -1 break case 'ArrowLeft': x = -1 break case 'ArrowRight': x = 1 } if (x || y) { const { keyEvent, arrowStep, arrowFastStep } = this.mergeConfig if (keyEvent) { const step = e.shiftKey ? arrowFastStep : arrowStep this.transformTool.move(x * step, y * step) } } } } protected onDoubleTap(e: PointerEvent): void { const { openInner, preventEditInner } = this.mergeConfig if (openInner === 'double' && !preventEditInner) this.openInner(e) } protected onLongPress(e: PointerEvent): void { const { openInner, preventEditInner } = this.mergeConfig if (openInner === 'long' && preventEditInner) this.openInner(e) } protected openInner(e: PointerEvent): void { const { editor, target } = this if (this.single) { if (target.locked) return if (target.isBranch && !target.editInner) { if ((target as IBox).textBox) { const { children } = target const find = children.find(item => item.editable && item instanceof Text) || children.find(item => item instanceof Text) if (find) return editor.openInnerEditor(find) // 文本Box直接进入编辑状态,如便利贴文本 } editor.openGroup(target as IGroup) editor.target = editor.selector.findDeepOne(e) } else { editor.openInnerEditor() } } } public listenPointEvents(point: IEditPoint, type: IEditPointType, direction: Direction9): void { point.direction = direction point.pointType = type this.__eventIds.push( point.on_([ [DragEvent.START, this.onDragStart, this], [DragEvent.DRAG, this.onDrag, this], [DragEvent.END, this.onDragEnd, this], [PointerEvent.ENTER, (e: PointerEvent) => { this.enterPoint = point, updatePointCursor(this, e) }], [PointerEvent.LEAVE, () => { this.enterPoint = null }] ]) ) } protected __listenEvents(): void { const { rect, editor, __eventIds: events } = this events.push( rect.on_([ [PointerEvent.DOUBLE_TAP, this.onDoubleTap, this], [PointerEvent.LONG_PRESS, this.onLongPress, this] ]) ) this.waitLeafer(() => { events.push( editor.app.on_([ [[KeyEvent.HOLD, KeyEvent.UP], this.onKey, this], [KeyEvent.DOWN, this.onArrow, this], [[MoveEvent.START, MoveEvent.BEFORE_MOVE, MoveEvent.END], this.onMove, this, true], [[ZoomEvent.START, ZoomEvent.BEFORE_ZOOM, ZoomEvent.END], this.onScale, this, true], [[RotateEvent.START, RotateEvent.BEFORE_ROTATE, RotateEvent.END], this.onRotate, this, true] ]) ) }) } protected __removeListenEvents(): void { this.off_(this.__eventIds) } public destroy(): void { this.editor = null this.__removeListenEvents() super.destroy() } }