@leafer-in/editor
Version:
573 lines (435 loc) • 21 kB
text/typescript
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
readonly mergeConfig: IEditorConfig
readonly mergedConfig: IEditorConfig
public hoverTarget?: IUI
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()
}
}
}