UNPKG

@leafer-in/editor

Version:
1,273 lines (1,199 loc) 96.8 kB
import { Event, isArray, defineKey, isNull, isUndefined, isObject, MatrixHelper, BoundsHelper, LeafBoundsHelper, getMatrixData, getBoundsData, surfaceType, UI, ColorConvert, isString, Paint, Group, Rect, Bounds, LeafList, Direction9, AroundHelper, MathHelper, PointHelper, isNumber, Box, DataHelper, ResizeEvent, getPointData, Text, Matrix, Debug, LeafHelper, PropertyEvent, Plugin, RenderEvent, LeaferEvent, Creator, dataType } from "@leafer-ui/draw"; import { PointerEvent, DragEvent, MoveEvent, ZoomEvent, DragBoundsHelper, KeyEvent, RotateEvent, useModule } from "@leafer-ui/core"; import "@leafer-in/resize"; function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) { var e = new Error(message); return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e; }; function toList(value) { return value ? isArray(value) ? value : [ value ] : []; } class EditorEvent extends Event { get list() { return toList(this.value); } get oldList() { return toList(this.oldValue); } constructor(type, data) { super(type); if (data) Object.assign(this, data); } } EditorEvent.BEFORE_SELECT = "editor.before_select"; EditorEvent.SELECT = "editor.select"; EditorEvent.AFTER_SELECT = "editor.after_select"; EditorEvent.BEFORE_HOVER = "editor.before_hover"; EditorEvent.HOVER = "editor.hover"; function targetAttr(fn) { return (target, key) => { const privateKey = "_" + key; defineKey(target, key, { get() { return this[privateKey]; }, set(value) { const old = this[privateKey]; if (old !== value) { const t = this; if (t.config) { const isSelect = key === "target"; if (isSelect) { const {beforeSelect: beforeSelect} = t.config; if (beforeSelect) { const check = beforeSelect({ target: value }); if (isObject(check)) value = check; else if (check === false) return; } if (t.hasDimOthers) t.cancelDimOthers(); if (isArray(value) && value.length > 1 && value[0].locked) value.splice(0, 1); if (t.single) { delete t.element.syncEventer; delete t.element.__world.ignorePixelSnap; } } const type = isSelect ? EditorEvent.BEFORE_SELECT : EditorEvent.BEFORE_HOVER; if (this.hasEvent(type)) this.emitEvent(new EditorEvent(type, { editor: t, value: value, oldValue: old })); } this[privateKey] = value, fn(this, old); } } }); }; } function mergeConfigAttr() { return (target, key) => { defineKey(target, key, { get() { const {config: config, element: element, dragPoint: dragPoint, editBox: editBox, editTool: editTool, innerEditor: innerEditor, app: app} = this, mergeConfig = Object.assign({}, config); if (innerEditor) innerEditor.editConfig && Object.assign(mergeConfig, innerEditor.editConfig); else if (editTool) editTool.editConfig && Object.assign(mergeConfig, editTool.editConfig); if (element && element.editConfig) { let {editConfig: editConfig} = element; if (editConfig.hover || editConfig.hoverStyle) { editConfig = Object.assign({}, editConfig); delete editConfig.hover; delete editConfig.hoverStyle; } Object.assign(mergeConfig, editConfig); } if (editBox.config) Object.assign(mergeConfig, editBox.config); if (dragPoint) { if (dragPoint.editConfig) Object.assign(mergeConfig, dragPoint.editConfig); if (mergeConfig.editSize === "font-size") mergeConfig.lockRatio = true; if (dragPoint.pointType === "resize-rotate") { mergeConfig.around || (mergeConfig.around = "center"); isNull(mergeConfig.lockRatio) && (mergeConfig.lockRatio = true); } } if (isUndefined(mergeConfig.dragLimitAnimate)) mergeConfig.dragLimitAnimate = app && app.config.pointer.dragLimitAnimate; return this.mergedConfig = mergeConfig; } }); }; } const {abs: abs$1} = Math; const {copy: copy$1} = MatrixHelper; const {setListWithFn: setListWithFn} = BoundsHelper; const {worldBounds: worldBounds} = LeafBoundsHelper; const matrix = getMatrixData(); const bounds$1 = getBoundsData(); class Stroker extends UI { constructor() { super(); this.list = []; this.visible = 0; this.hittable = false; this.strokeAlign = "center"; } setTarget(target, style) { if (style) this.set(style); this.target = target; this.update(); } update(style) { const {list: list} = this; if (list.length) { setListWithFn(bounds$1, list, worldBounds); if (style) this.set(style); this.set(bounds$1); this.visible = true; } else this.visible = 0; } __draw(canvas, options) { const {list: list} = this; if (list.length) { let leaf; const data = this.__, {stroke: stroke, strokeWidth: strokeWidth, fill: fill} = data, {bounds: bounds} = options; for (let i = 0; i < list.length; i++) { leaf = list[i]; const {worldTransform: worldTransform, worldRenderBounds: worldRenderBounds} = leaf; if (worldRenderBounds.width && worldRenderBounds.height && (!bounds || bounds.hit(worldRenderBounds, options.matrix))) { const aScaleX = abs$1(worldTransform.scaleX), aScaleY = abs$1(worldTransform.scaleY); copy$1(matrix, worldTransform); matrix.half = strokeWidth % 2; canvas.setWorld(matrix, options.matrix); canvas.beginPath(); if (this.strokePathType === "path") { leaf.__drawPath(canvas); } else { if (leaf.__.__useArrow) leaf.__drawPath(canvas); else leaf.__.__pathForRender ? leaf.__drawRenderPath(canvas) : leaf.__drawPathByBox(canvas); } data.strokeWidth = strokeWidth / Math.max(aScaleX, aScaleY); if (data.shadow) { const shadow = data.shadow[0], {scaleX: scaleX, scaleY: scaleY} = this.getRenderScaleData(true, shadow.scaleFixed); canvas.save(), canvas.setWorldShadow(shadow.x * scaleX, shadow.y * scaleY, shadow.blur * scaleX, ColorConvert.string(shadow.color)); } if (stroke) isString(stroke) ? Paint.stroke(stroke, this, canvas, options) : Paint.strokes(stroke, this, canvas, options); if (fill) isString(fill) ? Paint.fill(fill, this, canvas, options) : Paint.fills(fill, this, canvas, options); if (data.shadow) canvas.restore(); } } data.strokeWidth = strokeWidth; } } destroy() { this.target = null; super.destroy(); } } __decorate([ targetAttr(onTarget$1) ], Stroker.prototype, "target", void 0); __decorate([ surfaceType("render-path") ], Stroker.prototype, "strokePathType", void 0); function onTarget$1(stroker) { const value = stroker.target; stroker.list = value ? isArray(value) ? value : [ value ] : []; } class SelectArea extends Group { constructor(data) { super(data); this.strokeArea = new Rect({ strokeAlign: "center" }); this.fillArea = new Rect; this.visible = 0; this.hittable = false; this.addMany(this.fillArea, this.strokeArea); } setStyle(style, userStyle) { const {visible: visible, stroke: stroke, strokeWidth: strokeWidth} = style; this.visible = visible; this.strokeArea.reset(Object.assign({ stroke: stroke, strokeWidth: strokeWidth }, userStyle || {})); this.fillArea.reset({ visible: userStyle ? false : true, fill: stroke, opacity: .2 }); } setBounds(bounds) { this.strokeArea.set(bounds); this.fillArea.set(bounds); } } const EditSelectHelper = { findOne(path) { return path.list.find(leaf => leaf.editable); }, findByBounds(branch, bounds) { const list = []; eachFind([ branch ], list, bounds); return list; } }; function eachFind(children, list, bounds) { let child, data; for (let i = 0, len = children.length; i < len; i++) { child = children[i], data = child.__; if (data.hittable && data.visible && !data.locked && bounds.hit(child.__world)) { if (data.editable) { if (child.isBranch && !data.hitChildren) { if (data.hitSelf) list.push(child); continue; } else if (child.isFrame) { if (bounds.includes(child.__layout.boxBounds, child.__world)) { list.push(child); continue; } } else if (bounds.hit(child.__layout.boxBounds, child.__world) && data.hitSelf) list.push(child); } if (child.isBranch) eachFind(child.children, list, bounds); } } } const {findOne: findOne, findByBounds: findByBounds} = EditSelectHelper; class EditSelect extends Group { get dragging() { return !!this.originList; } get running() { const {editor: editor, app: app} = this; return this.hittable && editor.visible && editor.hittable && editor.mergeConfig.selector && (app && app.mode === "normal"); } get isMoveMode() { const {app: app} = this; return app && app.interaction.moveMode; } constructor(editor) { super(); this.hoverStroker = new Stroker; this.targetStroker = new Stroker; this.bounds = new Bounds; this.selectArea = new SelectArea; this.__eventIds = []; this.editor = editor; this.addMany(this.targetStroker, this.hoverStroker, this.selectArea); this.__listenEvents(); } onHover() { const {editor: editor} = this; if (this.running && !this.dragging && !editor.dragging) { const {hoverTarget: hoverTarget, mergeConfig: mergeConfig} = editor, config = Object.assign({}, mergeConfig); if (hoverTarget && hoverTarget.editConfig) Object.assign(config, hoverTarget.editConfig); const {stroke: stroke, strokeWidth: strokeWidth, hover: hover, hoverStyle: hoverStyle} = config; this.hoverStroker.setTarget(hover ? hoverTarget : null, Object.assign({ stroke: stroke, strokeWidth: strokeWidth }, hoverStyle || {})); } else { this.hoverStroker.target = null; } } onSelect() { if (this.running) { this.targetStroker.setTarget(this.editor.list); this.hoverStroker.target = null; } } update() { this.hoverStroker.update(); const {stroke: stroke, strokeWidth: strokeWidth, selectedPathType: selectedPathType, selectedStyle: selectedStyle} = this.editor.mergedConfig; this.targetStroker.update(Object.assign({ stroke: stroke, strokeWidth: strokeWidth && Math.max(1, strokeWidth / 2), strokePathType: selectedPathType }, selectedStyle || {})); } onPointerMove(e) { const {app: app, editor: editor} = this; if (this.running && !this.isMoveMode && app.interaction.canHover && !app.interaction.dragging) { const find = this.findUI(e); editor.hoverTarget = editor.hasItem(find) ? null : find; } if (this.isMoveMode) { editor.hoverTarget = null; } } onBeforeDown(e) { if (e.multiTouch) return; const {select: select} = this.editor.mergeConfig; if (select === "press") { if (this.app.config.mobile) { this.waitSelect = () => this.checkAndSelect(e); } else { this.checkAndSelect(e); } } } onTap(e) { if (e.multiTouch) return; const {editor: editor} = this; const {select: select, selectKeep: selectKeep} = editor.mergeConfig; if (select === "tap") this.checkAndSelect(e); else if (this.waitSelect) this.waitSelect(); if (this.needRemoveItem) { editor.removeItem(this.needRemoveItem); } else if (this.isMoveMode) { if (!selectKeep) editor.target = null; } } checkAndSelect(e) { this.needRemoveItem = null; if (this.allowSelect(e)) { const {editor: editor} = this; const find = this.findUI(e); if (find) { if (this.isMultipleSelect(e)) { if (editor.hasItem(find)) this.needRemoveItem = find; else editor.addItem(find); } else { editor.target = find; } e.path.needUpdate = true; } else if (this.allow(e.target)) { if (!this.isHoldMultipleSelectKey(e) && !this.editor.mergedConfig.selectKeep) editor.target = null; } } } onDragStart(e) { if (e.multiTouch) return; if (this.waitSelect) this.waitSelect(); if (this.allowDrag(e)) { const {editor: editor} = this; const {stroke: stroke, area: area} = editor.mergeConfig; const {x: x, y: y} = e.getInnerPoint(this); this.bounds.set(x, y); this.selectArea.setStyle({ visible: true, stroke: stroke, x: x, y: y }, area); this.selectArea.setBounds(this.bounds.get()); this.originList = editor.leafList.clone(); } } onDrag(e) { if (e.multiTouch) return; if (this.editor.dragging) return this.onDragEnd(e); if (this.dragging) { const {editor: editor} = this; const total = e.getInnerTotal(this); const dragBounds = this.bounds.clone().unsign(); const list = new LeafList(findByBounds(editor.app, dragBounds)); this.bounds.width = total.x; this.bounds.height = total.y; this.selectArea.setBounds(dragBounds.get()); if (list.length) { const selectList = []; this.originList.forEach(item => { if (!list.has(item)) selectList.push(item); }); list.forEach(item => { if (!this.originList.has(item)) selectList.push(item); }); if (selectList.length !== editor.list.length || editor.list.some((child, index) => child !== selectList[index])) { editor.target = selectList; } } else { editor.target = this.originList.list; } } } onDragEnd(e) { if (e.multiTouch) return; if (this.dragging) this.originList = null, this.selectArea.visible = 0; } onAutoMove(e) { if (this.dragging) { const {x: x, y: y} = e.getLocalMove(this); this.bounds.x += x; this.bounds.y += y; } } allow(target) { return target.leafer !== this.editor.leafer; } allowDrag(e) { const {boxSelect: boxSelect, multipleSelect: multipleSelect} = this.editor.mergeConfig; if (this.running && (multipleSelect && boxSelect) && !e.target.draggable) { return !this.editor.editing && this.allow(e.target) || this.isHoldMultipleSelectKey(e) && !findOne(e.path); } else { return false; } } allowSelect(e) { return this.running && !this.isMoveMode && !e.middle; } findDeepOne(e) { const options = { exclude: new LeafList(this.editor.editBox.rect) }; return findOne(e.target.leafer.interaction.findPath(e, options)); } findUI(e) { return this.isMultipleSelect(e) ? this.findDeepOne(e) : findOne(e.path); } isMultipleSelect(e) { const {multipleSelect: multipleSelect, continuousSelect: continuousSelect} = this.editor.mergeConfig; return multipleSelect && (this.isHoldMultipleSelectKey(e) || continuousSelect); } isHoldMultipleSelectKey(e) { const {multipleSelectKey: multipleSelectKey} = this.editor.mergedConfig; if (multipleSelectKey) return e.isHoldKeys(multipleSelectKey); return e.shiftKey; } __listenEvents() { const {editor: editor} = this; editor.waitLeafer(() => { const {app: app} = editor; app.selector.proxy = editor; this.__eventIds = [ editor.on_([ [ EditorEvent.HOVER, this.onHover, this ], [ EditorEvent.SELECT, this.onSelect, this ] ]), app.on_([ [ PointerEvent.MOVE, this.onPointerMove, this ], [ PointerEvent.BEFORE_DOWN, this.onBeforeDown, this ], [ PointerEvent.TAP, this.onTap, this ], [ DragEvent.START, this.onDragStart, this, true ], [ DragEvent.DRAG, this.onDrag, this ], [ DragEvent.END, this.onDragEnd, this ], [ MoveEvent.MOVE, this.onAutoMove, this ], [ [ ZoomEvent.ZOOM, MoveEvent.MOVE ], () => { this.editor.hoverTarget = null; } ] ]) ]; }); } __removeListenEvents() { this.off_(this.__eventIds); } destroy() { this.editor = this.originList = this.needRemoveItem = null; this.__removeListenEvents(); super.destroy(); } } const {topLeft: topLeft, top: top, topRight: topRight, right: right$1, bottomRight: bottomRight, bottom: bottom, bottomLeft: bottomLeft, left: left$1} = Direction9; const {toPoint: toPoint} = AroundHelper, {within: within, sign: sign} = MathHelper, {abs: abs} = Math; const EditDataHelper = { getScaleData(target, startBounds, direction, totalMoveOrScale, lockRatio, around, flipable, scaleMode) { let align, origin = {}, scaleX = 1, scaleY = 1, lockScale; const {boxBounds: boxBounds, widthRange: widthRange, heightRange: heightRange, dragBounds: dragBounds, worldBoxBounds: worldBoxBounds} = target; const {width: width, height: height} = startBounds; const originChangedScaleX = target.scaleX / startBounds.scaleX; const originChangedScaleY = target.scaleY / startBounds.scaleY; const signX = sign(originChangedScaleX); const signY = sign(originChangedScaleY); const changedScaleX = scaleMode ? originChangedScaleX : signX * boxBounds.width / width; const changedScaleY = scaleMode ? originChangedScaleY : signY * boxBounds.height / height; if (isNumber(totalMoveOrScale)) { scaleX = scaleY = Math.sqrt(totalMoveOrScale); } else { if (around) { totalMoveOrScale.x *= 2; totalMoveOrScale.y *= 2; } totalMoveOrScale.x *= scaleMode ? originChangedScaleX : signX; totalMoveOrScale.y *= scaleMode ? originChangedScaleY : signY; const topScale = (-totalMoveOrScale.y + height) / height; const rightScale = (totalMoveOrScale.x + width) / width; const bottomScale = (totalMoveOrScale.y + height) / height; const leftScale = (-totalMoveOrScale.x + width) / width; switch (direction) { case top: scaleY = topScale; align = "bottom"; break; case right$1: scaleX = rightScale; align = "left"; break; case bottom: scaleY = bottomScale; align = "top"; break; case left$1: scaleX = leftScale; align = "right"; break; case topLeft: scaleY = topScale; scaleX = leftScale; align = "bottom-right"; break; case topRight: scaleY = topScale; scaleX = rightScale; align = "bottom-left"; break; case bottomRight: scaleY = bottomScale; scaleX = rightScale; align = "top-left"; break; case bottomLeft: scaleY = bottomScale; scaleX = leftScale; align = "top-right"; } if (lockRatio) { if (lockRatio === "corner" && direction % 2) { lockRatio = false; } else { switch (direction) { case top: case bottom: scaleX = scaleY; break; case left$1: case right$1: scaleY = scaleX; break; default: lockScale = Math.sqrt(abs(scaleX * scaleY)); scaleX = sign(scaleX) * lockScale; scaleY = sign(scaleY) * lockScale; } } } } const useScaleX = scaleX !== 1, useScaleY = scaleY !== 1; if (useScaleX) scaleX /= changedScaleX; if (useScaleY) scaleY /= changedScaleY; if (!flipable) { const {worldTransform: worldTransform} = target; if (scaleX < 0) scaleX = 1 / boxBounds.width / worldTransform.scaleX; if (scaleY < 0) scaleY = 1 / boxBounds.height / worldTransform.scaleY; } toPoint(around || align, boxBounds, origin, true); if (dragBounds) { const scaleData = { x: scaleX, y: scaleY }; DragBoundsHelper.limitScaleOf(target, origin, scaleData, lockRatio); scaleX = scaleData.x; scaleY = scaleData.y; } if (useScaleX && widthRange) { const nowWidth = boxBounds.width * target.scaleX; scaleX = within(nowWidth * scaleX, widthRange) / nowWidth; } if (useScaleY && heightRange) { const nowHeight = boxBounds.height * target.scaleY; scaleY = within(nowHeight * scaleY, heightRange) / nowHeight; } if (useScaleX && abs(scaleX * worldBoxBounds.width) < 1) scaleX = sign(scaleX) / worldBoxBounds.width; if (useScaleY && abs(scaleY * worldBoxBounds.height) < 1) scaleY = sign(scaleY) / worldBoxBounds.height; if (lockRatio && scaleX !== scaleY) { lockScale = Math.min(abs(scaleX), abs(scaleY)); scaleX = sign(scaleX) * lockScale; scaleY = sign(scaleY) * lockScale; } isFinite(scaleX) || (scaleX = 1); isFinite(scaleY) || (scaleY = 1); return { origin: origin, scaleX: scaleX, scaleY: scaleY, direction: direction, lockRatio: lockRatio, around: around }; }, getRotateData(target, direction, current, last, around) { let align, origin = {}; switch (direction) { case topLeft: align = "bottom-right"; break; case topRight: align = "bottom-left"; break; case bottomRight: align = "top-left"; break; case bottomLeft: align = "top-right"; break; default: align = "center"; } toPoint(around || align, target.boxBounds, origin, true); return { origin: origin, rotation: PointHelper.getRotation(last, target.getWorldPointByBox(origin), current) }; }, getSkewData(bounds, direction, move, around) { let align, origin = {}, skewX = 0, skewY = 0; let last; switch (direction) { case top: case topLeft: last = { x: .5, y: 0 }; align = "bottom"; skewX = 1; break; case bottom: case bottomRight: last = { x: .5, y: 1 }; align = "top"; skewX = 1; break; case left$1: case bottomLeft: last = { x: 0, y: .5 }; align = "right"; skewY = 1; break; case right$1: case topRight: last = { x: 1, y: .5 }; align = "left"; skewY = 1; } const {width: width, height: height} = bounds; last.x = last.x * width; last.y = last.y * height; toPoint(around || align, bounds, origin, true); const rotation = PointHelper.getRotation(last, origin, { x: last.x + (skewX ? move.x : 0), y: last.y + (skewY ? move.y : 0) }); skewX ? skewX = -rotation : skewY = rotation; return { origin: origin, skewX: skewX, skewY: skewY }; }, getAround(around, altKey) { return altKey && !around ? "center" : around; }, getRotateDirection(direction, rotation, totalDirection = 8) { direction = (direction + Math.round(rotation / (360 / totalDirection))) % totalDirection; if (direction < 0) direction += totalDirection; return direction; }, getFlipDirection(direction, flipedX, flipedY) { if (flipedX) { switch (direction) { case left$1: direction = right$1; break; case topLeft: direction = topRight; break; case bottomLeft: direction = bottomRight; break; case right$1: direction = left$1; break; case topRight: direction = topLeft; break; case bottomRight: direction = bottomLeft; break; } } if (flipedY) { switch (direction) { case top: direction = bottom; break; case topLeft: direction = bottomLeft; break; case topRight: direction = bottomRight; break; case bottom: direction = top; break; case bottomLeft: direction = topLeft; break; case bottomRight: direction = topRight; break; } } return direction; } }; const cacheCursors = {}; function updatePointCursor(editBox, e) { const {enterPoint: point, dragging: dragging, skewing: skewing, resizing: resizing, flippedX: flippedX, flippedY: flippedY} = editBox; if (!point || !editBox.editor.editing || !editBox.canUse) return; if (point.name === "rect") return updateMoveCursor(editBox); if (point.name === "circle") return; let {rotation: rotation} = editBox; const {pointType: pointType} = point, {moveCursor: moveCursor, resizeCursor: resizeCursor, rotateCursor: rotateCursor, skewCursor: skewCursor, moveable: moveable, resizeable: resizeable, rotateable: rotateable, skewable: skewable} = editBox.mergeConfig; if (pointType === "move") { point.cursor = moveCursor; if (!moveable) point.visible = false; return; } else if (pointType === "button") { if (!point.cursor) point.cursor = "pointer"; return; } let showResize = pointType.includes("resize"); if (showResize && rotateable && (editBox.isHoldRotateKey(e) || !resizeable)) showResize = false; const showSkew = skewable && !showResize && (point.name === "resize-line" || pointType === "skew"); const cursor = dragging ? skewing ? skewCursor : resizing ? resizeCursor : rotateCursor : showSkew ? skewCursor : showResize ? resizeCursor : rotateCursor; rotation += (EditDataHelper.getFlipDirection(point.direction, flippedX, flippedY) + 1) * 45; rotation = Math.round(MathHelper.formatRotation(rotation, true) / 2) * 2; const {url: url, x: x, y: y} = cursor; const key = url + rotation; if (cacheCursors[key]) { point.cursor = cacheCursors[key]; } else { cacheCursors[key] = point.cursor = { url: toDataURL(url, rotation), x: x, y: y }; } } function updateMoveCursor(editBox) { const {moveCursor: moveCursor, moveable: moveable} = editBox.mergeConfig; if (editBox.canUse) editBox.rect.cursor = moveable ? moveCursor : undefined; } function toDataURL(svg, rotation) { return '"data:image/svg+xml,' + encodeURIComponent(svg.replace("{{rotation}}", rotation.toString())) + '"'; } class EditPoint extends Box { constructor(data) { super(data); this.useFastShadow = true; } } const fourDirection = [ "top", "right", "bottom", "left" ], editConfig = undefined; class EditBox extends Group { get mergeConfig() { const {config: config} = this, {mergeConfig: mergeConfig, editBox: editBox} = this.editor; return this.mergedConfig = config && editBox !== this ? Object.assign(Object.assign({}, mergeConfig), config) : mergeConfig; } get target() { return this._target || this.editor.element; } set target(target) { this._target = target; } get single() { return !!this._target || this.editor.single; } get transformTool() { return this._transformTool || this.editor; } set transformTool(tool) { this._transformTool = tool; } get flipped() { return this.flippedX || this.flippedY; } get flippedX() { return this.scaleX < 0; } get flippedY() { return this.scaleY < 0; } get flippedOne() { return this.scaleX * this.scaleY < 0; } get canUse() { return this.app && this.editor.editing; } get canGesture() { if (!this.canUse) return false; const {moveable: moveable, resizeable: resizeable, rotateable: rotateable} = this.mergeConfig; return isString(moveable) || isString(resizeable) || isString(rotateable); } get canDragLimitAnimate() { return this.moving && this.mergeConfig.dragLimitAnimate && this.target.dragBounds; } constructor(editor) { super(); this.view = new Group; this.rect = new EditPoint({ name: "rect", hitFill: "all", hitStroke: "none", strokeAlign: "center", hitRadius: 5 }); this.circle = new EditPoint({ name: "circle", strokeAlign: "center", around: "center", cursor: "crosshair", hitRadius: 5 }); this.buttons = new Group({ around: "center", hitSelf: false, visible: 0 }); this.resizePoints = []; this.rotatePoints = []; this.resizeLines = []; this.dragStartData = {}; this.__eventIds = []; this.editor = editor; this.visible = false; this.create(); this.__listenEvents(); } create() { let rotatePoint, resizeLine, resizePoint; const {view: view, resizePoints: resizePoints, rotatePoints: rotatePoints, resizeLines: resizeLines, rect: rect, circle: circle, buttons: buttons} = this; const arounds = [ "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); view.addMany(...rotatePoints, rect, circle, buttons, ...resizeLines, ...resizePoints); this.add(view); } load() { const {target: target, mergeConfig: mergeConfig, single: single, rect: rect, circle: circle, resizePoints: resizePoints, resizeLines: resizeLines} = this; const {stroke: stroke, strokeWidth: strokeWidth, ignorePixelSnap: ignorePixelSnap} = mergeConfig; const pointsStyle = this.getPointsStyle(); const middlePointsStyle = this.getMiddlePointsStyle(); const resizeLinesStyle = this.getResizeLinesStyle(); this.visible = !target.locked; let resizeP; 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(Object.assign({ pointType: "resize", rotation: (i - 1) / 2 * 90 }, resizeLinesStyle[(i - 1) / 2 % resizeLinesStyle.length] || {})); } circle.set(this.getPointStyle(mergeConfig.circle || mergeConfig.rotatePoint || pointsStyle[0])); rect.set(Object.assign({ stroke: stroke, strokeWidth: strokeWidth, opacity: 1, editConfig: editConfig }, mergeConfig.rect || {})); const rectThrough = isNull(mergeConfig.rectThrough) ? single : mergeConfig.rectThrough; rect.hittable = !rectThrough; if (rectThrough) { target.syncEventer = rect; this.app.interaction.bottomList = [ { target: rect, proxy: target } ]; } if (single) DataHelper.stintSet(target.__world, "ignorePixelSnap", ignorePixelSnap); updateMoveCursor(this); } update() { const {editor: editor} = this; const {x: x, y: y, scaleX: scaleX, scaleY: scaleY, rotation: rotation, skewX: skewX, skewY: skewY, width: width, height: height} = this.target.getLayoutBounds("box", editor, true); this.visible = !this.target.locked; this.set({ x: x, y: y, scaleX: scaleX, scaleY: scaleY, rotation: rotation, skewX: skewX, skewY: skewY }); this.updateBounds({ x: 0, y: 0, width: width, height: height }); } unload() { this.visible = false; if (this.app) this.rect.syncEventer = this.app.interaction.bottomList = null; } updateBounds(bounds) { const {editor: editor, mergeConfig: mergeConfig, single: single, rect: rect, circle: circle, buttons: buttons, resizePoints: resizePoints, rotatePoints: rotatePoints, resizeLines: resizeLines} = this; const {editMask: editMask} = editor; const {middlePoint: middlePoint, resizeable: resizeable, rotateable: rotateable, hideOnSmall: hideOnSmall, editBox: editBox, mask: mask, dimOthers: dimOthers, bright: bright, spread: spread, hideRotatePoints: hideRotatePoints, hideResizeLines: hideResizeLines} = mergeConfig; editMask.visible = mask ? true : 0; if (!isUndefined(dimOthers) || !isUndefined(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: width, height: height} = bounds; const smallSize = isNumber(hideOnSmall) ? hideOnSmall : 10; const showPoints = editBox && !(hideOnSmall && width < smallSize && height < smallSize); let point = {}, rotateP, resizeP, resizeL; 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); resizeP.visible = showPoints && !!(resizeable || rotateable); rotateP.visible = showPoints && rotateable && resizeable && !hideRotatePoints; if (i % 2) { 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) { 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; } } } circle.visible = showPoints && rotateable && !!(mergeConfig.circle || mergeConfig.rotatePoint); if (circle.visible) this.layoutCircle(); if (rect.path) rect.path = null; rect.set(Object.assign(Object.assign({}, bounds), { visible: single ? editBox : true })); buttons.visible = showPoints && buttons.children.length > 0 || 0; if (buttons.visible) this.layoutButtons(); } else rect.set(bounds); } layoutCircle() { const {circleDirection: circleDirection, circleMargin: circleMargin, buttonsMargin: buttonsMargin, buttonsDirection: buttonsDirection, middlePoint: middlePoint} = this.mergedConfig; const direction = fourDirection.indexOf(circleDirection || (this.buttons.children.length && buttonsDirection === "bottom" ? "top" : "bottom")); this.setButtonPosition(this.circle, direction, circleMargin || buttonsMargin, !!middlePoint); } layoutButtons() { const {buttons: buttons} = this; const {buttonsDirection: buttonsDirection, buttonsFixed: buttonsFixed, buttonsMargin: buttonsMargin, middlePoint: middlePoint} = this.mergedConfig; const {flippedX: flippedX, flippedY: flippedY} = this; let index = fourDirection.indexOf(buttonsDirection); if (index % 2 && flippedX || (index + 1) % 2 && flippedY) { if (buttonsFixed) index = (index + 2) % 4; } 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; } setButtonPosition(buttons, direction, buttonsMargin, useMiddlePoint) { const point = this.resizePoints[direction * 2 + 1]; const useX = direction % 2; const sign = !direction || direction === 3 ? -1 : 1; const useWidth = direction % 2; 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; } } getPointStyle(userStyle) { const {stroke: stroke, strokeWidth: strokeWidth, pointFill: pointFill, pointSize: pointSize, pointRadius: pointRadius} = this.mergedConfig; const defaultStyle = { fill: pointFill, stroke: stroke, strokeWidth: strokeWidth, around: "center", strokeAlign: "center", opacity: 1, width: pointSize, height: pointSize, cornerRadius: pointRadius, offsetX: 0, offsetY: 0, editConfig: editConfig }; return userStyle ? Object.assign(defaultStyle, userStyle) : defaultStyle; } getPointsStyle() { const {point: point} = this.mergedConfig; return isArray(point) ? point : [ point ]; } getMiddlePointsStyle() { const {middlePoint: middlePoint} = this.mergedConfig; return isArray(middlePoint) ? middlePoint : middlePoint ? [ middlePoint ] : this.getPointsStyle(); } getResizeLinesStyle() { const {resizeLine: resizeLine} = this.mergedConfig; return isArray(resizeLine) ? resizeLine : [ resizeLine ]; } onDragStart(e) { this.dragging = true; const point = this.dragPoint = e.current, {pointType: pointType} = point; const {moveable: moveable, resizeable: resizeable, rotateable: rotateable, skewable: skewable, onCopy: onCopy} = this.mergeConfig; if (pointType === "move") { 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); } onDrag(e) { const {transformTool: transformTool, moving: moving, resizing: resizing, rotating: rotating, skewing: skewing} = this; if (moving) { transformTool.onMove(e); } else if (resizing || rotating || skewing) { const point = e.current; if (point.pointType) this.enterPoint = point; if (rotating) transformTool.onRotate(e); if (resizing) transformTool.onScale(e); if (skewing) transformTool.onSkew(e); } updatePointCursor(this, e); } onDragEnd(e) { this.onTransformEnd(e); this.dragPoint = null; } onTransformStart(e) { if (this.moving || this.gesturing) this.editor.opacity = this.mergedConfig.hideOnMove ? 0 : 1; if (this.resizing) ResizeEvent.resizingKeys = this.editor.leafList.keys; const {dragStartData: dragStartData, target: target} = this; dragStartData.x = e.x; dragStartData.y = e.y; dragStartData.totalOffset = getPointData(); dragStartData.point = { x: target.x, y: target.y }; dragStartData.bounds = Object.assign({}, target.getLayoutBounds("box", "local")); dragStartData.rotation = target.rotation; } onTransformEnd(e) { 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(); } onMove(e) { 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); } } } } onScale(e) { 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); } } } } onRotate(e) { 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); } } } } isHoldRotateKey(e) { const {rotateKey: rotateKey} = this.mergedConfig; if (rotateKey) return e.isHoldKeys(rotateKey); return e.metaKey || e.ctrlKey; } onKey(e) { updatePointCursor(this, e); } onArrow(e) { 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: keyEvent, arrowStep: arrowStep, arrowFastStep: arrowFastStep} = this.mergeConfig; if (keyEvent) { const step = e.shiftKey ? arrowFastStep : arrowStep; this.transformTool.move(x * step, y * step); } } } } onDoubleTap(e) { const {openInner: openInner, preventEditInner: preventEditInner} = this.mergeConfig; if (openInner === "double" && !preventEditInner) this.openInner(e); } onLongPress(e) { const {openInner: openInner, preventEditInner: preventEditInner} = this.mergeConfig; if (openInner === "long" && preventEditInner) this.openInner(e); } openInner(e) { const {editor: editor, target: target} = this; if (this.single) { if (target.locked) return; if (target.isBranch && !target.editInner) { if (target.textBox) { const {children: children} = target; const find = children.find(item => item.editable && item instanceof Text) || children.find(item => item instanceof Text); if (find) return editor.openInnerEditor(find); }