@leafer-in/editor
Version:
478 lines (379 loc) • 19.8 kB
text/typescript
import { IRect, IEventListenerId, IBoundsData, IPointData, IKeyEvent, IGroup, IBox, IBoxInputData, IAlign, IUI, IEditorConfig, IEditorDragStartData, IEventParams, ITransformTool } from '@leafer-ui/interface'
import { Group, Box, Text, AroundHelper, Direction9, ResizeEvent, BoundsHelper } 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: IBox = new Box({ 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.visible && this.view.visible) as boolean } // 编辑框是否处于激活状态
public get canGesture(): boolean { // 是否支持手势
if (!this.canUse) return false
const { moveable, resizeable, rotateable } = this.mergeConfig
return typeof moveable === 'string' || typeof resizeable === 'string' || typeof rotateable === 'string'
}
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)
view.addMany(...rotatePoints, rect, circle, buttons, ...resizeLines, ...resizePoints)
this.add(view)
}
public load(): void {
const { target, mergeConfig, single, rect, circle, resizePoints } = this
const { stroke, strokeWidth } = mergeConfig
const pointsStyle = this.getPointsStyle()
const middlePointsStyle = this.getMiddlePointsStyle()
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
}
// rotate
circle.set(this.getPointStyle(mergeConfig.circle || mergeConfig.rotatePoint || pointsStyle[0]))
// rect
rect.set({ stroke, strokeWidth, editConfig, ...(mergeConfig.rect || {}) })
const syncEventer = single && this.transformTool.editTool
// 编辑框作为底部虚拟元素, 在 unload() 中重置
rect.hittable = !syncEventer
rect.syncEventer = syncEventer && this.editor // 单选下 rect 的事件不会冒泡,需要手动传递给editor
if (syncEventer) {
target.syncEventer = rect // 在 target 属性装饰中重置
this.app.interaction.bottomList = [{ target: rect, proxy: target }]
}
updateMoveCursor(this)
}
public update(): void {
const { editor } = this
const { x, y, scaleX, scaleY, rotation, skewX, skewY, width, height } = this.target.getLayoutBounds('box', editor, true)
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 { editMask } = this.editor
const { mergeConfig, single, rect, circle, buttons, resizePoints, rotatePoints, resizeLines } = this
const { middlePoint, resizeable, rotateable, hideOnSmall, editBox, mask, spread } = mergeConfig
this.visible = !this.target.locked
editMask.visible = mask ? true : 0
if (spread) BoundsHelper.spread(bounds, spread)
if (this.view.worldOpacity) {
const { width, height } = bounds
const smallSize = typeof hideOnSmall === 'number' ? 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]
resizeL = resizeLines[Math.floor(i / 2)]
resizeP.set(point)
rotateP.set(point)
resizeL.set(point)
// visible
resizeP.visible = resizeL.visible = showPoints && !!(resizeable || rotateable)
rotateP.visible = showPoints && rotateable && resizeable && !mergeConfig.rotatePoint
if (i % 2) { // top, right, bottom, left
resizeP.visible = rotateP.visible = showPoints && !!middlePoint
if (((i + 1) / 2) % 2) { // top, bottom
resizeL.width = width
if (hideOnSmall && resizeP.width * 2 > width) resizeP.visible = false
} else {
resizeL.height = 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', 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 point instanceof Array ? point : [point]
}
public getMiddlePointsStyle(): IBoxInputData[] {
const { middlePoint } = this.mergedConfig
return middlePoint instanceof Array ? middlePoint : (middlePoint ? [middlePoint] : this.getPointsStyle())
}
// drag
protected onDragStart(e: DragEvent): void {
this.dragging = true
const point = this.dragPoint = e.current as IEditPoint, { pointType } = point
const { editor, dragStartData } = this, { target } = this, { moveable, resizeable, rotateable, skewable, hideOnMove } = this.mergeConfig
// 确定模式
if (point.name === 'rect') {
moveable && (this.moving = true)
editor.opacity = hideOnMove ? 0 : 1 // move
} else {
if (pointType.includes('rotate') || e.metaKey || e.ctrlKey || !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)
}
dragStartData.x = e.x
dragStartData.y = e.y
dragStartData.point = { x: target.x, y: target.y } // 用于移动
dragStartData.bounds = { ...target.getLayoutBounds('box', 'local') } // 用于resize
dragStartData.rotation = target.rotation // 用于旋转
if (pointType && pointType.includes('resize')) ResizeEvent.resizingKeys = editor.leafList.keys // 记录正在resize中的元素列表
}
protected onDragEnd(e: DragEvent): void {
this.dragPoint = null
this.resetDoing()
const { name, pointType } = e.current as IEditPoint
if (name === 'rect') this.editor.opacity = 1 // move
if (pointType && pointType.includes('resize')) ResizeEvent.resizingKeys = null
}
protected onDrag(e: DragEvent): void {
const { transformTool, moving, resizing, rotating, skewing } = this
if (moving) {
transformTool.onMove(e)
updateMoveCursor(this)
} 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)
}
}
protected resetDoing(): void {
if (this.canUse) this.dragging = this.gesturing = this.moving = this.resizing = this.rotating = this.skewing = false
}
// 手势控制元素
public onMove(e: MoveEvent): void {
if (this.canGesture && e.moveType !== 'drag') {
e.stop()
if (typeof this.mergeConfig.moveable === 'string') {
this.gesturing = this.moving = true
this.transformTool.onMove(e)
}
}
}
public onScale(e: ZoomEvent): void {
if (this.canGesture) {
e.stop()
if (typeof this.mergeConfig.resizeable === 'string') {
this.gesturing = this.resizing = true
this.transformTool.onScale(e)
}
}
}
public onRotate(e: RotateEvent): void {
if (this.canGesture) {
e.stop()
if (typeof this.mergeConfig.rotateable === 'string') {
this.gesturing = this.rotating = true
this.transformTool.onRotate(e)
}
}
}
// 键盘
protected onKey(e: KeyEvent): void {
updatePointCursor(this, e)
}
public onArrow(e: IKeyEvent): void {
const { editor, transformTool } = this
if (this.canUse && editor.editing && this.mergeConfig.keyEvent) {
let x = 0, y = 0
const distance = e.shiftKey ? 10 : 1
switch (e.code) {
case 'ArrowDown':
y = distance
break
case 'ArrowUp':
y = -distance
break
case 'ArrowLeft':
x = -distance
break
case 'ArrowRight':
x = distance
}
if (x || y) transformTool.move(x, y)
}
}
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
const events: IEventParams[] = [
[DragEvent.START, this.onDragStart, this],
[DragEvent.DRAG, this.onDrag, this],
[DragEvent.END, this.onDragEnd, this],
[PointerEvent.LEAVE, () => { this.enterPoint = null }],
]
if (point.name !== 'circle') events.push([PointerEvent.ENTER, (e: PointerEvent) => { this.enterPoint = point, updatePointCursor(this, e) }])
this.__eventIds.push(point.on_(events))
}
protected __listenEvents(): void {
const { rect, editor, __eventIds: events } = this
events.push(
rect.on_([
[DragEvent.START, this.onDragStart, this],
[DragEvent.DRAG, this.onDrag, this],
[DragEvent.END, this.onDragEnd, this],
[PointerEvent.ENTER, () => updateMoveCursor(this)],
[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.BEFORE_MOVE, this.onMove, this, true],
[ZoomEvent.BEFORE_ZOOM, this.onScale, this, true],
[RotateEvent.BEFORE_ROTATE, this.onRotate, this, true],
[MoveEvent.END, this.resetDoing, this],
[ZoomEvent.END, this.resetDoing, this],
[RotateEvent.END, this.resetDoing, this],
])
)
})
}
protected __removeListenEvents(): void {
this.off_(this.__eventIds)
}
public destroy(): void {
this.editor = null
this.__removeListenEvents()
super.destroy()
}
}