UNPKG

@leafer-in/editor

Version:
1,247 lines (1,220 loc) 82.3 kB
'use strict'; var draw = require('@leafer-ui/draw'); var core = require('@leafer-ui/core'); require('@leafer-in/resize'); /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ 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 ? (value instanceof Array ? value : [value]) : []; } class EditorEvent extends draw.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.BEFORE_HOVER = 'editor.before_hover'; EditorEvent.HOVER = 'editor.hover'; class EditorMoveEvent extends EditorEvent { constructor(type, data) { super(type, data); } } EditorMoveEvent.BEFORE_MOVE = 'editor.before_move'; EditorMoveEvent.MOVE = 'editor.move'; class EditorScaleEvent extends EditorEvent { constructor(type, data) { super(type, data); } } EditorScaleEvent.BEFORE_SCALE = 'editor.before_scale'; EditorScaleEvent.SCALE = 'editor.scale'; class EditorRotateEvent extends EditorEvent { constructor(type, data) { super(type, data); } } EditorRotateEvent.BEFORE_ROTATE = 'editor.before_rotate'; EditorRotateEvent.ROTATE = 'editor.rotate'; class EditorSkewEvent extends EditorEvent { constructor(type, data) { super(type, data); } } EditorSkewEvent.BEFORE_SKEW = 'editor.before_skew'; EditorSkewEvent.SKEW = 'editor.skew'; function targetAttr(fn) { return (target, key) => { const privateKey = '_' + key; draw.defineKey(target, key, { get() { return this[privateKey]; }, set(value) { const old = this[privateKey]; if (old !== value) { if (this.config) { const isSelect = key === 'target'; if (isSelect) { if (value instanceof Array && value.length > 1 && value[0].locked) value.splice(0, 1); const { beforeSelect } = this.config; if (beforeSelect) { const check = beforeSelect({ target: value }); if (typeof check === 'object') value = check; else if (check === false) return; } } const type = isSelect ? EditorEvent.BEFORE_SELECT : EditorEvent.BEFORE_HOVER; if (this.hasEvent(type)) this.emitEvent(new EditorEvent(type, { editor: this, value: value, oldValue: old })); } this[privateKey] = value, fn(this, old); } } }); }; } function mergeConfigAttr() { return (target, key) => { draw.defineKey(target, key, { get() { const { config, element, dragPoint } = this, mergeConfig = Object.assign({}, config); if (element && element.editConfig) Object.assign(mergeConfig, element.editConfig); 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'); draw.isNull(mergeConfig.lockRatio) && (mergeConfig.lockRatio = true); } } return this.mergedConfig = mergeConfig; } }); }; } const { abs } = Math; const { copy: copy$1, scale } = draw.MatrixHelper; const { setListWithFn } = draw.BoundsHelper; const { worldBounds } = draw.LeafBoundsHelper; const matrix = draw.getMatrixData(); const bounds$1 = draw.getBoundsData(); class Stroker extends draw.UI { constructor() { super(); this.list = []; this.visible = 0; this.hittable = false; this.strokeAlign = 'center'; } setTarget(target, style) { this.set(style); this.target = target; this.update(); } update() { const { list } = this; if (list.length) { setListWithFn(bounds$1, list, worldBounds); this.set(bounds$1); this.visible = true; } else this.visible = 0; } __draw(canvas, options) { const { list } = this; if (list.length) { let leaf; const data = this.__, { stroke, strokeWidth, fill } = data, { bounds } = options; for (let i = 0; i < list.length; i++) { leaf = list[i]; const { worldTransform, worldRenderBounds } = leaf; if (worldRenderBounds.width && worldRenderBounds.height && (!bounds || bounds.hit(worldRenderBounds, options.matrix))) { const aScaleX = abs(worldTransform.scaleX), aScaleY = abs(worldTransform.scaleY); copy$1(matrix, worldTransform); matrix.half = strokeWidth % 2; if (aScaleX !== aScaleY) { scale(matrix, 1 / aScaleX, 1 / aScaleY); canvas.setWorld(matrix, options.matrix); canvas.beginPath(); data.strokeWidth = strokeWidth; const { x, y, width, height } = leaf.__layout.boxBounds; canvas.rect(x * aScaleX, y * aScaleY, width * aScaleX, height * aScaleY); } else { canvas.setWorld(matrix, options.matrix); canvas.beginPath(); if (leaf.__.__useArrow) leaf.__drawPath(canvas); else leaf.__.__pathForRender ? leaf.__drawRenderPath(canvas) : leaf.__drawPathByBox(canvas); data.strokeWidth = strokeWidth / abs(worldTransform.scaleX); } if (stroke) typeof stroke === 'string' ? draw.Paint.stroke(stroke, this, canvas) : draw.Paint.strokes(stroke, this, canvas); if (fill) typeof fill === 'string' ? draw.Paint.fill(fill, this, canvas) : draw.Paint.fills(fill, this, canvas); } } data.strokeWidth = strokeWidth; } } destroy() { this.target = null; super.destroy(); } } __decorate([ targetAttr(onTarget$1) ], Stroker.prototype, "target", void 0); function onTarget$1(stroker) { const value = stroker.target; stroker.list = value ? (value instanceof Array ? value : [value]) : []; } class SelectArea extends draw.Group { constructor(data) { super(data); this.strokeArea = new draw.Rect({ strokeAlign: 'center' }); this.fillArea = new draw.Rect(); this.visible = 0; this.hittable = false; this.addMany(this.fillArea, this.strokeArea); } setStyle(style, userStyle) { const { visible, stroke, strokeWidth } = style; this.visible = visible; this.strokeArea.reset(Object.assign({ stroke, strokeWidth }, (userStyle || {}))); this.fillArea.reset({ visible: userStyle ? false : true, fill: stroke, opacity: 0.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, findByBounds } = EditSelectHelper; class EditSelect extends draw.Group { get dragging() { return !!this.originList; } get running() { const { editor } = this; return this.hittable && editor.visible && editor.hittable && editor.mergeConfig.selector; } get isMoveMode() { return this.app && this.app.interaction.moveMode; } constructor(editor) { super(); this.hoverStroker = new Stroker(); this.targetStroker = new Stroker(); this.bounds = new draw.Bounds(); this.selectArea = new SelectArea(); this.__eventIds = []; this.editor = editor; this.addMany(this.targetStroker, this.hoverStroker, this.selectArea); this.__listenEvents(); } onHover() { const { editor } = this; if (this.running && !this.dragging && !editor.dragging) { const { stroke, strokeWidth, hover, hoverStyle } = editor.mergeConfig; this.hoverStroker.setTarget(hover ? this.editor.hoverTarget : null, Object.assign({ stroke, strokeWidth }, (hoverStyle || {}))); } else { this.hoverStroker.target = null; } } onSelect() { if (this.running) { const { mergeConfig, list } = this.editor; const { stroke, strokeWidth, selectedStyle } = mergeConfig; this.targetStroker.setTarget(list, Object.assign({ stroke, strokeWidth: Math.max(1, strokeWidth / 2) }, (selectedStyle || {}))); this.hoverStroker.target = null; } } update() { this.hoverStroker.update(); this.targetStroker.update(); } onPointerMove(e) { const { app, 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 } = 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 } = this; const { select } = 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) { editor.target = null; } } checkAndSelect(e) { this.needRemoveItem = null; if (this.allowSelect(e)) { const { 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; } } else if (this.allow(e.target)) { if (!e.shiftKey) editor.target = null; } } } onDragStart(e) { if (e.multiTouch) return; if (this.waitSelect) this.waitSelect(); if (this.allowDrag(e)) { const { editor } = this; const { stroke, area } = editor.mergeConfig; const { x, y } = e.getInnerPoint(this); this.bounds.set(x, y); this.selectArea.setStyle({ visible: true, stroke, x, 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 } = this; const total = e.getInnerTotal(this); const dragBounds = this.bounds.clone().unsign(); const list = new draw.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, y } = e.getLocalMove(this); this.bounds.x += x; this.bounds.y += y; } } allow(target) { return target.leafer !== this.editor.leafer; } allowDrag(e) { const { boxSelect, multipleSelect } = this.editor.mergeConfig; if (this.running && (multipleSelect && boxSelect) && !e.target.draggable) { return (!this.editor.editing && this.allow(e.target)) || (e.shiftKey && !findOne(e.path)); } else { return false; } } allowSelect(e) { return this.running && !this.isMoveMode && !e.middle; } findDeepOne(e) { const options = { exclude: new draw.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, continuousSelect } = this.editor.mergeConfig; return multipleSelect && (e.shiftKey || continuousSelect); } __listenEvents() { const { editor } = this; editor.waitLeafer(() => { const { app } = editor; app.selector.proxy = editor; this.__eventIds = [ editor.on_(EditorEvent.HOVER, this.onHover, this), editor.on_(EditorEvent.SELECT, this.onSelect, this), app.on_(core.PointerEvent.MOVE, this.onPointerMove, this), app.on_(core.PointerEvent.BEFORE_DOWN, this.onBeforeDown, this), app.on_(core.PointerEvent.TAP, this.onTap, this), app.on_(core.DragEvent.START, this.onDragStart, this, true), app.on_(core.DragEvent.DRAG, this.onDrag, this), app.on_(core.DragEvent.END, this.onDragEnd, this), app.on_(core.MoveEvent.MOVE, this.onAutoMove, this), app.on_([core.ZoomEvent.ZOOM, core.MoveEvent.MOVE], () => { this.editor.hoverTarget = null; }), ]; }); } __removeListenEvents() { if (this.__eventIds) { this.off_(this.__eventIds); this.__eventIds.length = 0; } } destroy() { this.editor = this.originList = this.needRemoveItem = null; this.__removeListenEvents(); super.destroy(); } } const { topLeft, top, topRight, right: right$1, bottomRight, bottom, bottomLeft, left: left$1 } = draw.Direction9; const { toPoint } = draw.AroundHelper; const { within } = draw.MathHelper; const EditDataHelper = { getScaleData(element, startBounds, direction, totalMove, lockRatio, around, flipable, scaleMode) { let align, origin = {}, scaleX = 1, scaleY = 1; const { boxBounds, widthRange, heightRange, dragBounds, worldBoxBounds } = element; const { width, height } = startBounds; if (around) { totalMove.x *= 2; totalMove.y *= 2; } const originChangedScaleX = element.scaleX / startBounds.scaleX; const originChangedScaleY = element.scaleY / startBounds.scaleY; const signX = originChangedScaleX < 0 ? -1 : 1; const signY = originChangedScaleY < 0 ? -1 : 1; const changedScaleX = scaleMode ? originChangedScaleX : signX * boxBounds.width / width; const changedScaleY = scaleMode ? originChangedScaleY : signY * boxBounds.height / height; totalMove.x *= scaleMode ? originChangedScaleX : signX; totalMove.y *= scaleMode ? originChangedScaleY : signY; const topScale = (-totalMove.y + height) / height; const rightScale = (totalMove.x + width) / width; const bottomScale = (totalMove.y + height) / height; const leftScale = (-totalMove.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 { let scale; switch (direction) { case top: case bottom: scale = scaleY; break; case left$1: case right$1: scale = scaleX; break; default: scale = Math.sqrt(Math.abs(scaleX * scaleY)); } scaleX = scaleX < 0 ? -scale : scale; scaleY = scaleY < 0 ? -scale : scale; } } const useScaleX = scaleX !== 1, useScaleY = scaleY !== 1; if (useScaleX) scaleX /= changedScaleX; if (useScaleY) scaleY /= changedScaleY; if (!flipable) { const { worldTransform } = element; 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 allowBounds = dragBounds === 'parent' ? element.parent.boxBounds : dragBounds; const localBounds = new draw.Bounds(element.__localBoxBounds); localBounds.scaleOf(element.getLocalPointByInner(origin), scaleX, scaleY); if (!draw.BoundsHelper.includes(allowBounds, localBounds)) { const realBounds = localBounds.getIntersect(allowBounds); const fitScaleX = realBounds.width / localBounds.width, fitScaleY = realBounds.height / localBounds.height; if (useScaleX) scaleX *= fitScaleX; if (useScaleY) scaleY *= fitScaleY; } } if (useScaleX && widthRange) { const nowWidth = boxBounds.width * element.scaleX; scaleX = within(nowWidth * scaleX, widthRange) / nowWidth; } if (useScaleY && heightRange) { const nowHeight = boxBounds.height * element.scaleY; scaleY = within(nowHeight * scaleY, heightRange) / nowHeight; } if (useScaleX && Math.abs(scaleX * worldBoxBounds.width) < 1) scaleX = (scaleX < 0 ? -1 : 1) / worldBoxBounds.width; if (useScaleY && Math.abs(scaleY * worldBoxBounds.height) < 1) scaleY = (scaleY < 0 ? -1 : 1) / worldBoxBounds.height; if (lockRatio && scaleX !== scaleY) scaleY = scaleX = Math.min(scaleX, scaleY); return { origin, scaleX, scaleY, direction, lockRatio, around }; }, getRotateData(bounds, 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, bounds, origin, true); return { origin, rotation: draw.PointHelper.getRotation(last, origin, current) }; }, getSkewData(bounds, direction, move, around) { let align, origin = {}, skewX = 0, skewY = 0; let last; switch (direction) { case top: case topLeft: last = { x: 0.5, y: 0 }; align = 'bottom'; skewX = 1; break; case bottom: case bottomRight: last = { x: 0.5, y: 1 }; align = 'top'; skewX = 1; break; case left$1: case bottomLeft: last = { x: 0, y: 0.5 }; align = 'right'; skewY = 1; break; case right$1: case topRight: last = { x: 1, y: 0.5 }; align = 'left'; skewY = 1; } const { width, height } = bounds; last.x = last.x * width; last.y = last.y * height; toPoint(around || align, bounds, origin, true); const rotation = draw.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, skewX, 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 updateCursor(editor, e) { const { editBox } = editor, point = editBox.enterPoint; if (!point || !editor.editing || !editBox.visible) return; if (point.name === 'circle') return; if (point.pointType === 'button') { if (!point.cursor) point.cursor = 'pointer'; return; } let { rotation } = editBox; const { resizeCursor, rotateCursor, skewCursor, resizeable, rotateable, skewable } = editor.mergeConfig; const { pointType } = point, { flippedX, flippedY } = editBox; let showResize = pointType.includes('resize'); if (showResize && rotateable && (e.metaKey || e.ctrlKey || !resizeable)) showResize = false; const showSkew = skewable && !showResize && (point.name === 'resize-line' || pointType === 'skew'); const cursor = showSkew ? skewCursor : (showResize ? resizeCursor : rotateCursor); rotation += (EditDataHelper.getFlipDirection(point.direction, flippedX, flippedY) + 1) * 45; rotation = Math.round(draw.MathHelper.formatRotation(rotation, true) / 2) * 2; const { url, x, y } = cursor; const key = url + rotation; if (cacheCursors[key]) { point.cursor = cacheCursors[key]; } else { cacheCursors[key] = point.cursor = { url: toDataURL(url, rotation), x, y }; } } function updateMoveCursor(editor) { const { moveCursor, moveable } = editor.mergeConfig; editor.editBox.rect.cursor = moveable ? moveCursor : undefined; } function toDataURL(svg, rotation) { return '"data:image/svg+xml,' + encodeURIComponent(svg.replace('{{rotation}}', rotation.toString())) + '"'; } class EditPoint extends draw.Box { } const fourDirection = ['top', 'right', 'bottom', 'left'], editConfig = undefined; class EditBox extends draw.Group { 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; } constructor(editor) { super(); this.view = new draw.Group(); this.rect = new draw.Box({ 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 draw.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, resizePoints, rotatePoints, resizeLines, rect, circle, 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); view.addMany(...rotatePoints, rect, circle, buttons, ...resizeLines, ...resizePoints); this.add(view); } load() { const { mergeConfig, element, single } = this.editor; const { rect, circle, resizePoints } = this; const { stroke, strokeWidth } = mergeConfig; const pointsStyle = this.getPointsStyle(); const middlePointsStyle = this.getMiddlePointsStyle(); 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])); if (!(i % 2)) resizeP.rotation = (i / 2) * 90; } circle.set(this.getPointStyle(mergeConfig.circle || mergeConfig.rotatePoint || pointsStyle[0])); rect.set(Object.assign({ stroke, strokeWidth, editConfig }, (mergeConfig.rect || {}))); rect.hittable = !single; rect.syncEventer = single && this.editor; if (single) { element.syncEventer = rect; this.app.interaction.bottomList = [{ target: rect, proxy: element }]; } } update(bounds) { const { rect, circle, buttons, resizePoints, rotatePoints, resizeLines, editor } = this; const { mergeConfig, element, multiple, editMask } = editor; const { middlePoint, resizeable, rotateable, hideOnSmall, editBox, mask } = mergeConfig; this.visible = !element.locked; editMask.visible = mask ? true : 0; 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 = {}, rotateP, resizeP, resizeL; for (let i = 0; i < 8; i++) { draw.AroundHelper.toPoint(draw.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); resizeP.visible = resizeL.visible = showPoints && !!(resizeable || rotateable); rotateP.visible = showPoints && rotateable && resizeable && !mergeConfig.rotatePoint; if (i % 2) { resizeP.visible = rotateP.visible = showPoints && !!middlePoint; if (((i + 1) / 2) % 2) { resizeL.width = width; if (hideOnSmall && resizeP.width * 2 > width) resizeP.visible = false; } else { resizeL.height = height; resizeP.rotation = 90; if (hideOnSmall && resizeP.width * 2 > height) resizeP.visible = false; } } } circle.visible = showPoints && rotateable && !!(mergeConfig.circle || mergeConfig.rotatePoint); if (circle.visible) this.layoutCircle(mergeConfig); if (rect.path) rect.path = null; rect.set(Object.assign(Object.assign({}, bounds), { visible: multiple ? true : editBox })); buttons.visible = showPoints && buttons.children.length > 0 || 0; if (buttons.visible) this.layoutButtons(mergeConfig); } else rect.set(bounds); } layoutCircle(config) { const { circleDirection, circleMargin, buttonsMargin, buttonsDirection, middlePoint } = config; const direction = fourDirection.indexOf(circleDirection || ((this.buttons.children.length && buttonsDirection === 'bottom') ? 'top' : 'bottom')); this.setButtonPosition(this.circle, direction, circleMargin || buttonsMargin, !!middlePoint); } layoutButtons(config) { const { buttons } = this; const { buttonsDirection, buttonsFixed, buttonsMargin, middlePoint } = config; const { flippedX, 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; } } unload() { this.visible = false; } getPointStyle(userStyle) { const { stroke, strokeWidth, pointFill, pointSize, pointRadius } = this.editor.mergeConfig; const defaultStyle = { fill: pointFill, stroke, strokeWidth, around: 'center', strokeAlign: 'center', width: pointSize, height: pointSize, cornerRadius: pointRadius, offsetX: 0, offsetY: 0, editConfig }; return userStyle ? Object.assign(defaultStyle, userStyle) : defaultStyle; } getPointsStyle() { const { point } = this.editor.mergeConfig; return point instanceof Array ? point : [point]; } getMiddlePointsStyle() { const { middlePoint } = this.editor.mergeConfig; return middlePoint instanceof Array ? middlePoint : (middlePoint ? [middlePoint] : this.getPointsStyle()); } onSelect(e) { if (e.oldList.length === 1) { e.oldList[0].syncEventer = null; if (this.app) this.app.interaction.bottomList = null; } } onDragStart(e) { this.dragging = true; const point = this.dragPoint = e.current, { pointType } = point; const { editor, dragStartData } = this, { element } = editor; if (point.name === 'rect') { this.moving = true; editor.opacity = editor.mergeConfig.hideOnMove ? 0 : 1; } dragStartData.x = e.x; dragStartData.y = e.y; dragStartData.point = { x: element.x, y: element.y }; dragStartData.bounds = Object.assign({}, element.getLayoutBounds('box', 'local')); dragStartData.rotation = element.rotation; if (pointType && pointType.includes('resize')) draw.ResizeEvent.resizingKeys = editor.leafList.keys; } onDragEnd(e) { this.dragging = false; this.dragPoint = null; this.moving = false; const { name, pointType } = e.current; if (name === 'rect') this.editor.opacity = 1; if (pointType && pointType.includes('resize')) draw.ResizeEvent.resizingKeys = null; } onDrag(e) { const { editor } = this; const { pointType } = this.enterPoint = e.current; if (pointType.includes('rotate') || e.metaKey || e.ctrlKey || !editor.mergeConfig.resizeable) { editor.onRotate(e); if (pointType === 'resize-rotate') editor.onScale(e); } else if (pointType === 'resize') editor.onScale(e); if (pointType === 'skew') editor.onSkew(e); updateCursor(editor, e); } onArrow(e) { const { editor } = this; if (editor.editing && editor.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) editor.move(x, y); } } onDoubleTap(e) { if (this.editor.mergeConfig.openInner === 'double') this.openInner(e); } onLongPress(e) { if (this.editor.mergeConfig.openInner === 'long') this.openInner(e); } openInner(e) { const { editor } = this; if (editor.single) { const { element } = editor; if (element.locked) return; if (element.isBranch && !element.editInner) { if (element.textBox) { const { children } = element; const find = children.find(item => item.editable && item instanceof draw.Text) || children.find(item => item instanceof draw.Text); if (find) return editor.openInnerEditor(find); } editor.openGroup(element); editor.target = editor.selector.findDeepOne(e); } else { editor.openInnerEditor(); } } } listenPointEvents(point, type, direction) { const { editor } = this; point.direction = direction; point.pointType = type; point.on_(core.DragEvent.START, this.onDragStart, this); point.on_(core.DragEvent.DRAG, this.onDrag, this); point.on_(core.DragEvent.END, this.onDragEnd, this); point.on_(core.PointerEvent.LEAVE, () => this.enterPoint = null); if (point.name !== 'circle') point.on_(core.PointerEvent.ENTER, (e) => { this.enterPoint = point, updateCursor(editor, e); }); } __listenEvents() { const { rect, editor } = this; this.__eventIds = [ editor.on_(EditorEvent.SELECT, this.onSelect, this), rect.on_(core.DragEvent.START, this.onDragStart, this), rect.on_(core.DragEvent.DRAG, editor.onMove, editor), rect.on_(core.DragEvent.END, this.onDragEnd, this), rect.on_(core.PointerEvent.ENTER, () => updateMoveCursor(editor)), rect.on_(core.PointerEvent.DOUBLE_TAP, this.onDoubleTap, this), rect.on_(core.PointerEvent.LONG_PRESS, this.onLongPress, this) ]; } __removeListenEvents() { this.off_(this.__eventIds); this.__eventIds.length = 0; } destroy() { this.editor = null; this.__removeListenEvents(); super.destroy(); } } const bigBounds = { x: 0, y: 0, width: 100000, height: 100000 }; class EditMask extends draw.UI { constructor(editor) { super(); this.editor = editor; this.hittable = false; this.visible = 0; } __updateWorldBounds() { Object.assign(this.__local, bigBounds); Object.assign(this.__world, bigBounds); } __draw(canvas, options) { const { editor } = this, { mask } = editor.mergedConfig; if (mask && editor.editing) { canvas.fillWorld(canvas.bounds, mask === true ? 'rgba(0,0,0,0.8)' : mask); if (options.bounds && !options.bounds.hit(editor.editBox.rect.__world, options.matrix)) return; canvas.saveBlendMode('destination-out'); editor.list.forEach(item => { item.__renderShape(canvas, options); const { __box, parent } = item; if ((item = __box) || ((item = parent) && parent.textBox)) item.__renderShape(canvas, options); }); canvas.restoreBlendMode(); } } destroy() { this.editor = null; super.destroy(); } } const filterStyle = ` <feOffset dy="1"/> <feGaussianBlur stdDeviation="1.5"/> <feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/> <feBlend mode="normal" in="SourceGraphic" result="shape"/>`; const resizeSVG = ` <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> <g filter="url(#f)"> <g transform="rotate({{rotation}},12,12)"> <path d="M7.5 8.0H8.5V5.9L6.8 7.2L7.5 8.0ZM3 11.4L2.3 10.6L1.3 11.4L2.3 12.2L3 11.4ZM7.5 10.4H6.5V11.4H7.5V10.4ZM16.5 10.4V11.4H17.5V10.4H16.5ZM16.5 8.0L17.1 7.2L15.5 5.9V8.0H16.5ZM21 11.4L21.6 12.2L22.6 11.4L21.6 10.6L21 11.4ZM16.5 14.9H15.5V16.9L17.1 15.7L16.5 14.9ZM16.5 12.4H17.5V11.4H16.5V12.4ZM7.5 12.4V11.4H6.5V12.4H7.5ZM7.5 14.9L6.8 15.7L8.5 16.9V14.9H7.5ZM6.8 7.2L2.3 10.6L3.6 12.2L8.1 8.7L6.8 7.2ZM8.5 10.4V8.0H6.5V10.4H8.5ZM16.5 9.4H7.5V11.4H16.5V9.4ZM17.5 10.4V8.0H15.5V10.4H17.5ZM15.8 8.7L20.3 12.2L21.6 10.6L17.1 7.2L15.8 8.7ZM20.3 10.6L15.8 14.1L17.1 15.7L21.6 12.2L20.3 10.6ZM17.5 14.9V12.4H15.5V14.9H17.5ZM7.5 13.4H16.5V11.4H7.5V13.4ZM8.5 14.9V12.4H6.5V14.9H8.5ZM2.3 12.2L6.8 15.7L8.1 14.1L3.6 10.6L2.3 12.2Z" fill="white"/> <path fill-rule="evenodd" d="M3 11.4L7.5 8.0V10.4H16.5V8.0L21 11.4L16.5 14.9V12.4H7.5V14.9L3 11.4Z" fill="black"/> </g> </g> <defs> <filter id="f" x="-1.6" y="3.9" width="27.2" height="16.9" filterUnits="userSpaceOnUse"> ${filterStyle} </filter> </defs> </svg> `; const rotateSVG = ` <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> <g filter="url(#f)"> <g transform="rotate(135,12,12),rotate({{rotation}},12,12)"> <path d="M20.4 8H21.4L20.8 7.1L17.3 2.6L17 2.1L16.6 2.6L13.1 7.1L12.5 8H13.5H15.4C14.9 11.8 11.8 14.9 8 15.4V13.5V12.5L7.1 13.1L2.6 16.6L2.1 17L2.6 17.3L7.1 20.8L8 21.4V20.4V18.4C13.5 17.9 17.9 13.5 18.4 8H20.4Z" stroke="white"/> <path fill-rule="evenodd" d="M17 3L20.4 7.5H17.9C17.7 13.1 13.1 17.7 7.5 17.9V20.4L3 17L7.5 13.5V15.9C12.0 15.7 15.7 12.0 15.9 7.5H13.5L17 3Z" fill="black"/> </g> </g> <defs> <filter id="f" x="-1.6" y="-0.6" width="27.1" height="27.1" filterUnits="userSpaceOnUse"> ${filterStyle} </filter> </defs> </svg> `; const skewSVG = ` <svg width="24" height="24" xmlns="http://www.w3.org/2000/svg"> <g filter="url(#f)"> <g transform="rotate(90,12,12),rotate({{rotation}},12,12)"> <path d="M21 10.4L21 11.4L23.8 11.4L21.6 9.6L21 10.4ZM17 10.4V11.4L17 11.4L17 10.4ZM15.5 6L16.1 5.2L14.5 3.9V6H15.5ZM15.5 8.4V9.4H16.5V8.4H15.5ZM6 8.4V7.4H5V8.4H6ZM6 10.4H5V11.4H6V10.4ZM7 14.4V13.4L7 13.4L7 14.4ZM3 14.4L3 13.4L0.1 13.4L2.3 15.2L3 14.4ZM8.5 18.9L7.8 19.7L9.5 21.0V18.9H8.5ZM8.5 16.4V15.4H7.5V16.4H8.5ZM19 16.4V17.4H20V16.4H19ZM19 14.4H20V13.4H19V14.4ZM21 9.4L17 9.4L17 11.4L21 11.4L21 9.4ZM14.8 6.7L20.3 11.2L21.6 9.6L16.1 5.2L14.8 6.7ZM16.5 8.4V6H14.5V8.4H16.5ZM6 9.4H15.5V7.4H6V9.4ZM7 10.4V8.4H5V10.4H7ZM15.5 9.4H6V11.4H15.5V9.4ZM17 9.4H15.5V11.4H17V9.4ZM7 15.4H8.5V13.4H7V15.4ZM3 15.4L7 15.4L7 13.4L3 13.4L3 15.4ZM9.1 18.1L3.6 13.6L2.3 15.2L7.8 19.7L9.1 18.1ZM7.5 16.4V18.9H9.5V16.4H7.5ZM19 15.4H8.5V17.4H19V15.4ZM18 14.4V16.4H20V14.4H18ZM8.5 15.4H19V13.4H8.5V15.4Z" fill="white"/> <path fill-rule="evenodd" d="M17 10.4L21 10.4L15.5 6V8.4H6V10.4H15.5H17ZM8.5 14.4H7L3 14.4L8.5 18.9V16.4H19V14.4H8.5Z" fill="black"/> </g> </g> <defs> <filter x="-2.8" y="1.9" width="29.6" height="23.1" filterUnits="userSpaceOnUse" > ${filterStyle} </filter> </defs> </svg> `; const config = { editSize: 'size', keyEvent: true, stroke: '#836DFF', strokeWidth: 2, pointFill: '#FFFFFF', pointSize: 10, pointRadius: 16, rotateGap: 45, buttonsDirection: 'bottom', buttonsMargin: 12, hideOnSmall: true, moveCursor: 'move', resizeCursor: { url: resizeSVG, x: 12, y: 12 }, rotateCursor: { url: rotateSVG, x: 12, y: 12 }, skewCursor: { url: skewSVG, x: 12, y: 12 }, selector: true, editBox: true, hover: true, select: 'press', openInner: 'double', multipleSelect: true, boxSelect: true, moveable: true, resizeable: true, flipable: true, rotateable: true, skewable: true }; const bounds = new draw.Bounds(); function simulate(editor) { const { simulateTarget, list } = editor; const { zoomLayer } = list[0].leafer.zoomLayer; simulateTarget.safeChange(() => { bounds.setListWithFn(list, (leaf) => leaf.getBounds('box', 'page')); if (bounds.width === 0) bounds.width = 0.1; if (bounds.height === 0) bounds.height = 0.1; simulateTarget.reset(bounds.get()); }); zoomLayer.add(simulateTarget); } function onTarget(editor, oldValue) { const { target } = editor; if (target) { editor.leafList = target instanceof draw.LeafList ? target : new draw.LeafList(target); if (editor.multiple) simulate(editor); } else { editor.simulateTarget.remove(); editor.leafList.reset(); editor.closeInnerEditor(); } editor.emitEvent(new EditorEvent(EditorEvent.SELECT, { editor, value: target, oldValue })); editor.checkOpenedGroups(); if (editor.editing) { editor.waitLeafer(() => { updateMoveCursor(editor); editor.updateEditTool(); editor.update(); editor.listenTargetEvents(); }); } else { editor.updateEditTool(); editor.removeTargetEvents(); } } function onHover(editor, oldValue) { editor.emitEvent(new EditorEvent(EditorEvent.HOVER, { editor, value: editor.hoverTarget, oldValue })); } const order = (a, b) => a.parent.children.indexOf(a) - b.parent.children.indexOf(b); const reverseOrder = (a, b) => b.parent.children.indexOf(b) - a.parent.children.indexOf(a); const EditorHelper = { group(list, element, userGroup) { list.sort(reverseOrder); const { app, parent } = list[0]; let group; if (userGroup && userGroup.add) { group = userGroup; } else { group = new draw.Group(userGroup); } parent.addAt(group, parent.children.indexOf(list[0])); list.sort(order); const matrx = new draw.Matrix(element.worldTransform); matrx