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
JavaScript
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