UNPKG

@leafer-in/editor

Version:
573 lines (435 loc) 21 kB
import { IGroupInputData, IUI, IEventListenerId, IPointData, ILeafList, IEditSize, IGroup, IObject, IAlign, IAxis, IFunction, IMatrix, IApp } from '@leafer-ui/interface' import { Group, DataHelper, MathHelper, LeafList, Matrix, RenderEvent, LeafHelper, Direction9 } from '@leafer-ui/draw' import { DragEvent, RotateEvent, KeyEvent, ZoomEvent, MoveEvent, Plugin } from '@leafer-ui/core' import { IEditBox, IEditPoint, IEditor, IEditorConfig, IEditTool, IEditorScaleEvent, IInnerEditor, ISimulateElement, IEditorMoveEvent, IEditorRotateEvent, IEditorSkewEvent } from '@leafer-in/interface' import { EditorMoveEvent } from './event/EditorMoveEvent' import { EditorScaleEvent } from './event/EditorScaleEvent' import { EditorRotateEvent } from './event/EditorRotateEvent' import { EditorSkewEvent } from './event/EditorSkewEvent' import { EditSelect } from './display/EditSelect' import { EditBox } from './display/EditBox' import { EditMask } from './display/EditMask' import { config } from './config' import { onTarget, onHover } from './editor/target' import { targetAttr, mergeConfigAttr } from './decorator/data' import { EditorHelper } from './helper/EditorHelper' import { EditDataHelper } from './helper/EditDataHelper' import { simulate } from './editor/simulate' import { updateCursor } from './editor/cursor' import { EditToolCreator } from './tool/EditToolCreator' import { InnerEditorEvent } from './event/InnerEditorEvent' import { EditorGroupEvent } from './event/EditorGroupEvent' import { SimulateElement } from './display/SimulateElement' export class Editor extends Group implements IEditor { public config: IEditorConfig @mergeConfigAttr() readonly mergeConfig: IEditorConfig readonly mergedConfig: IEditorConfig @targetAttr(onHover) public hoverTarget?: IUI @targetAttr(onTarget) public target?: IUI | IUI[] // 列表 public leafList: ILeafList = new LeafList() // from target public get list(): IUI[] { return this.leafList.list as IUI[] } public get dragHoverExclude(): IUI[] { return [this.editBox.rect] } public openedGroupList: ILeafList = new LeafList() // 状态 public get editing(): boolean { return !!this.list.length } public innerEditing: boolean public get groupOpening(): boolean { return !!this.openedGroupList.length } public resizeDirection?: Direction9 public get multiple(): boolean { return this.list.length > 1 } public get single(): boolean { return this.list.length === 1 } public get dragging(): boolean { return this.editBox.dragging } public get moving(): boolean { return this.editBox.moving } public get dragPoint(): IEditPoint { return this.editBox.dragPoint } // 组件 public get element() { return this.multiple ? this.simulateTarget : this.list[0] as ISimulateElement } public simulateTarget: ISimulateElement = new SimulateElement(this) public editBox: IEditBox = new EditBox(this) public get buttons() { return this.editBox.buttons } public editTool?: IEditTool public innerEditor?: IInnerEditor public editToolList: IObject = {} public selector: EditSelect = new EditSelect(this) public editMask: EditMask = new EditMask(this) public targetChanged: boolean public targetEventIds: IEventListenerId[] = [] constructor(userConfig?: IEditorConfig, data?: IGroupInputData) { super(data) let mergedConfig: IEditorConfig = DataHelper.clone(config) if (userConfig) mergedConfig = DataHelper.default(userConfig, mergedConfig) this.mergedConfig = this.config = mergedConfig this.addMany(this.editMask, this.selector, this.editBox) if (!Plugin.has('resize')) this.config.editSize = 'scale' } // select public select(target: IUI | IUI[]): void { this.target = target } public cancel(): void { this.target = null } // item public hasItem(item: IUI): boolean { return this.leafList.has(item) } public addItem(item: IUI): void { if (!this.hasItem(item) && !item.locked) this.leafList.add(item), this.target = this.leafList.list as IUI[] } public removeItem(item: IUI): void { if (this.hasItem(item)) this.leafList.remove(item), this.target = this.leafList.list as IUI[] } public shiftItem(item: IUI): void { this.hasItem(item) ? this.removeItem(item) : this.addItem(item) } // update public update(): void { if (this.editing) { if (!this.element.parent) return this.cancel() if (this.innerEditing) this.innerEditor.update() this.editTool.update() this.selector.update() } } public updateEditBox(): void { if (this.multiple) simulate(this) this.update() } public updateEditTool(): void { const tool = this.editTool if (tool) { this.editBox.unload() tool.unload() this.editTool = null } if (this.editing) { const tag = this.single ? this.list[0].editOuter as string : 'EditTool' this.editTool = this.editToolList[tag] = this.editToolList[tag] || EditToolCreator.get(tag, this) this.editBox.load() this.editTool.load() } } // get public getEditSize(_ui: IUI): IEditSize { return this.mergeConfig.editSize } // operate public onMove(e: DragEvent | MoveEvent): void { if (e instanceof MoveEvent) { if (e.moveType !== 'drag') { const { moveable, resizeable } = this.mergeConfig const move = e.getLocalMove(this.element) if (moveable === 'move') e.stop(), this.move(move.x, move.y) else if (resizeable === 'zoom') e.stop() } } else { const total = { x: e.totalX, y: e.totalY } if (e.shiftKey) { if (Math.abs(total.x) > Math.abs(total.y)) total.y = 0 else total.x = 0 } this.move(DragEvent.getValidMove(this.element, this.editBox.dragStartData.point, total)) } } public onScale(e: DragEvent | ZoomEvent): void { const { element } = this let { around, lockRatio, resizeable, flipable, editSize } = this.mergeConfig if (e instanceof ZoomEvent) { if (resizeable === 'zoom') e.stop(), this.scaleOf(element.getBoxPoint(e), e.scale, e.scale) } else { const { direction } = e.current as IEditPoint if (e.shiftKey || element.lockRatio) lockRatio = true const data = EditDataHelper.getScaleData(element, this.editBox.dragStartData.bounds, direction, e.getInnerTotal(element), lockRatio, EditDataHelper.getAround(around, e.altKey), flipable, this.multiple || editSize === 'scale') if (this.editTool.onScaleWithDrag) { data.drag = e this.scaleWithDrag(data) } else { this.scaleOf(data.origin, data.scaleX, data.scaleY) } } } public onRotate(e: DragEvent | RotateEvent): void { const { skewable, rotateable, around, rotateGap } = this.mergeConfig const { direction, name } = e.current as IEditPoint if (skewable && name === 'resize-line') return this.onSkew(e as DragEvent) const { element } = this, { dragStartData } = this.editBox let origin: IPointData, rotation: number if (e instanceof RotateEvent) { if (rotateable === 'rotate') e.stop(), rotation = e.rotation, origin = element.getBoxPoint(e) else return if (element.scaleX * element.scaleY < 0) rotation = -rotation // flippedOne } else { const data = EditDataHelper.getRotateData(element.boxBounds, direction, e.getBoxPoint(element), element.getBoxPoint(dragStartData), e.shiftKey ? null : (element.around || element.origin || around || 'center')) rotation = data.rotation origin = data.origin } if (element.scaleX * element.scaleY < 0) rotation = -rotation // flippedOne if (e instanceof DragEvent) rotation = dragStartData.rotation + rotation - element.rotation rotation = MathHelper.float(MathHelper.getGapRotation(rotation, rotateGap, element.rotation), 2) if (!rotation) return this.rotateOf(origin, rotation) } public onSkew(e: DragEvent): void { const { element } = this const { around } = this.mergeConfig const { origin, skewX, skewY } = EditDataHelper.getSkewData(element.boxBounds, (e.current as IEditPoint).direction, e.getInnerMove(element), EditDataHelper.getAround(around, e.altKey)) if (!skewX && !skewY) return this.skewOf(origin, skewX, skewY) } // transform public move(x: number | IPointData, y = 0): void { if (!this.checkTransform('moveable')) return if (typeof x === 'object') y = x.y, x = x.x const { element: target } = this, { beforeMove } = this.mergeConfig if (beforeMove) { const check = beforeMove({ target, x, y }) if (typeof check === 'object') x = check.x, y = check.y else if (check === false) return } const world = target.getWorldPointByLocal({ x, y }, null, true) if (this.multiple) target.safeChange(() => target.move(x, y)) const data: IEditorMoveEvent = { target, editor: this, moveX: world.x, moveY: world.y } this.emitEvent(new EditorMoveEvent(EditorMoveEvent.BEFORE_MOVE, data)) const event = new EditorMoveEvent(EditorMoveEvent.MOVE, data) this.editTool.onMove(event) this.emitEvent(event) } public scaleWithDrag(data: IEditorScaleEvent): void { if (!this.checkTransform('resizeable')) return const { element: target } = this, { beforeScale } = this.mergeConfig if (beforeScale) { const { origin, scaleX, scaleY, drag } = data const check = beforeScale({ target, drag, origin, scaleX, scaleY }) if (check === false) return } data = { ...data, target, editor: this, worldOrigin: target.getWorldPoint(data.origin) } this.emitEvent(new EditorScaleEvent(EditorScaleEvent.BEFORE_SCALE, data)) const event = new EditorScaleEvent(EditorScaleEvent.SCALE, data) this.editTool.onScaleWithDrag(event) this.emitEvent(event) } override scaleOf(origin: IPointData | IAlign, scaleX: number, scaleY = scaleX, _resize?: boolean): void { if (!this.checkTransform('resizeable')) return const { element: target } = this, { beforeScale } = this.mergeConfig if (beforeScale) { const check = beforeScale({ target, origin, scaleX, scaleY }) if (typeof check === 'object') scaleX = check.scaleX, scaleY = check.scaleY else if (check === false) return } const worldOrigin = this.getWorldOrigin(origin) const transform = this.multiple && this.getChangedTransform(() => target.safeChange(() => target.scaleOf(origin, scaleX, scaleY))) const data: IEditorScaleEvent = { target, editor: this, worldOrigin, scaleX, scaleY, transform } this.emitEvent(new EditorScaleEvent(EditorScaleEvent.BEFORE_SCALE, data)) const event = new EditorScaleEvent(EditorScaleEvent.SCALE, data) this.editTool.onScale(event) this.emitEvent(event) } override flip(axis: IAxis): void { if (!this.checkTransform('resizeable')) return const { element } = this const worldOrigin = this.getWorldOrigin('center') const transform = this.multiple ? this.getChangedTransform(() => element.safeChange(() => element.flip(axis))) : new Matrix(LeafHelper.getFlipTransform(element, axis)) const data: IEditorScaleEvent = { target: element, editor: this, worldOrigin, scaleX: axis === 'x' ? -1 : 1, scaleY: axis === 'y' ? -1 : 1, transform } this.emitEvent(new EditorScaleEvent(EditorScaleEvent.BEFORE_SCALE, data)) const event = new EditorScaleEvent(EditorScaleEvent.SCALE, data) this.editTool.onScale(event) this.emitEvent(event) } override rotateOf(origin: IPointData | IAlign, rotation: number): void { if (!this.checkTransform('rotateable')) return const { element: target } = this, { beforeRotate } = this.mergeConfig if (beforeRotate) { const check = beforeRotate({ target, origin, rotation }) if (typeof check === 'number') rotation = check else if (check === false) return } const worldOrigin = this.getWorldOrigin(origin) const transform = this.multiple && this.getChangedTransform(() => target.safeChange(() => target.rotateOf(origin, rotation))) const data: IEditorRotateEvent = { target, editor: this, worldOrigin, rotation, transform } this.emitEvent(new EditorRotateEvent(EditorRotateEvent.BEFORE_ROTATE, data)) const event = new EditorRotateEvent(EditorRotateEvent.ROTATE, data) this.editTool.onRotate(event) this.emitEvent(event) } override skewOf(origin: IPointData | IAlign, skewX: number, skewY = 0, _resize?: boolean): void { if (!this.checkTransform('skewable')) return const { element: target } = this, { beforeSkew } = this.mergeConfig if (beforeSkew) { const check = beforeSkew({ target, origin, skewX, skewY }) if (typeof check === 'object') skewX = check.skewX, skewY = check.skewY else if (check === false) return } const worldOrigin = this.getWorldOrigin(origin) const transform = this.multiple && this.getChangedTransform(() => target.safeChange(() => target.skewOf(origin, skewX, skewY))) const data: IEditorSkewEvent = { target, editor: this, worldOrigin, skewX, skewY, transform } this.emitEvent(new EditorSkewEvent(EditorSkewEvent.BEFORE_SKEW, data)) const event = new EditorSkewEvent(EditorSkewEvent.SKEW, data) this.editTool.onSkew(event) this.emitEvent(event) } public checkTransform(type: 'moveable' | 'resizeable' | 'rotateable' | 'skewable'): boolean { return this.element && !this.element.locked && this.mergeConfig[type] as boolean } protected getWorldOrigin(origin: IPointData | IAlign): IPointData { return this.element.getWorldPoint(LeafHelper.getInnerOrigin(this.element, origin)) } protected getChangedTransform(func: IFunction): IMatrix { const { element } = this if (this.multiple && !element.canChange) return element.changedTransform const oldMatrix = new Matrix(element.worldTransform) func() return new Matrix(element.worldTransform).divide(oldMatrix) // world change transform } // group public group(userGroup?: IGroup | IGroupInputData): IGroup { if (this.multiple) { this.emitGroupEvent(EditorGroupEvent.BEFORE_GROUP) this.target = EditorHelper.group(this.list, this.element, userGroup) this.emitGroupEvent(EditorGroupEvent.GROUP, this.target as IGroup) } return this.target as IGroup } public ungroup(): IUI[] { const { list } = this if (list.length) { list.forEach(item => item.isBranch && this.emitGroupEvent(EditorGroupEvent.BEFORE_UNGROUP, item as IGroup)) this.target = EditorHelper.ungroup(list) list.forEach(item => item.isBranch && this.emitGroupEvent(EditorGroupEvent.UNGROUP, item as IGroup)) } return this.list } public openGroup(group: IGroup): void { this.emitGroupEvent(EditorGroupEvent.BEFORE_OPEN, group) this.openedGroupList.add(group) group.hitChildren = true this.emitGroupEvent(EditorGroupEvent.OPEN, group) } public closeGroup(group: IGroup): void { this.emitGroupEvent(EditorGroupEvent.BEFORE_CLOSE, group) this.openedGroupList.remove(group) group.hitChildren = false this.emitGroupEvent(EditorGroupEvent.CLOSE, group) } public checkOpenedGroups(): void { const opened = this.openedGroupList if (opened.length) { let { list } = opened if (this.editing) list = [], opened.forEach(item => this.list.every(leaf => !LeafHelper.hasParent(leaf, item)) && list.push(item)) list.forEach(item => this.closeGroup(item as IGroup)) } if (this.editing && !this.selector.dragging) this.checkDeepSelect() } public checkDeepSelect(): void { let parent: IGroup, { list } = this for (let i = 0; i < list.length; i++) { parent = list[i].parent while (parent && !parent.hitChildren) { this.openGroup(parent) parent = parent.parent } } } public emitGroupEvent(type: string, group?: IGroup): void { const event = new EditorGroupEvent(type, { editTarget: group }) this.emitEvent(event) if (group) group.emitEvent(event) } // inner public openInnerEditor(target?: IUI, select?: boolean): void { if (target && select) this.target = target if (this.single) { const editTarget = target || this.element const tag = editTarget.editInner if (tag && EditToolCreator.list[tag]) { this.editTool.unload() this.innerEditing = true this.innerEditor = this.editToolList[tag] || EditToolCreator.get(tag, this) this.innerEditor.editTarget = editTarget this.emitInnerEvent(InnerEditorEvent.BEFORE_OPEN) this.innerEditor.load() this.emitInnerEvent(InnerEditorEvent.OPEN) } } } public closeInnerEditor(): void { if (this.innerEditing) { this.innerEditing = false this.emitInnerEvent(InnerEditorEvent.BEFORE_CLOSE) this.innerEditor.unload() this.emitInnerEvent(InnerEditorEvent.CLOSE) this.editTool.load() this.innerEditor = null } } public emitInnerEvent(type: string): void { const { innerEditor } = this, { editTarget } = innerEditor const event = new InnerEditorEvent(type, { editTarget, innerEditor }) this.emitEvent(event) editTarget.emitEvent(event) } // lock public lock(): void { this.list.forEach(leaf => leaf.locked = true) this.update() } public unlock(): void { this.list.forEach(leaf => leaf.locked = false) this.update() } // level public toTop(): void { if (this.list.length) { EditorHelper.toTop(this.list) this.leafList.update() } } public toBottom(): void { if (this.list.length) { EditorHelper.toBottom(this.list) this.leafList.update() } } protected onAppRenderStart(app: IApp): void { if (this.targetChanged = app.children.some(leafer => leafer !== this.leafer && leafer.renderer.changed)) this.editBox.forceRender() } protected onRenderStart(): void { if (this.targetChanged) this.update() } protected onKey(e: KeyEvent): void { updateCursor(this, e) } // event public listenTargetEvents(): void { if (!this.targetEventIds.length) { const { app, leafer, editBox, editMask } = this this.targetEventIds = [ leafer.on_(RenderEvent.START, this.onRenderStart, this), app.on_(RenderEvent.CHILD_START, this.onAppRenderStart, this), app.on_(MoveEvent.BEFORE_MOVE, this.onMove, this, true), app.on_(ZoomEvent.BEFORE_ZOOM, this.onScale, this, true), app.on_(RotateEvent.BEFORE_ROTATE, this.onRotate, this, true), app.on_([KeyEvent.HOLD, KeyEvent.UP], this.onKey, this), app.on_(KeyEvent.DOWN, editBox.onArrow, editBox) ] if (editMask.visible) editMask.forceRender() } } public removeTargetEvents(): void { const { targetEventIds, editMask } = this if (targetEventIds.length) { this.off_(targetEventIds) targetEventIds.length = 0 if (editMask.visible) editMask.forceRender() } } public destroy(): void { if (!this.destroyed) { this.target = this.hoverTarget = null Object.values(this.editToolList).forEach(item => item.destroy()) this.simulateTarget.destroy() this.editToolList = {} this.simulateTarget = this.editTool = this.innerEditor = null super.destroy() } } }