UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

1,006 lines (1,005 loc) 35.4 kB
import { _defineProperty } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/defineProperty.mjs"; import "../constants.mjs"; import { classRegistry } from "../ClassRegistry.mjs"; import { Point } from "../Point.mjs"; import { isActiveSelection } from "../util/typeAssertions.mjs"; import { getDocumentFromElement, getWindowFromElement } from "../util/dom_misc.mjs"; import { isTouchEvent, stopEvent } from "../util/dom_event.mjs"; import { sendPointToPlane } from "../util/misc/planeChange.mjs"; import { SelectableCanvas } from "./SelectableCanvas.mjs"; import { TextEditingManager } from "./TextEditingManager.mjs"; //#region src/canvas/Canvas.ts const addEventOptions = { passive: false }; const getEventPoints = (canvas, e) => { return { viewportPoint: canvas.getViewportPoint(e), scenePoint: canvas.getScenePoint(e) }; }; const addListener = (el, ...args) => el.addEventListener(...args); const removeListener = (el, ...args) => el.removeEventListener(...args); const syntheticEventConfig = { mouse: { in: "over", out: "out", targetIn: "mouseover", targetOut: "mouseout", canvasIn: "mouse:over", canvasOut: "mouse:out" }, drag: { in: "enter", out: "leave", targetIn: "dragenter", targetOut: "dragleave", canvasIn: "drag:enter", canvasOut: "drag:leave" } }; var Canvas = class extends SelectableCanvas { constructor(el, options = {}) { super(el, options); _defineProperty(this, "_isClick", void 0); _defineProperty(this, "textEditingManager", new TextEditingManager(this)); [ "_onMouseDown", "_onTouchStart", "_onMouseMove", "_onMouseUp", "_onTouchEnd", "_onResize", "_onMouseWheel", "_onMouseOut", "_onMouseEnter", "_onContextMenu", "_onClick", "_onDragStart", "_onDragEnd", "_onDragProgress", "_onDragOver", "_onDragEnter", "_onDragLeave", "_onDrop" ].forEach((eventHandler) => { this[eventHandler] = this[eventHandler].bind(this); }); this.addOrRemove(addListener); } /** * return an event prefix pointer or mouse. * @private */ _getEventPrefix() { return this.enablePointerEvents ? "pointer" : "mouse"; } addOrRemove(functor, forTouch = false) { const canvasElement = this.upperCanvasEl, eventTypePrefix = this._getEventPrefix(); functor(getWindowFromElement(canvasElement), "resize", this._onResize); functor(canvasElement, eventTypePrefix + "down", this._onMouseDown); functor(canvasElement, `${eventTypePrefix}move`, this._onMouseMove, addEventOptions); functor(canvasElement, `${eventTypePrefix}out`, this._onMouseOut); functor(canvasElement, `${eventTypePrefix}enter`, this._onMouseEnter); functor(canvasElement, "wheel", this._onMouseWheel, { passive: false }); functor(canvasElement, "contextmenu", this._onContextMenu); if (!forTouch) { functor(canvasElement, "click", this._onClick); functor(canvasElement, "dblclick", this._onClick); } functor(canvasElement, "dragstart", this._onDragStart); functor(canvasElement, "dragend", this._onDragEnd); functor(canvasElement, "dragover", this._onDragOver); functor(canvasElement, "dragenter", this._onDragEnter); functor(canvasElement, "dragleave", this._onDragLeave); functor(canvasElement, "drop", this._onDrop); if (!this.enablePointerEvents) functor(canvasElement, "touchstart", this._onTouchStart, addEventOptions); } /** * Removes all event listeners, used when disposing the instance */ removeListeners() { this.addOrRemove(removeListener); const eventTypePrefix = this._getEventPrefix(); const doc = getDocumentFromElement(this.upperCanvasEl); removeListener(doc, `${eventTypePrefix}up`, this._onMouseUp); removeListener(doc, "touchend", this._onTouchEnd, addEventOptions); removeListener(doc, `${eventTypePrefix}move`, this._onMouseMove, addEventOptions); removeListener(doc, "touchmove", this._onMouseMove, addEventOptions); clearTimeout(this._willAddMouseDown); } /** * @private * @param {Event} [e] Event object fired on wheel event */ _onMouseWheel(e) { this._cacheTransformEventData(e); this._handleEvent(e, "wheel"); this._resetTransformEventData(); } /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseOut(e) { const target = this._hoveredTarget; const shared = { e, ...getEventPoints(this, e) }; this.fire("mouse:out", { ...shared, target }); this._hoveredTarget = void 0; target && target.fire("mouseout", { ...shared }); this._hoveredTargets.forEach((nestedTarget) => { this.fire("mouse:out", { ...shared, target: nestedTarget }); nestedTarget && nestedTarget.fire("mouseout", { ...shared }); }); this._hoveredTargets = []; } /** * @private * Used when the mouse cursor enter the canvas from outside * @param {Event} e Event object fired on mouseenter */ _onMouseEnter(e) { const { target } = this.findTarget(e); if (!this._currentTransform && !target) { this.fire("mouse:over", { e, ...getEventPoints(this, e) }); this._hoveredTarget = void 0; this._hoveredTargets = []; } } /** * supports native like text dragging * @private * @param {DragEvent} e */ _onDragStart(e) { this._isClick = false; const activeObject = this.getActiveObject(); if (activeObject && activeObject.onDragStart(e)) { this._dragSource = activeObject; const options = { e, target: activeObject }; this.fire("dragstart", options); activeObject.fire("dragstart", options); addListener(this.upperCanvasEl, "drag", this._onDragProgress); return; } stopEvent(e); } /** * First we clear top context where the effects are being rendered. * Then we render the effects. * Doing so will render the correct effect for all cases including an overlap between `source` and `target`. * @private */ _renderDragEffects(e, source, target) { let dirty = false; const dropTarget = this._dropTarget; if (dropTarget && dropTarget !== source && dropTarget !== target) { dropTarget.clearContextTop(); dirty = true; } source === null || source === void 0 || source.clearContextTop(); target !== source && (target === null || target === void 0 || target.clearContextTop()); const ctx = this.contextTop; ctx.save(); ctx.transform(...this.viewportTransform); if (source) { ctx.save(); source.transform(ctx); source.renderDragSourceEffect(e); ctx.restore(); dirty = true; } if (target) { ctx.save(); target.transform(ctx); target.renderDropTargetEffect(e); ctx.restore(); dirty = true; } ctx.restore(); dirty && (this.contextTopDirty = true); } /** * supports native like text dragging * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#finishing_a_drag * @private * @param {DragEvent} e */ _onDragEnd(e) { const { currentSubTargets } = this.findTarget(e); const didDrop = !!e.dataTransfer && e.dataTransfer.dropEffect !== "none", dropTarget = didDrop ? this._activeObject : void 0, options = { e, target: this._dragSource, subTargets: currentSubTargets, dragSource: this._dragSource, didDrop, dropTarget }; removeListener(this.upperCanvasEl, "drag", this._onDragProgress); this.fire("dragend", options); this._dragSource && this._dragSource.fire("dragend", options); delete this._dragSource; this._onMouseUp(e); } /** * fire `drag` event on canvas and drag source * @private * @param {DragEvent} e */ _onDragProgress(e) { const options = { e, target: this._dragSource, dragSource: this._dragSource, dropTarget: this._draggedoverTarget }; this.fire("drag", options); this._dragSource && this._dragSource.fire("drag", options); } /** * prevent default to allow drop event to be fired * https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Drag_operations#specifying_drop_targets * @private * @param {DragEvent} [e] Event object fired on Event.js shake */ _onDragOver(e) { const eventType = "dragover"; const { currentContainer: target, currentSubTargets } = this.findTarget(e); const dragSource = this._dragSource; const options = { e, target, subTargets: currentSubTargets, dragSource, canDrop: false, dropTarget: void 0 }; let dropTarget; this.fire(eventType, options); this._fireEnterLeaveEvents(e, target, options); if (target) { if (target.canDrop(e)) dropTarget = target; target.fire(eventType, options); } for (let i = 0; i < currentSubTargets.length; i++) { const subTarget = currentSubTargets[i]; if (subTarget.canDrop(e)) dropTarget = subTarget; subTarget.fire(eventType, options); } this._renderDragEffects(e, dragSource, dropTarget); this._dropTarget = dropTarget; } /** * fire `dragleave` on `dragover` targets * @private * @param {Event} [e] Event object fired on Event.js shake */ _onDragEnter(e) { const { currentContainer, currentSubTargets } = this.findTarget(e); const options = { e, target: currentContainer, subTargets: currentSubTargets, dragSource: this._dragSource }; this.fire("dragenter", options); this._fireEnterLeaveEvents(e, currentContainer, options); } /** * fire `dragleave` on `dragover` targets * @private * @param {Event} [e] Event object fired on Event.js shake */ _onDragLeave(e) { const { currentSubTargets } = this.findTarget(e); const options = { e, target: this._draggedoverTarget, subTargets: currentSubTargets, dragSource: this._dragSource }; this.fire("dragleave", options); this._fireEnterLeaveEvents(e, void 0, options); this._renderDragEffects(e, this._dragSource); this._dropTarget = void 0; this._hoveredTargets = []; } /** * `drop:before` is a an event that allows you to schedule logic * before the `drop` event. Prefer `drop` event always, but if you need * to run some drop-disabling logic on an event, since there is no way * to handle event handlers ordering, use `drop:before` * @private * @param {Event} e */ _onDrop(e) { const { currentContainer, currentSubTargets } = this.findTarget(e); const options = this._basicEventHandler("drop:before", { e, target: currentContainer, subTargets: currentSubTargets, dragSource: this._dragSource, ...getEventPoints(this, e) }); options.didDrop = false; options.dropTarget = void 0; this._basicEventHandler("drop", options); this.fire("drop:after", options); } /** * @private * @param {Event} e Event object fired on mousedown */ _onContextMenu(e) { const { target, subTargets } = this.findTarget(e); const options = this._basicEventHandler("contextmenu:before", { e, target, subTargets }); this.stopContextMenu && stopEvent(e); this._basicEventHandler("contextmenu", options); return false; } /** * @private * @param {Event} e Event object fired on mousedown */ _onClick(e) { const clicks = e.detail; if (clicks > 3 || clicks < 2) return; this._cacheTransformEventData(e); clicks == 2 && e.type === "dblclick" && this._handleEvent(e, "dblclick"); clicks == 3 && this._handleEvent(e, "tripleclick"); this._resetTransformEventData(); } /** * This supports gesture event firing * It is a method to keep some code organized, it exposes private methods * in a way that works and still keep them private * This is supposed to mirror _handleEvent */ fireEventFromPointerEvent(e, eventName, secondaryName, extraData = {}) { this._cacheTransformEventData(e); const { target, subTargets } = this.findTarget(e), options = { e, target, subTargets, ...getEventPoints(this, e), transform: this._currentTransform, ...extraData }; this.fire(eventName, options); target && target.fire(secondaryName, options); for (let i = 0; i < subTargets.length; i++) subTargets[i] !== target && subTargets[i].fire(secondaryName, options); this._resetTransformEventData(); } /** * Return a the id of an event. * returns either the pointerId or the identifier or 0 for the mouse event * @private * @param {Event} evt Event object */ getPointerId(evt) { const changedTouches = evt.changedTouches; if (changedTouches) return changedTouches[0] && changedTouches[0].identifier; if (this.enablePointerEvents) return evt.pointerId; return -1; } /** * Determines if an event has the id of the event that is considered main * @private * @param {evt} event Event object */ _isMainEvent(evt) { if (evt.isPrimary === true) return true; if (evt.isPrimary === false) return false; if (evt.type === "touchend" && evt.touches.length === 0) return true; if (evt.changedTouches) return evt.changedTouches[0].identifier === this.mainTouchId; return true; } /** * @private * @param {Event} e Event object fired on mousedown */ _onTouchStart(e) { this._cacheTransformEventData(e); let shouldPreventScrolling = !this.allowTouchScrolling; const currentActiveObject = this._activeObject; if (this.mainTouchId === void 0) this.mainTouchId = this.getPointerId(e); this.__onMouseDown(e); const { target } = this.findTarget(e); if (this.isDrawingMode || currentActiveObject && target === currentActiveObject) shouldPreventScrolling = true; shouldPreventScrolling && e.preventDefault(); const canvasElement = this.upperCanvasEl, eventTypePrefix = this._getEventPrefix(); const doc = getDocumentFromElement(canvasElement); addListener(doc, "touchend", this._onTouchEnd, addEventOptions); shouldPreventScrolling && addListener(doc, "touchmove", this._onMouseMove, addEventOptions); removeListener(canvasElement, `${eventTypePrefix}down`, this._onMouseDown); this._resetTransformEventData(); } /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDown(e) { this._cacheTransformEventData(e); this.__onMouseDown(e); const canvasElement = this.upperCanvasEl, eventTypePrefix = this._getEventPrefix(); removeListener(canvasElement, `${eventTypePrefix}move`, this._onMouseMove, addEventOptions); const doc = getDocumentFromElement(canvasElement); addListener(doc, `${eventTypePrefix}up`, this._onMouseUp); addListener(doc, `${eventTypePrefix}move`, this._onMouseMove, addEventOptions); this._resetTransformEventData(); } /** * @private * @param {Event} e Event object fired on mousedown */ _onTouchEnd(e) { if (e.touches.length > 0) return; this._cacheTransformEventData(e); this.__onMouseUp(e); this._resetTransformEventData(); delete this.mainTouchId; const eventTypePrefix = this._getEventPrefix(); const doc = getDocumentFromElement(this.upperCanvasEl); removeListener(doc, "touchend", this._onTouchEnd, addEventOptions); removeListener(doc, "touchmove", this._onMouseMove, addEventOptions); if (this._willAddMouseDown) clearTimeout(this._willAddMouseDown); this._willAddMouseDown = setTimeout(() => { addListener(this.upperCanvasEl, `${eventTypePrefix}down`, this._onMouseDown); this._willAddMouseDown = 0; }, 400); } /** * @private * @param {Event} e Event object fired on mouseup */ _onMouseUp(e) { this._cacheTransformEventData(e); this.__onMouseUp(e); const canvasElement = this.upperCanvasEl, eventTypePrefix = this._getEventPrefix(); if (this._isMainEvent(e)) { const doc = getDocumentFromElement(this.upperCanvasEl); removeListener(doc, `${eventTypePrefix}up`, this._onMouseUp); removeListener(doc, `${eventTypePrefix}move`, this._onMouseMove, addEventOptions); addListener(canvasElement, `${eventTypePrefix}move`, this._onMouseMove, addEventOptions); } this._resetTransformEventData(); } /** * @private * @param {Event} e Event object fired on mousemove */ _onMouseMove(e) { this._cacheTransformEventData(e); const activeObject = this.getActiveObject(); !this.allowTouchScrolling && (!activeObject || !activeObject.shouldStartDragging(e)) && e.preventDefault && e.preventDefault(); this.__onMouseMove(e); this._resetTransformEventData(); } /** * @private */ _onResize() { this.calcOffset(); this._resetTransformEventData(); } /** * Decides whether the canvas should be redrawn in mouseup and mousedown events. * @private * @param {Object} target */ _shouldRender(target) { const activeObject = this.getActiveObject(); return !!activeObject !== !!target || activeObject && target && activeObject !== target; } /** * Method that defines the actions when mouse is released on canvas. * The method resets the currentTransform parameters, store the image corner * position in the image object and render the canvas on top. * @private * @param {Event} e Event object fired on mouseup */ __onMouseUp(e) { var _this$_activeObject; this._handleEvent(e, "up:before"); const transform = this._currentTransform; const isClick = this._isClick; const { target } = this.findTarget(e); const { button } = e; if (button) { (this.fireMiddleClick && button === 1 || this.fireRightClick && button === 2) && this._handleEvent(e, "up"); return; } if (this.isDrawingMode && this._isCurrentlyDrawing) { this._onMouseUpInDrawingMode(e); return; } if (!this._isMainEvent(e)) return; let shouldRender = false; if (transform) { this._finalizeCurrentTransform(e); shouldRender = transform.actionPerformed; } if (!isClick) { const targetWasActive = target === this._activeObject; this.handleSelection(e); if (!shouldRender) shouldRender = this._shouldRender(target) || !targetWasActive && target === this._activeObject; } let pointer, corner; if (target) { const { key, control } = target.findControl(this.getViewportPoint(e), isTouchEvent(e)) || {}; corner = key; if (target.selectable && target !== this._activeObject && target.activeOn === "up") { this.setActiveObject(target, e); shouldRender = true; } else if (control) { const mouseUpHandler = control.getMouseUpHandler(e, target, control); if (mouseUpHandler) { pointer = this.getScenePoint(e); mouseUpHandler.call(control, e, transform, pointer.x, pointer.y); } } target.isMoving = false; } if (transform && (transform.target !== target || transform.corner !== corner)) { const originalControl = transform.target && transform.target.controls[transform.corner], originalMouseUpHandler = originalControl && originalControl.getMouseUpHandler(e, transform.target, originalControl); pointer = pointer || this.getScenePoint(e); originalMouseUpHandler && originalMouseUpHandler.call(originalControl, e, transform, pointer.x, pointer.y); } this._setCursorFromEvent(e, target); this._handleEvent(e, "up"); this._groupSelector = null; this._currentTransform = null; target && (target.__corner = void 0); if (shouldRender) this.requestRenderAll(); else if (!isClick && !((_this$_activeObject = this._activeObject) === null || _this$_activeObject === void 0 ? void 0 : _this$_activeObject.isEditing)) this.renderTop(); } _basicEventHandler(eventType, options) { const { target, subTargets = [] } = options; this.fire(eventType, options); target && target.fire(eventType, options); for (let i = 0; i < subTargets.length; i++) subTargets[i] !== target && subTargets[i].fire(eventType, options); return options; } /** * @private * Handle event firing for target and subtargets * @param {TPointerEvent} e event from mouse * @param {TPointerEventNames} eventType */ _handleEvent(e, eventType, extraData) { const { target, subTargets } = this.findTarget(e), options = { e, target, subTargets, ...getEventPoints(this, e), transform: this._currentTransform, ...eventType === "down:before" || eventType === "down" ? extraData : {} }; if (eventType === "up:before" || eventType === "up") options.isClick = this._isClick; this.fire(`mouse:${eventType}`, options); target && target.fire(`mouse${eventType}`, options); for (let i = 0; i < subTargets.length; i++) subTargets[i] !== target && subTargets[i].fire(`mouse${eventType}`, options); } /** * @private * @param {Event} e Event object fired on mousedown */ _onMouseDownInDrawingMode(e) { this._isCurrentlyDrawing = true; if (this.getActiveObject()) { this.discardActiveObject(e); this.requestRenderAll(); } const pointer = this.getScenePoint(e); this.freeDrawingBrush && this.freeDrawingBrush.onMouseDown(pointer, { e, pointer }); this._handleEvent(e, "down", { alreadySelected: false }); } /** * @private * @param {Event} e Event object fired on mousemove */ _onMouseMoveInDrawingMode(e) { if (this._isCurrentlyDrawing) { const pointer = this.getScenePoint(e); this.freeDrawingBrush && this.freeDrawingBrush.onMouseMove(pointer, { e, pointer }); } this.setCursor(this.freeDrawingCursor); this._handleEvent(e, "move"); } /** * @private * @param {Event} e Event object fired on mouseup */ _onMouseUpInDrawingMode(e) { const pointer = this.getScenePoint(e); if (this.freeDrawingBrush) this._isCurrentlyDrawing = !!this.freeDrawingBrush.onMouseUp({ e, pointer }); else this._isCurrentlyDrawing = false; this._handleEvent(e, "up"); } /** * Method that defines the actions when mouse is clicked on canvas. * The method inits the currentTransform parameters and renders all the * canvas so the current image can be placed on the top canvas and the rest * in on the container one. * @private * @param {Event} e Event object fired on mousedown */ __onMouseDown(e) { this._isClick = true; this._handleEvent(e, "down:before"); let { target } = this.findTarget(e); let alreadySelected = !!target && target === this._activeObject; const { button } = e; if (button) { (this.fireMiddleClick && button === 1 || this.fireRightClick && button === 2) && this._handleEvent(e, "down", { alreadySelected }); return; } if (this.isDrawingMode) { this._onMouseDownInDrawingMode(e); return; } if (!this._isMainEvent(e)) return; if (this._currentTransform) return; let shouldRender = this._shouldRender(target); let grouped = false; if (this.handleMultiSelection(e, target)) { target = this._activeObject; grouped = true; shouldRender = true; } else if (this._shouldClearSelection(e, target)) this.discardActiveObject(e); if (this.selection && (!target || !target.selectable && !target.isEditing && target !== this._activeObject)) { const p = this.getScenePoint(e); this._groupSelector = { x: p.x, y: p.y, deltaY: 0, deltaX: 0 }; } alreadySelected = !!target && target === this._activeObject; if (target) { if (target.selectable && target.activeOn === "down") this.setActiveObject(target, e); const handle = target.findControl(this.getViewportPoint(e), isTouchEvent(e)); if (target === this._activeObject && (handle || !grouped)) { this._setupCurrentTransform(e, target, alreadySelected); const control = handle ? handle.control : void 0, pointer = this.getScenePoint(e), mouseDownHandler = control && control.getMouseDownHandler(e, target, control); mouseDownHandler && mouseDownHandler.call(control, e, this._currentTransform, pointer.x, pointer.y); } } shouldRender && (this._objectsToRender = void 0); this._handleEvent(e, "down", { alreadySelected }); shouldRender && this.requestRenderAll(); } /** * reset cache form common information needed during event processing * @private */ _resetTransformEventData() { this._targetInfo = this._viewportPoint = this._scenePoint = void 0; } /** * Cache common information needed during event processing * @private * @param {Event} e Event object fired on event */ _cacheTransformEventData(e) { this._resetTransformEventData(); this._viewportPoint = this.getViewportPoint(e); this._scenePoint = sendPointToPlane(this._viewportPoint, void 0, this.viewportTransform); this._targetInfo = this.findTarget(e); if (this._currentTransform) this._targetInfo.target = this._currentTransform.target; } /** * Method that defines the actions when mouse is hovering the canvas. * The currentTransform parameter will define whether the user is rotating/scaling/translating * an image or neither of them (only hovering). A group selection is also possible and would cancel * all any other type of action. * In case of an image transformation only the top canvas will be rendered. * @private * @param {Event} e Event object fired on mousemove */ __onMouseMove(e) { this._isClick = false; this._handleEvent(e, "move:before"); if (this.isDrawingMode) { this._onMouseMoveInDrawingMode(e); return; } if (!this._isMainEvent(e)) return; const groupSelector = this._groupSelector; if (groupSelector) { const pointer = this.getScenePoint(e); groupSelector.deltaX = pointer.x - groupSelector.x; groupSelector.deltaY = pointer.y - groupSelector.y; this.renderTop(); } else if (!this._currentTransform) { const { target } = this.findTarget(e); this._setCursorFromEvent(e, target); this._fireOverOutEvents(e, target); } else this._transformObject(e); this.textEditingManager.onMouseMove(e); this._handleEvent(e, "move"); } /** * Manage the mouseout, mouseover events for the fabric object on the canvas * @param {Fabric.Object} target the target where the target from the mousemove event * @param {Event} e Event object fired on mousemove * @private */ _fireOverOutEvents(e, target) { const { _hoveredTarget, _hoveredTargets } = this, { subTargets, currentTarget: actualTarget } = this.findTarget(e), length = Math.max(_hoveredTargets.length, subTargets.length); this.fireSyntheticInOutEvents("mouse", { e, target, oldTarget: _hoveredTarget, actualTarget, oldActualTarget: this._hoveredActualTarget, fireCanvas: true }); for (let i = 0; i < length; i++) { if (subTargets[i] === target || _hoveredTargets[i] && _hoveredTargets[i] === _hoveredTarget) continue; this.fireSyntheticInOutEvents("mouse", { e, target: subTargets[i], oldTarget: _hoveredTargets[i] }); } this._hoveredActualTarget = actualTarget; this._hoveredTarget = target; this._hoveredTargets = subTargets; } /** * Manage the dragEnter, dragLeave events for the fabric objects on the canvas * @param {Fabric.Object} target the target where the target from the onDrag event * @param {Object} data Event object fired on dragover * @private */ _fireEnterLeaveEvents(e, target, data) { const draggedoverTarget = this._draggedoverTarget, _hoveredTargets = this._hoveredTargets, { subTargets } = this.findTarget(e), length = Math.max(_hoveredTargets.length, subTargets.length); this.fireSyntheticInOutEvents("drag", { ...data, target, oldTarget: draggedoverTarget, fireCanvas: true }); for (let i = 0; i < length; i++) this.fireSyntheticInOutEvents("drag", { ...data, target: subTargets[i], oldTarget: _hoveredTargets[i] }); this._draggedoverTarget = target; } /** * Manage the synthetic in/out events for the fabric objects on the canvas * @param {Fabric.Object} target the target where the target from the supported events * @param {Object} data Event object fired * @param {Object} config configuration for the function to work * @param {String} config.targetName property on the canvas where the old target is stored * @param {String} [config.canvasEvtOut] name of the event to fire at canvas level for out * @param {String} config.evtOut name of the event to fire for out * @param {String} [config.canvasEvtIn] name of the event to fire at canvas level for in * @param {String} config.evtIn name of the event to fire for in * @private */ fireSyntheticInOutEvents(type, { target, oldTarget, actualTarget, oldActualTarget, fireCanvas, e, ...data }) { const { targetIn, targetOut, canvasIn, canvasOut } = syntheticEventConfig[type]; const targetChanged = oldTarget !== target; const actualTargetChanged = oldActualTarget !== actualTarget; const targetFires = target && targetChanged; const actualTargetFires = actualTarget && actualTargetChanged; const oldTargetFires = oldTarget && targetChanged; const oldActualTargetFires = oldActualTarget && actualTargetChanged; const commonData = { ...data, e, ...getEventPoints(this, e) }; const outOpt = { ...commonData, target: oldTarget, nextTarget: target, actualTarget: oldActualTarget, nextActualTarget: actualTarget }; if (oldTargetFires || oldActualTargetFires) fireCanvas && this.fire(canvasOut, outOpt); oldTargetFires && oldTarget.fire(targetOut, outOpt); oldActualTargetFires && oldTarget !== oldActualTarget && oldActualTarget.fire(targetOut, outOpt); const inOpt = { ...commonData, target, previousTarget: oldTarget, actualTarget, previousActualTarget: oldActualTarget }; if (targetFires || actualTargetFires) fireCanvas && this.fire(canvasIn, inOpt); targetFires && target.fire(targetIn, inOpt); actualTargetFires && actualTarget !== target && actualTarget.fire(targetIn, inOpt); } /** * @private * @param {Event} e Event fired on mousemove */ _transformObject(e) { const scenePoint = this.getScenePoint(e), transform = this._currentTransform, target = transform.target, localPointer = target.group ? sendPointToPlane(scenePoint, void 0, target.group.calcTransformMatrix()) : scenePoint; transform.shiftKey = e.shiftKey; transform.altKey = !!this.centeredKey && e[this.centeredKey]; this._performTransformAction(e, transform, localPointer); transform.actionPerformed && this.requestRenderAll(); } /** * @private */ _performTransformAction(e, transform, pointer) { const { action, actionHandler, target } = transform; const actionPerformed = !!actionHandler && actionHandler(e, transform, pointer.x, pointer.y); actionPerformed && target.setCoords(); if (action === "drag" && actionPerformed) { transform.target.isMoving = true; this.setCursor(transform.target.moveCursor || this.moveCursor); } transform.actionPerformed = transform.actionPerformed || actionPerformed; } /** * Sets the cursor depending on where the canvas is being hovered. * Note: very buggy in Opera * @param {Event} e Event object * @param {Object} target Object that the mouse is hovering, if so. */ _setCursorFromEvent(e, target) { if (!target) { this.setCursor(this.defaultCursor); return; } let hoverCursor = target.hoverCursor || this.hoverCursor; const activeSelection = isActiveSelection(this._activeObject) ? this._activeObject : null, corner = (!activeSelection || target.group !== activeSelection) && target.findControl(this.getViewportPoint(e)); if (!corner) { if (target.subTargetCheck) { const { subTargets } = this.findTarget(e); subTargets.concat().reverse().forEach((_target) => { hoverCursor = _target.hoverCursor || hoverCursor; }); } this.setCursor(hoverCursor); } else { const { control, coord } = corner; this.setCursor(control.cursorStyleHandler(e, control, target, coord)); } } /** * ## Handles multiple selection * - toggles `target` selection (selects/deselects `target` if it isn't/is selected respectively) * - sets the active object in case it is not set or in case there is a single active object left under active selection. * --- * - If the active object is the active selection we add/remove `target` from it * - If not, add the active object and `target` to the active selection and make it the active object. * @TODO rewrite this after find target is refactored * @private * @param {TPointerEvent} e Event object * @param {FabricObject} target target of event to select/deselect * @returns true if grouping occurred */ handleMultiSelection(e, target) { const activeObject = this._activeObject; const isAS = isActiveSelection(activeObject); if (!!activeObject && this._isSelectionKeyPressed(e) && this.selection && !!target && target.selectable && (activeObject !== target || isAS) && (isAS || !target.isDescendantOf(activeObject) && !activeObject.isDescendantOf(target)) && !target.onSelect({ e }) && !activeObject.getActiveControl()) { if (isAS) { const prevActiveObjects = activeObject.getObjects(); let subTargets = []; if (target === activeObject) { const pointer = this.getScenePoint(e); let targetInfo = this.searchPossibleTargets(prevActiveObjects, pointer); if (targetInfo.target) { target = targetInfo.target; subTargets = targetInfo.subTargets; } else { targetInfo = this.searchPossibleTargets(this._objects, pointer); target = targetInfo.target; subTargets = targetInfo.subTargets; } if (!target || !target.selectable) return false; } if (target.group === activeObject) { activeObject.remove(target); this._hoveredTarget = target; this._hoveredTargets = subTargets; if (activeObject.size() === 1) this._setActiveObject(activeObject.item(0), e); } else { activeObject.multiSelectAdd(target); this._hoveredTarget = activeObject; this._hoveredTargets = subTargets; } this._fireSelectionEvents(prevActiveObjects, e); } else { activeObject.isEditing && activeObject.exitEditing(); const newActiveSelection = new (classRegistry.getClass("ActiveSelection"))([], { canvas: this }); newActiveSelection.multiSelectAdd(activeObject, target); this._hoveredTarget = newActiveSelection; this._setActiveObject(newActiveSelection, e); this._fireSelectionEvents([activeObject], e); } return true; } return false; } /** * ## Handles selection * - selects objects that are contained in (and possibly intersecting) the selection bounding box * - sets the active object * --- * runs on mouse up after a mouse move */ handleSelection(e) { if (!this.selection || !this._groupSelector) return false; const { x, y, deltaX, deltaY } = this._groupSelector, point1 = new Point(x, y), point2 = point1.add(new Point(deltaX, deltaY)), tl = point1.min(point2), size = point1.max(point2).subtract(tl); const collectedObjects = this.collectObjects({ left: tl.x, top: tl.y, width: size.x, height: size.y }, { includeIntersecting: !this.selectionFullyContained }); const objects = point1.eq(point2) ? collectedObjects[0] ? [collectedObjects[0]] : [] : collectedObjects.length > 1 ? collectedObjects.filter((object) => !object.onSelect({ e })).reverse() : collectedObjects; if (objects.length === 1) this.setActiveObject(objects[0], e); else if (objects.length > 1) { const klass = classRegistry.getClass("ActiveSelection"); this.setActiveObject(new klass(objects, { canvas: this }), e); } this._groupSelector = null; return true; } /** * Wraps the original toCanvasElement with a function that removes * the context top for the time the function is run. * So we avoid painting side effects on the upper canvas when exporting */ toCanvasElement(multiplier = 1, options) { const { upper } = this.elements; upper.ctx = void 0; const htmlElement = super.toCanvasElement(multiplier, options); upper.ctx = upper.el.getContext("2d"); return htmlElement; } /** * @override clear {@link textEditingManager} */ clear() { this.textEditingManager.clear(); super.clear(); } /** * @override clear {@link textEditingManager} */ destroy() { this.removeListeners(); this.textEditingManager.dispose(); super.destroy(); } }; //#endregion export { Canvas }; //# sourceMappingURL=Canvas.mjs.map