diff --git a/CHANGELOG.md b/CHANGELOG.md index 596c8788185..1be57f76735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## [next] +- chore(TS): Convert Canvas events mixin and grouping mixin [#8519](https://github.com/fabricjs/fabric.js/pull/8519) - chore(TS): Remove backward compatibility initialize methods [#8525](https://github.com/fabricjs/fabric.js/pull/8525/) - chore(TS): replace getKlass utility with a registry that doesn't require full fabricJS to work [#8500](https://github.com/fabricjs/fabric.js/pull/8500) - chore(): use context in static constructors [#8522](https://github.com/fabricjs/fabric.js/issues/8522) diff --git a/index.js b/index.js index a607baaddca..6ed54ee8982 100644 --- a/index.js +++ b/index.js @@ -9,13 +9,11 @@ import './src/color'; import './src/gradient'; // optional gradient import './src/pattern.class'; // optional pattern import './src/shadow.class'; // optional shadow -import './src/static_canvas.class'; -import './src/canvas.class'; // optional interaction -import './src/mixins/canvas_events.mixin'; // optional interaction -import './src/mixins/canvas_grouping.mixin'; // optional interaction +import './src/canvas/static_canvas.class'; +import './src/canvas/canvas_events'; // optional interaction import './src/mixins/canvas_dataurl_exporter.mixin'; import './src/mixins/canvas_serialization.mixin'; // optional serialization -import './src/mixins/canvas_gestures.mixin'; // optional gestures +import './src/canvas/canvas_gestures.mixin'; // optional gestures import './src/mixins/canvas_animation.mixin'; // optional animation import './src/mixins/canvas_straightening.mixin'; // optional animation import './src/shapes/Object/FabricObject'; diff --git a/src/EventTypeDefs.ts b/src/EventTypeDefs.ts index 09d4f9ac05d..32f79deba2b 100644 --- a/src/EventTypeDefs.ts +++ b/src/EventTypeDefs.ts @@ -4,9 +4,9 @@ import type { FabricObject } from './shapes/Object/FabricObject'; import type { Group } from './shapes/group.class'; import type { TOriginX, TOriginY, TRadian } from './typedefs'; import type { saveObjectTransform } from './util/misc/objectTransforms'; -import type { Canvas } from './__types__'; +import type { Canvas } from './canvas/canvas_events'; import type { IText } from './shapes/itext.class'; -import type { StaticCanvas } from './static_canvas.class'; +import type { StaticCanvas } from './canvas/static_canvas.class'; export type ModifierKey = keyof Pick< MouseEvent | PointerEvent | TouchEvent, @@ -24,9 +24,19 @@ export type TransformAction = ( y: number ) => R; +/** + * Control handlers that define a transformation + * Those handlers run when the user starts a transform and during a transform + */ export type TransformActionHandler = TransformAction; +/** + * Control handlers that run on control click/down/up + * Those handlers run with or without a transform defined + */ +export type ControlActionHandler = TransformAction; + export type ControlCallback = ( eventData: TPointerEvent, control: Control, @@ -42,7 +52,7 @@ export type ControlCursorCallback = ControlCallback; export type Transform = { target: FabricObject; action: string; - actionHandler: TransformActionHandler; + actionHandler?: TransformActionHandler; corner: string | 0; scaleX: number; scaleY: number; @@ -65,6 +75,7 @@ export type Transform = { originX: TOriginX; originY: TOriginY; }; + actionPerformed: boolean; }; export type TEvent = { @@ -87,23 +98,40 @@ export type TModificationEvents = | 'skewing' | 'resizing'; -type ObjectModifiedEvents = Record & { - modified: BasicTransformEvent | never; +export type ModifiedEvent = TEvent & { + transform: Transform; + target: FabricObject; + action: string; }; -type CanvasModifiedEvents = Record< - `object:${keyof ObjectModifiedEvents}`, - BasicTransformEvent & { target: FabricObject } ->; +type ModificationEventsSpec< + Prefix extends string = '', + Modification = BasicTransformEvent, + Modified = ModifiedEvent | never +> = Record<`${Prefix}${TModificationEvents}`, Modification> & + Record<`${Prefix}modified`, Modified>; + +type ObjectModificationEvents = ModificationEventsSpec; + +type CanvasModificationEvents = ModificationEventsSpec< + 'object:', + BasicTransformEvent & { target: FabricObject }, + ModifiedEvent | { target: FabricObject } +> & { + 'before:transform': TEvent & { transform: Transform }; +}; -export type TransformEvent = - BasicTransformEvent & { - target: FabricObject; - subTargets: FabricObject[]; - button: number; +export type TPointerEventInfo = + TEvent & { + target?: FabricObject; + subTargets?: FabricObject[]; + button?: number; isClick: boolean; pointer: Point; + transform?: Transform | null; absolutePointer: Point; + currentSubTargets?: FabricObject[]; + currentTarget?: FabricObject | null; }; type SimpleEventHandler = @@ -119,10 +147,12 @@ type OutEvent = { nextTarget?: FabricObject; }; -type DragEventData = TEventWithTarget & { +export type DragEventData = TEvent & { + target?: FabricObject; subTargets?: FabricObject[]; dragSource?: FabricObject; canDrop?: boolean; + didDrop?: boolean; dropTarget?: FabricObject; }; @@ -146,10 +176,10 @@ type CanvasDnDEvents = DnDEvents & { }; type CanvasSelectionEvents = { - 'selection:created': TEvent & { + 'selection:created': Partial & { selected: FabricObject[]; }; - 'selection:updated': TEvent & { + 'selection:updated': Partial & { selected: FabricObject[]; deselected: FabricObject[]; }; @@ -169,24 +199,37 @@ export type CollectionEvents = { type BeforeSuffix = `${T}:before`; type WithBeforeSuffix = T | BeforeSuffix; -type TPointerEvents> = Record< +type TPointerEvents = Record< `${Prefix}${ | WithBeforeSuffix<'down'> | WithBeforeSuffix<'move'> | WithBeforeSuffix<'up'> | 'dblclick'}`, - TransformEvent & E + TPointerEventInfo > & - Record<`${Prefix}wheel`, TransformEvent & E> & - Record<`${Prefix}over`, TransformEvent & InEvent & E> & - Record<`${Prefix}out`, TransformEvent & OutEvent & E>; + Record<`${Prefix}wheel`, TPointerEventInfo> & + Record<`${Prefix}over`, TPointerEventInfo & InEvent> & + Record<`${Prefix}out`, TPointerEventInfo & OutEvent>; + +export type TPointerEventNames = + | WithBeforeSuffix<'down'> + | WithBeforeSuffix<'move'> + | WithBeforeSuffix<'up'> + | 'dblclick' + | 'wheel'; export type ObjectPointerEvents = TPointerEvents<'mouse'>; export type CanvasPointerEvents = TPointerEvents<'mouse:'>; +export type MiscEvents = { + 'contextmenu:before': SimpleEventHandler; + contextmenu: SimpleEventHandler; +}; + export type ObjectEvents = ObjectPointerEvents & DnDEvents & - ObjectModifiedEvents & { + MiscEvents & + ObjectModificationEvents & { // selection selected: Partial & { target: FabricObject; @@ -215,7 +258,8 @@ export type StaticCanvasEvents = CollectionEvents & { export type CanvasEvents = StaticCanvasEvents & CanvasPointerEvents & CanvasDnDEvents & - CanvasModifiedEvents & + MiscEvents & + CanvasModificationEvents & CanvasSelectionEvents & { // brushes 'before:path:created': { path: FabricObject }; @@ -240,8 +284,4 @@ export type CanvasEvents = StaticCanvasEvents & 'text:changed': { target: IText }; 'text:editing:entered': { target: IText }; 'text:editing:exited': { target: IText }; - - // misc - 'contextmenu:before': SimpleEventHandler; - contextmenu: SimpleEventHandler; }; diff --git a/src/__types__.ts b/src/__types__.ts deleted file mode 100644 index 3c825f356ce..00000000000 --- a/src/__types__.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { CanvasEvents, ModifierKey, StaticCanvasEvents } from './EventTypeDefs'; -import type { Observable } from './mixins/observable.mixin'; -import type { Point } from './point.class'; -import type { FabricObject } from './shapes/Object/FabricObject'; -import { TMat2D } from './typedefs'; - -/** - * @todo remove transient - */ -export type Canvas = StaticCanvas & { - altActionKey: ModifierKey; - uniScaleKey: ModifierKey; - uniformScaling: boolean; -} & Record & - Observable; -export type StaticCanvas = Record & { - getZoom(): number; - viewportTransform: TMat2D; - vptCoords: { - tl: Point; - br: Point; - }; - getRetinaScaling(): number; - _objects: FabricObject[]; -} & Observable; diff --git a/src/brushes/base_brush.class.ts b/src/brushes/base_brush.class.ts index a7c224e1583..5982581188d 100644 --- a/src/brushes/base_brush.class.ts +++ b/src/brushes/base_brush.class.ts @@ -3,7 +3,7 @@ import { Color } from '../color'; import type { Point } from '../point.class'; import { TEvent } from '../EventTypeDefs'; import type { Shadow } from '../shadow.class'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; type TBrushEventData = TEvent & { pointer: Point }; diff --git a/src/brushes/circle_brush.class.ts b/src/brushes/circle_brush.class.ts index 1dbd8b01172..0738962b4b9 100644 --- a/src/brushes/circle_brush.class.ts +++ b/src/brushes/circle_brush.class.ts @@ -5,7 +5,7 @@ import { Shadow } from '../shadow.class'; import { Circle } from '../shapes/circle.class'; import { Group } from '../shapes/group.class'; import { getRandomInt } from '../util/internals'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; import { BaseBrush } from './base_brush.class'; export type CircleBrushPoint = { diff --git a/src/brushes/pattern_brush.class.ts b/src/brushes/pattern_brush.class.ts index 195abc4e819..a52e0f24fce 100644 --- a/src/brushes/pattern_brush.class.ts +++ b/src/brushes/pattern_brush.class.ts @@ -2,7 +2,7 @@ import { fabric } from '../../HEADER'; import { Pattern } from '../pattern.class'; import { PathData } from '../typedefs'; import { createCanvasElement } from '../util/misc/dom'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; import { PencilBrush } from './pencil_brush.class'; export class PatternBrush extends PencilBrush { diff --git a/src/brushes/pencil_brush.class.ts b/src/brushes/pencil_brush.class.ts index 8cac3585d3c..ba9c1c64227 100644 --- a/src/brushes/pencil_brush.class.ts +++ b/src/brushes/pencil_brush.class.ts @@ -5,7 +5,7 @@ import { Shadow } from '../shadow.class'; import { Path } from '../shapes/path.class'; import { PathData } from '../typedefs'; import { getSmoothPathFromPoints, joinPath } from '../util/path'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; import { BaseBrush } from './base_brush.class'; /** diff --git a/src/brushes/spray_brush.class.ts b/src/brushes/spray_brush.class.ts index 725d2af8aff..870b11e338e 100644 --- a/src/brushes/spray_brush.class.ts +++ b/src/brushes/spray_brush.class.ts @@ -4,7 +4,7 @@ import { Group } from '../shapes/group.class'; import { Shadow } from '../shadow.class'; import { Rect } from '../shapes/rect.class'; import { getRandomInt } from '../util/internals'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; import { BaseBrush } from './base_brush.class'; export type SprayBrushPoint = { diff --git a/src/canvas.class.ts b/src/canvas/canvas.class.ts similarity index 93% rename from src/canvas.class.ts rename to src/canvas/canvas.class.ts index bc6ae564794..c853e96971d 100644 --- a/src/canvas.class.ts +++ b/src/canvas/canvas.class.ts @@ -1,44 +1,45 @@ -import { fabric } from '../HEADER'; -import { dragHandler, getActionFromCorner } from './controls/actions'; -import { Point } from './point.class'; -import { FabricObject } from './shapes/Object/FabricObject'; +import { fabric } from '../../HEADER'; +import { dragHandler, getActionFromCorner } from '../controls/actions'; +import { Point } from '../point.class'; +import { FabricObject } from '../shapes/Object/FabricObject'; import { CanvasEvents, ModifierKey, TOptionalModifierKey, TPointerEvent, Transform, -} from './EventTypeDefs'; +} from '../EventTypeDefs'; import { addTransformToObject, saveObjectTransform, -} from './util/misc/objectTransforms'; +} from '../util/misc/objectTransforms'; import { StaticCanvas, TCanvasSizeOptions } from './static_canvas.class'; import { isActiveSelection, isCollection, isFabricObjectCached, isInteractiveTextObject, -} from './util/types'; -import { invertTransform, transformPoint } from './util/misc/matrix'; -import { isTransparent } from './util/misc/isTransparent'; -import { TMat2D, TOriginX, TOriginY, TSize } from './typedefs'; -import { degreesToRadians } from './util/misc/radiansDegreesConversion'; -import { getPointer, isTouchEvent } from './util/dom_event'; -import type { IText } from './shapes/itext.class'; +} from '../util/types'; +import { invertTransform, transformPoint } from '../util/misc/matrix'; +import { isTransparent } from '../util/misc/isTransparent'; +import { TMat2D, TOriginX, TOriginY, TSize } from '../typedefs'; +import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; +import { getPointer, isTouchEvent } from '../util/dom_event'; +import type { IText } from '../shapes/itext.class'; import { cleanUpJsdomNode, makeElementUnselectable, wrapElement, -} from './util/dom_misc'; -import { setStyle } from './util/dom_style'; -import type { BaseBrush } from './brushes/base_brush.class'; -import type { Textbox } from './shapes/textbox.class'; -import { pick } from './util/misc/pick'; -import { TSVGReviver } from './mixins/object.svg_export'; +} from '../util/dom_misc'; +import { setStyle } from '../util/dom_style'; +import type { BaseBrush } from '../brushes/base_brush.class'; +import type { Textbox } from '../shapes/textbox.class'; +import { pick } from '../util/misc/pick'; +import { TSVGReviver } from '../mixins/object.svg_export'; +import { sendPointToPlane } from '../util/misc/planeChange'; type TDestroyedCanvas = Omit< - Canvas, + SelectableCanvas, | 'contextTop' | 'contextCache' | 'lowerCanvasEl' @@ -148,7 +149,7 @@ type TDestroyedCanvas = Omit< * }); * */ -export class Canvas< +export class SelectableCanvas< EventSpec extends CanvasEvents = CanvasEvents > extends StaticCanvas { /** @@ -291,31 +292,31 @@ export class Canvas< /** * Default cursor value used when hovering over an object on canvas - * @type String - * @default + * @type CSSStyleDeclaration['cursor'] + * @default move */ - hoverCursor: string; + hoverCursor: CSSStyleDeclaration['cursor']; /** * Default cursor value used when moving an object on canvas - * @type String - * @default + * @type CSSStyleDeclaration['cursor'] + * @default move */ - moveCursor: string; + moveCursor: CSSStyleDeclaration['cursor']; /** * Default cursor value used for the entire canvas * @type String - * @default + * @default default */ - defaultCursor: string; + defaultCursor: CSSStyleDeclaration['cursor']; /** * Cursor value used during free drawing * @type String * @default crosshair */ - freeDrawingCursor: string; + freeDrawingCursor: CSSStyleDeclaration['cursor']; /** * Cursor value used for disabled elements ( corners with disabled action ) @@ -323,7 +324,7 @@ export class Canvas< * @since 2.0.0 * @default not-allowed */ - notAllowedCursor: string; + notAllowedCursor: CSSStyleDeclaration['cursor']; /** * Default element class that's given to wrapper (div) element of canvas @@ -405,19 +406,12 @@ export class Canvas< */ targets: FabricObject[] = []; - /** - * When the option is enabled, PointerEvent is used instead of TPointerEvent. - * @type Boolean - * @default - */ - enablePointerEvents: boolean; - /** * Keep track of the hovered target * @type FabricObject | null * @private */ - _hoveredTarget: FabricObject | null = null; + _hoveredTarget?: FabricObject; /** * hold the list of nested targets hovered @@ -469,19 +463,29 @@ export class Canvas< /** * During a mouse event we may need the pointer multiple times in multiple functions. - * _absolutePointer holds a reference to the pointer in coordinates that is valide for the event + * _absolutePointer holds a reference to the pointer in fabricCanvas/design coordinates that is valid for the event * lifespan. Every fabricJS mouse event create and delete the cache every time * We do this because there are some HTML DOM inspection functions to get the actual pointer coordinates + * @type {Point} */ - _absolutePointer?: Point; + protected _absolutePointer?: Point; /** * During a mouse event we may need the pointer multiple times in multiple functions. - * _pointer holds a reference to the pointer in coordinates that is valide for the event + * _pointer holds a reference to the pointer in html coordinates that is valid for the event * lifespan. Every fabricJS mouse event create and delete the cache every time * We do this because there are some HTML DOM inspection functions to get the actual pointer coordinates + * @type {Point} */ - _pointer?: Point; + protected _pointer?: Point; + + /** + * During a mouse event we may need the target multiple times in multiple functions. + * _target holds a reference to the target that is valid for the event + * lifespan. Every fabricJS mouse event create and delete the cache every time + * @type {FabricObject} + */ + protected _target?: FabricObject; upperCanvasEl: HTMLCanvasElement; contextTop: CanvasRenderingContext2D; @@ -489,7 +493,7 @@ export class Canvas< cacheCanvasEl: HTMLCanvasElement; protected _isCurrentlyDrawing: boolean; freeDrawingBrush?: BaseBrush; - _activeObject: FabricObject | null; + _activeObject?: FabricObject; _hasITextHandlers?: boolean; _iTextInstances: (IText | Textbox)[]; /** @@ -549,7 +553,7 @@ export class Canvas< }); } if (obj === this._hoveredTarget) { - this._hoveredTarget = null; + this._hoveredTarget = undefined; this._hoveredTargets = []; } super._onObjectRemoved(obj); @@ -729,7 +733,7 @@ export class Canvas< * @param {TPointerEvent} e Event object * @param {FabricObject} target */ - _shouldClearSelection(e: TPointerEvent, target: FabricObject): boolean { + _shouldClearSelection(e: TPointerEvent, target?: FabricObject): boolean { const activeObjects = this.getActiveObjects(), activeObject = this._activeObject; @@ -747,20 +751,19 @@ export class Canvas< } /** - * This is an internal method to decide if given the action and the modifier key pressed - * the transformation should with the object center as origin - * centeredScaling from object can't override centeredScaling from canvas. - * this should be fixed, since object setting should take precedence over canvas. - * also this should be something that will be migrated in the control properties. - * as ability to define the origin of the transformation that the control provide. + * This method will take in consideration a modifier key pressed and the control we are + * about to drag, and try to guess the anchor point ( origin ) of the transormation. + * This should be really in the realm of controls, and we should remove specific code for legacy + * embedded actions. * @TODO this probably deserve discussion/rediscovery and change/refactor * @private + * @deprecated * @param {FabricObject} target * @param {string} action * @param {boolean} altKey * @returns {boolean} true if the transformation should be centered */ - _shouldCenterTransform( + private _shouldCenterTransform( target: FabricObject, action: string, modifierKeyPressed: boolean @@ -833,10 +836,7 @@ export class Canvas< let pointer = this.getPointer(e); if (target.group) { // transform pointer to target's containing coordinate plane - // should we use send point to plane? - pointer = pointer.transform( - invertTransform(target.group.calcTransformMatrix()) - ); + pointer = sendPointToPlane(pointer, target.group.calcTransformMatrix()); } const corner = target.__corner || '', control = target.controls[corner], @@ -854,7 +854,8 @@ export class Canvas< transform: Transform = { target: target, action: action, - actionHandler: actionHandler, + actionHandler, + actionPerformed: false, corner, scaleX: target.scaleX, scaleY: target.scaleY, @@ -946,9 +947,9 @@ export class Canvas< * @param {Boolean} skipGroup when true, activeGroup is skipped and only objects are traversed through * @return {FabricObject | null} the target found */ - findTarget(e: TPointerEvent, skipGroup: boolean): FabricObject | null { + findTarget(e: TPointerEvent, skipGroup = false): FabricObject | undefined { if (this.skipTargetFind) { - return null; + return undefined; } const pointer = this.getPointer(e, true), @@ -1055,9 +1056,9 @@ export class Canvas< _searchPossibleTargets( objects: FabricObject[], pointer: Point - ): FabricObject | null { + ): FabricObject | undefined { // Cache all targets where their bounding box contains point. - let target = null, + let target, i = objects.length; // Do not check for currently grouped objects, since we check the parent group itself. // until we call this function specifically to search inside the activeGroup @@ -1088,7 +1089,10 @@ export class Canvas< * @param {Object} [pointer] x,y object of point coordinates we want to check. * @return {FabricObject} **top most object on screen** that contains pointer */ - searchPossibleTargets(objects: FabricObject[], pointer: Point) { + searchPossibleTargets( + objects: FabricObject[], + pointer: Point + ): FabricObject | undefined { const target = this._searchPossibleTargets(objects, pointer); // if we found something in this.targets, and the group is interactive, return that subTarget // TODO: reverify why interactive. the target should be returned always, but selected only @@ -1273,8 +1277,8 @@ export class Canvas< this.wrapperEl = wrapElement(this.lowerCanvasEl, container); this.wrapperEl.setAttribute('data-fabric', 'wrapper'); setStyle(this.wrapperEl, { - width: this.width + 'px', - height: this.height + 'px', + width: `${this.width}px`, + height: `${this.height}px`, position: 'relative', }); makeElementUnselectable(this.wrapperEl); @@ -1331,7 +1335,7 @@ export class Canvas< * Returns currently active object * @return {FabricObject | null} active object */ - getActiveObject(): FabricObject | null { + getActiveObject(): FabricObject | undefined { return this._activeObject; } @@ -1343,7 +1347,7 @@ export class Canvas< const active = this._activeObject; if (active) { if (isActiveSelection(active)) { - return active._objects.slice(0); + return [...active._objects]; } else { return [active]; } @@ -1461,14 +1465,14 @@ export class Canvas< const obj = this._activeObject; if (obj) { // onDeselect return TRUE to cancel selection; - if (obj.onDeselect({ e: e, object })) { + if (obj.onDeselect({ e, object })) { return false; } if (this._currentTransform && this._currentTransform.target === obj) { // @ts-ignore this.endCurrentTransform(e); } - this._activeObject = null; + this._activeObject = undefined; } return true; } @@ -1487,7 +1491,7 @@ export class Canvas< if (currentActives.length) { this.fire('before:selection:cleared', { e, - deselected: [activeObject], + deselected: [activeObject!], }); } this._discardActiveObject(e); @@ -1508,9 +1512,9 @@ export class Canvas< */ destroy(this: TDestroyedCanvas) { const wrapperEl = this.wrapperEl as HTMLDivElement, - lowerCanvasEl = this.lowerCanvasEl as HTMLCanvasElement, - upperCanvasEl = this.upperCanvasEl as HTMLCanvasElement, - cacheCanvasEl = this.cacheCanvasEl as HTMLCanvasElement; + lowerCanvasEl = this.lowerCanvasEl!, + upperCanvasEl = this.upperCanvasEl!, + cacheCanvasEl = this.cacheCanvasEl!; // @ts-ignore this.removeListeners(); super.destroy(); @@ -1616,8 +1620,8 @@ export class Canvas< instance: FabricObject, reviver: TSVGReviver ) { - //If the object is in a selection group, simulate what would happen to that - //object when the group is deselected + // If the object is in a selection group, simulate what would happen to that + // object when the group is deselected const originalProperties = this._realizeGroupTransformOnObject(instance); super._setSVGObject(markup, instance, reviver); instance.set(originalProperties); @@ -1635,7 +1639,7 @@ export class Canvas< } } -Object.assign(Canvas.prototype, { +Object.assign(SelectableCanvas.prototype, { uniformScaling: true, uniScaleKey: 'shiftKey', centeredScaling: false, @@ -1665,5 +1669,3 @@ Object.assign(Canvas.prototype, { fireMiddleClick: false, enablePointerEvents: false, }); - -fabric.Canvas = Canvas; diff --git a/src/canvas/canvas_events.ts b/src/canvas/canvas_events.ts new file mode 100644 index 00000000000..c593918a01c --- /dev/null +++ b/src/canvas/canvas_events.ts @@ -0,0 +1,1650 @@ +import { fabric } from '../../HEADER'; +import { + CanvasEvents, + DragEventData, + ObjectEvents, + TEvent, + TPointerEvent, + TPointerEventInfo, + TPointerEventNames, + Transform, +} from '../EventTypeDefs'; +import { Point } from '../point.class'; +import { ActiveSelection } from '../shapes/active_selection.class'; +import { Group } from '../shapes/group.class'; +import type { FabricObject } from '../shapes/Object/FabricObject'; +import { stopEvent } from '../util/dom_event'; +import { sendPointToPlane } from '../util/misc/planeChange'; +import { + isActiveSelection, + isFabricObjectWithDragSupport, + isInteractiveTextObject, +} from '../util/types'; +import { SelectableCanvas } from './canvas.class'; + +const RIGHT_CLICK = 3, + MIDDLE_CLICK = 2, + LEFT_CLICK = 1, + addEventOptions = { passive: false } as EventListenerOptions; + +function checkClick(e: TPointerEvent, value: number) { + return !!(e as MouseEvent).button && (e as MouseEvent).button === value - 1; +} + +// just to be clear, the utils are now deprecated and those are here exactly as minifier helpers +// because el.addEventListener can't me be minified while a const yes and we use it 47 times in this file. +// few bytes but why give it away. +const addListener = ( + el: HTMLElement, + ...args: Parameters +) => el.addEventListener(...args); +const removeListener = ( + el: HTMLElement, + ...args: Parameters +) => 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', + }, +} as const; + +type TSyntheticEventContext = { + mouse: { e: TPointerEvent }; + drag: DragEventData; +}; + +export class Canvas extends SelectableCanvas { + /** + * Contains the id of the touch event that owns the fabric transform + * @type Number + * @private + */ + mainTouchId: null | number; + + /** + * When the option is enabled, PointerEvent is used instead of TPointerEvent. + * @type Boolean + * @default + */ + enablePointerEvents: boolean; + + /** + * an internal flag that is used to remember if we already bound the events + * @type Boolean + * @private + */ + private eventsBound: boolean; + + /** + * Holds a reference to a setTimeout timer for event synchronization + * @type number + * @private + */ + private _willAddMouseDown: number; + + /** + * Holds a reference to an object on the canvas that is receiving the drag over event. + * @type FabricObject + * @private + */ + private _draggedoverTarget?: FabricObject; + + /** + * Holds a reference to an object on the canvas from where the drag operation started + * @type FabricObject + * @private + */ + private _dragSource?: FabricObject; + + currentTarget?: FabricObject; + + currentSubTargets?: FabricObject[]; + + /** + * Holds a reference to a pointer during mousedown to compare on mouse up and determine + * if it was a click event + * @type FabricObject + * @private + */ + _previousPointer: Point; + + /** + * Adds mouse listeners to canvas + * @private + */ + private _initEventListeners() { + // in case we initialized the class twice. This should not happen normally + // but in some kind of applications where the canvas element may be changed + // this is a workaround to having double listeners. + this.removeListeners(); + this._bindEvents(); + // @ts-ginore + this.addOrRemove(addListener, 'add'); + } + + /** + * return an event prefix pointer or mouse. + * @private + */ + private _getEventPrefix() { + return this.enablePointerEvents ? 'pointer' : 'mouse'; + } + + addOrRemove(functor: any, eventjsFunctor: 'add' | 'remove') { + const canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + functor(fabric.window, '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); + functor(canvasElement, 'contextmenu', this._onContextMenu); + functor(canvasElement, 'dblclick', this._onDoubleClick); + 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); + } + // if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { + // eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); + // eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); + // eventjs[eventjsFunctor]( + // canvasElement, + // 'orientation', + // this._onOrientationChange + // ); + // eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); + // eventjs[eventjsFunctor](canvasElement, 'longpress', this._onLongPress); + // } + } + + /** + * Removes all event listeners + */ + removeListeners() { + this.addOrRemove(removeListener, 'remove'); + // if you dispose on a mouseDown, before mouse up, you need to clean document to... + const eventTypePrefix = this._getEventPrefix(); + removeListener( + fabric.document, + `${eventTypePrefix}up`, + this._onMouseUp as EventListener + ); + removeListener( + fabric.document, + 'touchend', + this._onTouchEnd as EventListener, + addEventOptions + ); + removeListener( + fabric.document, + `${eventTypePrefix}move`, + this._onMouseMove as EventListener, + addEventOptions + ); + removeListener( + fabric.document, + 'touchmove', + this._onMouseMove as EventListener, + addEventOptions + ); + } + + /** + * @private + */ + private _bindEvents() { + if (this.eventsBound) { + // for any reason we pass here twice we do not want to bind events twice. + return; + } + ( + [ + '_onMouseDown', + '_onTouchStart', + '_onMouseMove', + '_onMouseUp', + '_onTouchEnd', + '_onResize', + // '_onGesture', + // '_onDrag', + // '_onShake', + // '_onLongPress', + // '_onOrientationChange', + '_onMouseWheel', + '_onMouseOut', + '_onMouseEnter', + '_onContextMenu', + '_onDoubleClick', + '_onDragStart', + '_onDragEnd', + '_onDragProgress', + '_onDragOver', + '_onDragEnter', + '_onDragLeave', + '_onDrop', + ] as const + ).forEach((eventHandler) => { + // @ts-expect-error dumb TS + this[eventHandler] = this[eventHandler].bind(this); + }); + this.eventsBound = true; + } + + /** + * @private + * @param {Event} [e] Event object fired on wheel event + */ + private _onMouseWheel(e: MouseEvent) { + this.__onMouseWheel(e); + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + private _onMouseOut(e: TPointerEvent) { + const target = this._hoveredTarget; + const shared = { + e, + isClick: false, + pointer: this.getPointer(e), + absolutePointer: this.getPointer(e, true), + }; + this.fire('mouse:out', { ...shared, target }); + this._hoveredTarget = undefined; + target && target.fire('mouseout', { ...shared }); + this._hoveredTargets.forEach((nestedTarget) => { + this.fire('mouse:out', { ...shared, target: nestedTarget }); + nestedTarget && nestedTarget.fire('mouseout', { ...shared }); + }); + this._hoveredTargets = []; + } + + /** + * @private + * @param {Event} e Event object fired on mouseenter + */ + private _onMouseEnter(e: TPointerEvent) { + // This find target and consequent 'mouse:over' is used to + // clear old instances on hovered target. + // calling findTarget has the side effect of killing target.__corner. + // as a short term fix we are not firing this if we are currently transforming. + // as a long term fix we need to separate the action of finding a target with the + // side effects we added to it. + if (!this._currentTransform && !this.findTarget(e)) { + this.fire('mouse:over', { + e, + isClick: false, + pointer: this.getPointer(e), + absolutePointer: this.getPointer(e, true), + }); + this._hoveredTarget = undefined; + this._hoveredTargets = []; + } + } + + /** + * supports native like text dragging + * @private + * @param {DragEvent} e + */ + private _onDragStart(e: DragEvent) { + const activeObject = this.getActiveObject(); + if ( + isFabricObjectWithDragSupport(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 as EventListener + ); + return; + } + stopEvent(e); + } + + /** + * @private + */ + private _renderDragEffects( + e: DragEvent, + source?: FabricObject, + target?: FabricObject + ) { + const ctx = this.contextTop; + if (source) { + source.clearContextTop(true); + source.renderDragSourceEffect(e); + } + if (target) { + if (target !== source) { + ctx.restore(); + ctx.save(); + target.clearContextTop(true); + } + target.renderDropTargetEffect(e); + } + ctx.restore(); + } + + /** + * 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 + */ + private _onDragEnd(e: DragEvent) { + const didDrop = !!e.dataTransfer && e.dataTransfer.dropEffect !== 'none', + dropTarget = didDrop ? this._activeObject : undefined, + options = { + e, + target: this._dragSource as FabricObject, + subTargets: this.targets, + dragSource: this._dragSource as FabricObject, + didDrop, + dropTarget: dropTarget as FabricObject, + }; + removeListener( + this.upperCanvasEl, + 'drag', + this._onDragProgress as EventListener + ); + this.fire('dragend', options); + this._dragSource && this._dragSource.fire('dragend', options); + delete this._dragSource; + // we need to call mouse up synthetically because the browser won't + this._onMouseUp(e); + } + + /** + * fire `drag` event on canvas and drag source + * @private + * @param {DragEvent} e + */ + private _onDragProgress(e: DragEvent) { + const options = { + e, + target: this._dragSource as FabricObject | undefined, + dragSource: this._dragSource as FabricObject | undefined, + dropTarget: this._draggedoverTarget as FabricObject, + }; + 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 + */ + private _onDragOver(e: DragEvent) { + const eventType = 'dragover', + target = this.findTarget(e), + targets = this.targets, + options = { + e: e, + target, + subTargets: targets, + dragSource: this._dragSource as FabricObject, + canDrop: false, + dropTarget: undefined, + }; + let dropTarget; + // fire on canvas + this.fire(eventType, options); + // make sure we fire dragenter events before dragover + // if dragleave is needed, object will not fire dragover so we don't need to trouble ourselves with it + this._fireEnterLeaveEvents(target, options); + if (target) { + // render drag selection before rendering target cursor for correct visuals + if (target.canDrop(e)) { + dropTarget = target; + } + target.fire(eventType, options); + } + // propagate the event to subtargets + for (let i = 0; i < targets.length; i++) { + const subTarget = targets[i]; + // accept event only if previous targets didn't + // TODO: verify if those should loop in inverse order then? + // what is the order of subtargets? + if (!e.defaultPrevented && subTarget.canDrop(e)) { + dropTarget = subTarget; + } + subTarget.fire(eventType, options); + } + // render drag effects now that relations between source and target is clear + this._renderDragEffects(e, this._dragSource, dropTarget); + } + + /** + * fire `dragleave` on `dragover` targets + * @private + * @param {Event} [e] Event object fired on Event.js shake + */ + private _onDragEnter(e: DragEvent) { + const target = this.findTarget(e); + const options = { + e, + target: target as FabricObject, + subTargets: this.targets, + dragSource: this._dragSource, + }; + this.fire('dragenter', options); + // fire dragenter on targets + this._fireEnterLeaveEvents(target, options); + } + + /** + * fire `dragleave` on `dragover` targets + * @private + * @param {Event} [e] Event object fired on Event.js shake + */ + private _onDragLeave(e: DragEvent) { + const options = { + e, + target: this._draggedoverTarget, + subTargets: this.targets, + dragSource: this._dragSource, + }; + this.fire('dragleave', options); + // fire dragleave on targets + this._fireEnterLeaveEvents(undefined, options); + // clear targets + this.targets = []; + 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 + */ + private _onDrop(e: DragEvent) { + const options = this._simpleEventHandler('drop:before', { + e, + dragSource: this._dragSource, + pointer: this.getPointer(e), + }); + // will be set by the drop target + options.didDrop = false; + // will be set by the drop target, used in case options.target refuses the drop + options.dropTarget = undefined; + // fire `drop` + this._basicEventHandler('drop', options); + // inform canvas of the drop + // we do this because canvas was unaware of what happened at the time the `drop` event was fired on it + // use for side effects + this.fire('drop:after', options); + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + private _onContextMenu(e: TPointerEvent): false { + const options = this._simpleEventHandler('contextmenu:before', { e }); + // TODO: this line is silly because the dev can subscribe to the event and prevent it themselves + this.stopContextMenu && stopEvent(e); + this._basicEventHandler('contextmenu', options); + return false; + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + private _onDoubleClick(e: TPointerEvent) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'dblclick'); + 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: TouchEvent | PointerEvent): number { + const changedTouches = (evt as TouchEvent).changedTouches; + + if (changedTouches) { + return changedTouches[0] && changedTouches[0].identifier; + } + + if (this.enablePointerEvents) { + return (evt as PointerEvent).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: TPointerEvent): boolean { + if ((evt as PointerEvent).isPrimary === true) { + return true; + } + if ((evt as PointerEvent).isPrimary === false) { + return false; + } + if (evt.type === 'touchend' && (evt as TouchEvent).touches.length === 0) { + return true; + } + if ((evt as TouchEvent).changedTouches) { + return ( + (evt as TouchEvent).changedTouches[0].identifier === this.mainTouchId + ); + } + return true; + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchStart(e: TouchEvent) { + e.preventDefault(); + if (this.mainTouchId === null) { + this.mainTouchId = this.getPointerId(e); + } + this.__onMouseDown(e); + this._resetTransformEventData(); + const canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + addListener( + fabric.document, + 'touchend', + this._onTouchEnd as EventListener, + addEventOptions + ); + addListener( + fabric.document, + 'touchmove', + this._onMouseMove as EventListener, + addEventOptions + ); + // Unbind mousedown to prevent double triggers from touch devices + removeListener( + canvasElement, + eventTypePrefix + 'down', + this._onMouseDown as EventListener + ); + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDown(e: TPointerEvent) { + this.__onMouseDown(e); + this._resetTransformEventData(); + const canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + removeListener( + canvasElement, + `${eventTypePrefix}move`, + this._onMouseMove as EventListener, + addEventOptions + ); + addListener( + fabric.document, + `${eventTypePrefix}up`, + this._onMouseUp as EventListener + ); + addListener( + fabric.document, + `${eventTypePrefix}move`, + this._onMouseMove as EventListener, + addEventOptions + ); + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onTouchEnd(e: TouchEvent) { + if (e.touches.length > 0) { + // if there are still touches stop here + return; + } + this.__onMouseUp(e); + this._resetTransformEventData(); + this.mainTouchId = null; + const eventTypePrefix = this._getEventPrefix(); + removeListener( + fabric.document, + 'touchend', + this._onTouchEnd as EventListener, + addEventOptions + ); + removeListener( + fabric.document, + 'touchmove', + this._onMouseMove as EventListener, + addEventOptions + ); + if (this._willAddMouseDown) { + clearTimeout(this._willAddMouseDown); + } + this._willAddMouseDown = setTimeout(() => { + // Wait 400ms before rebinding mousedown to prevent double triggers + // from touch devices + addListener( + this.upperCanvasEl, + eventTypePrefix + 'down', + this._onMouseDown as EventListener + ); + this._willAddMouseDown = 0; + }, 400) as unknown as number; + } + + /** + * @private + * @param {Event} e Event object fired on mouseup + */ + _onMouseUp(e: TPointerEvent) { + this.__onMouseUp(e); + this._resetTransformEventData(); + const canvasElement = this.upperCanvasEl, + eventTypePrefix = this._getEventPrefix(); + if (this._isMainEvent(e)) { + removeListener( + fabric.document, + `${eventTypePrefix}up`, + this._onMouseUp as EventListener + ); + removeListener( + fabric.document, + `${eventTypePrefix}move`, + this._onMouseMove as EventListener, + addEventOptions + ); + addListener( + canvasElement, + `${eventTypePrefix}move`, + this._onMouseMove as EventListener, + addEventOptions + ); + } + } + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMove(e: TPointerEvent) { + const activeObject = this.getActiveObject(); + !this.allowTouchScrolling && + (!activeObject || !activeObject.__isDragging) && + e.preventDefault && + e.preventDefault(); + this.__onMouseMove(e); + } + + /** + * @private + */ + _onResize() { + this.calcOffset(); + this._resetTransformEventData(); + } + + /** + * Decides whether the canvas should be redrawn in mouseup and mousedown events. + * @private + * @param {Object} target + */ + _shouldRender(target: FabricObject | undefined) { + const activeObject = this.getActiveObject(); + + // if just one of them is available or if they are both but are different objects + if ( + !!activeObject !== !!target || + (activeObject && target && activeObject !== target) + ) { + // this covers: switch of target, from target to no target, selection of target + // multiSelection with key and mouse + return true; + } else if (isInteractiveTextObject(activeObject)) { + // if we mouse up/down over a editing textbox a cursor change, + // there is no need to re render + return false; + } + return false; + } + + /** + * 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: TPointerEvent) { + const transform = this._currentTransform, + groupSelector = this._groupSelector, + isClick = + !groupSelector || (groupSelector.left === 0 && groupSelector.top === 0); + this._cacheTransformEventData(e); + const target = this._target; + this._handleEvent(e, 'up:before'); + // if right/middle click just fire events and return + // target undefined will make the _handleEvent search the target + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'up', RIGHT_CLICK, isClick); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); + } + this._resetTransformEventData(); + 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._maybeGroupObjects(e); + if (!shouldRender) { + shouldRender = + this._shouldRender(target) || + (!targetWasActive && target === this._activeObject); + } + } + let pointer, corner; + if (target) { + corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + if ( + target.selectable && + target !== this._activeObject && + target.activeOn === 'up' + ) { + this.setActiveObject(target, e); + shouldRender = true; + } else { + const control = target.controls[corner as string]; + const mouseUpHandler = + control && control.getMouseUpHandler(e, target, control); + if (mouseUpHandler) { + pointer = this.getPointer(e); + mouseUpHandler(e, transform!, pointer.x, pointer.y); + } + } + target.isMoving = false; + } + // if we are ending up a transform on a different control or a new object + // fire the original mouse up from the corner that started the transform + 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.getPointer(e); + originalMouseUpHandler && + originalMouseUpHandler(e, transform, pointer.x, pointer.y); + } + this._setCursorFromEvent(e, target); + this._handleEvent(e, 'up', LEFT_CLICK, isClick); + this._groupSelector = null; + this._currentTransform = null; + // reset the target information about which corner is selected + target && (target.__corner = 0); + if (shouldRender) { + this.requestRenderAll(); + } else if (!isClick) { + this.renderTop(); + } + } + + /** + * @private + * Handle event firing for target and subtargets + * @param {String} eventType event to fire (up, down or move) + * @param {Event} e event from mouse + * @param {object} [data] event data overrides + * @return {object} options + */ + _simpleEventHandler< + T extends keyof (CanvasEvents | ObjectEvents), + E extends TPointerEvent | DragEvent + >( + eventType: T, + { + e, + ...data + }: Omit<(CanvasEvents & ObjectEvents)[T], 'target' | 'subTargets'> & + TEvent + ) { + const target = this.findTarget(e), + subTargets = this.targets || []; + // @ts-expect-error TODO fix generic e + return this._basicEventHandler(eventType, { + e, + target, + subTargets, + ...data, + }); + } + + _basicEventHandler( + eventType: T, + options: (CanvasEvents & ObjectEvents)[T] + ) { + const { target, subTargets = [] } = options as { + target?: FabricObject; + subTargets: FabricObject[]; + }; + this.fire(eventType, options); + target && target.fire(eventType, options); + for (let i = 0; i < subTargets.length; i++) { + subTargets[i].fire(eventType, options); + } + return options; + } + + /** + * @private + * Handle event firing for target and subtargets + * @param {Event} e event from mouse + * @param {String} eventType event to fire (up, down or move) + * @param {fabric.Object} targetObj receiving event + * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right + * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. + */ + _handleEvent( + e: TPointerEvent, + eventType: TPointerEventNames, + button = LEFT_CLICK, + isClick = false + ) { + const target = this._target, + targets = this.targets || [], + options: TPointerEventInfo = { + e: e, + target: target, + subTargets: targets, + button, + isClick, + pointer: this.getPointer(e), + absolutePointer: this.getPointer(e, true), + transform: this._currentTransform, + }; + if (eventType === 'up') { + options.currentTarget = this.findTarget(e); + options.currentSubTargets = this.targets; + } + this.fire(`mouse:${eventType}`, options); + // this may be a little be more complicated of what we want to handle + target && target.fire(`mouse${eventType}`, options); + for (let i = 0; i < targets.length; i++) { + targets[i].fire(`mouse${eventType}`, options); + } + } + + /** + * End the current transform. + * You don't usually need to call this method unless you are interrupting a user initiated transform + * because of some other event ( a press of key combination, or something that block the user UX ) + * @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event + */ + endCurrentTransform(e: TPointerEvent) { + const transform = this._currentTransform; + this._finalizeCurrentTransform(e); + if (transform && transform.target) { + // this could probably go inside _finalizeCurrentTransform + transform.target.isMoving = false; + } + this._currentTransform = null; + } + + /** + * @private + * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event + */ + _finalizeCurrentTransform(e: TPointerEvent) { + const transform = this._currentTransform!, + target = transform.target, + options = { + e, + target, + transform, + action: transform.action, + }; + + if (target._scaling) { + target._scaling = false; + } + + target.setCoords(); + + if ( + transform.actionPerformed || + // @ts-ignore + (this.stateful && target.hasStateChanged()) + ) { + this.fire('object:modified', options); + target.fire('modified', options); + } + } + + /** + * @private + * @param {Event} e Event object fired on mousedown + */ + _onMouseDownInDrawingMode(e: TPointerEvent) { + this._isCurrentlyDrawing = true; + if (this.getActiveObject()) { + this.discardActiveObject(e); + this.requestRenderAll(); + } + const pointer = this.getPointer(e); + this.freeDrawingBrush && + this.freeDrawingBrush.onMouseDown(pointer, { e, pointer }); + this._handleEvent(e, 'down'); + } + + /** + * @private + * @param {Event} e Event object fired on mousemove + */ + _onMouseMoveInDrawingMode(e: TPointerEvent) { + if (this._isCurrentlyDrawing) { + const pointer = this.getPointer(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: TPointerEvent) { + const pointer = this.getPointer(e); + if (this.freeDrawingBrush) { + this._isCurrentlyDrawing = !!this.freeDrawingBrush.onMouseUp({ + e: e, + pointer: 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: TPointerEvent) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'down:before'); + let target: FabricObject | undefined = this._target; + // if right click just fire events + if (checkClick(e, RIGHT_CLICK)) { + if (this.fireRightClick) { + this._handleEvent(e, 'down', RIGHT_CLICK); + } + return; + } + + if (checkClick(e, MIDDLE_CLICK)) { + if (this.fireMiddleClick) { + this._handleEvent(e, 'down', MIDDLE_CLICK); + } + return; + } + + if (this.isDrawingMode) { + this._onMouseDownInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + // ignore if some object is being transformed at this moment + if (this._currentTransform) { + return; + } + + const pointer = this.getPointer(e, true); + // save pointer for check in __onMouseUp event + this._previousPointer = pointer; + const shouldRender = this._shouldRender(target), + shouldGroup = this._shouldGroup(e, target); + if (this._shouldClearSelection(e, target)) { + this.discardActiveObject(e); + } else if (shouldGroup) { + // in order for shouldGroup to be true, target needs to be true + this._handleGrouping(e, target!); + target = this._activeObject; + } + // we start a group selector rectangle if + // selection is enabled + // and there is no target, or the following 3 condition both apply + // target is not selectable ( otherwise we selected it ) + // target is not editing + // target is not already selected ( otherwise we drage ) + if ( + this.selection && + (!target || + (!target.selectable && + // @ts-ignore + !target.isEditing && + target !== this._activeObject)) + ) { + const p = this.getPointer(e); + this._groupSelector = { + ex: p.x, + ey: p.y, + top: 0, + left: 0, + }; + } + + if (target) { + const alreadySelected = target === this._activeObject; + if (target.selectable && target.activeOn === 'down') { + this.setActiveObject(target, e); + } + const corner = target._findTargetCorner( + this.getPointer(e, true), + fabric.util.isTouchEvent(e) + ); + if (target === this._activeObject && (corner || !shouldGroup)) { + this._setupCurrentTransform(e, target, alreadySelected); + const control = target.controls[corner], + pointer = this.getPointer(e), + mouseDownHandler = + control && control.getMouseDownHandler(e, target, control); + if (mouseDownHandler) { + mouseDownHandler(e, this._currentTransform!, pointer.x, pointer.y); + } + } + } + const invalidate = shouldRender || shouldGroup; + // we clear `_objectsToRender` in case of a change in order to repopulate it at rendering + // run before firing the `down` event to give the dev a chance to populate it themselves + invalidate && (this._objectsToRender = undefined); + this._handleEvent(e, 'down'); + // we must renderAll so that we update the visuals + invalidate && this.requestRenderAll(); + } + + /** + * reset cache form common information needed during event processing + * @private + */ + _resetTransformEventData() { + this._target = undefined; + this._pointer = undefined; + this._absolutePointer = undefined; + } + + /** + * Cache common information needed during event processing + * @private + * @param {Event} e Event object fired on event + */ + _cacheTransformEventData(e: TPointerEvent) { + // reset in order to avoid stale caching + this._resetTransformEventData(); + this._pointer = this.getPointer(e, true); + this._absolutePointer = this.restorePointerVpt(this._pointer); + this._target = this._currentTransform + ? this._currentTransform.target + : this.findTarget(e); + } + + /** + * @private + */ + _beforeTransform(e: TPointerEvent) { + const t = this._currentTransform!; + // @ts-ignore + this.stateful && t.target.saveState(); + this.fire('before:transform', { + e, + transform: t, + }); + } + + /** + * 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: TPointerEvent) { + this._handleEvent(e, 'move:before'); + this._cacheTransformEventData(e); + + if (this.isDrawingMode) { + this._onMouseMoveInDrawingMode(e); + return; + } + + if (!this._isMainEvent(e)) { + return; + } + + const groupSelector = this._groupSelector; + + // We initially clicked in an empty area, so we draw a box for multiple selection + if (groupSelector) { + const pointer = this.getPointer(e); + + groupSelector.left = pointer.x - groupSelector.ex; + groupSelector.top = pointer.y - groupSelector.ey; + + this.renderTop(); + } else if (!this._currentTransform) { + const target = this.findTarget(e); + this._setCursorFromEvent(e, target); + this._fireOverOutEvents(e, target); + } else { + this._transformObject(e); + } + this._handleEvent(e, 'move'); + this._resetTransformEventData(); + } + + /** + * 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: TPointerEvent, target?: FabricObject) { + const _hoveredTarget = this._hoveredTarget, + _hoveredTargets = this._hoveredTargets, + targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents('mouse', { + e, + target, + oldTarget: _hoveredTarget, + fireCanvas: true, + }); + for (let i = 0; i < length; i++) { + this.fireSyntheticInOutEvents('mouse', { + e, + target: targets[i], + oldTarget: _hoveredTargets[i], + }); + } + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + } + + /** + * 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(target: FabricObject | undefined, data: DragEventData) { + const draggedoverTarget = this._draggedoverTarget, + _hoveredTargets = this._hoveredTargets, + targets = this.targets, + length = Math.max(_hoveredTargets.length, targets.length); + + this.fireSyntheticInOutEvents('drag', { + ...data, + target, + oldTarget: draggedoverTarget, + fireCanvas: true, + }); + for (let i = 0; i < length; i++) { + this.fireSyntheticInOutEvents('drag', { + ...data, + target: targets[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: T, + { + target, + oldTarget, + fireCanvas, + e, + ...data + }: TSyntheticEventContext[T] & { + target?: FabricObject; + oldTarget?: FabricObject; + fireCanvas?: boolean; + } + ) { + const { targetIn, targetOut, canvasIn, canvasOut } = + syntheticEventConfig[type]; + const targetChanged = oldTarget !== target; + + if (oldTarget && targetChanged) { + const outOpt = { + ...data, + e, + target: oldTarget, + nextTarget: target, + isClick: false, + pointer: this.getPointer(e), + absolutePointer: this.getPointer(e, true), + }; + fireCanvas && this.fire(canvasIn, outOpt); + oldTarget.fire(targetOut, outOpt); + } + if (target && targetChanged) { + const inOpt: TPointerEventInfo = { + ...data, + e, + target, + previousTarget: oldTarget, + isClick: false, + pointer: this.getPointer(e), + absolutePointer: this.getPointer(e, true), + }; + fireCanvas && this.fire(canvasOut, inOpt); + target.fire(targetIn, inOpt); + } + } + + /** + * Method that defines actions when an Event Mouse Wheel + * @param {Event} e Event object fired on mouseup + */ + __onMouseWheel(e: TPointerEvent) { + this._cacheTransformEventData(e); + this._handleEvent(e, 'wheel'); + this._resetTransformEventData(); + } + + /** + * @private + * @param {Event} e Event fired on mousemove + */ + _transformObject(e: TPointerEvent) { + const pointer = this.getPointer(e), + transform = this._currentTransform!, + target = transform.target, + // transform pointer to target's containing coordinate plane + // both pointer and object should agree on every point + localPointer = target.group + ? sendPointToPlane( + pointer, + undefined, + target.group.calcTransformMatrix() + ) + : pointer; + // seems used only here. + // @TODO: investigate; + // @ts-ignore + transform.reset = false; + transform.shiftKey = e.shiftKey; + transform.altKey = !!this.centeredKey && e[this.centeredKey]; + + this._performTransformAction(e, transform, localPointer); + transform.actionPerformed && this.requestRenderAll(); + } + + /** + * @private + */ + _performTransformAction( + e: TPointerEvent, + transform: Transform, + pointer: Point + ) { + const x = pointer.x, + y = pointer.y, + action = transform.action, + actionHandler = transform.actionHandler; + let actionPerformed = false; + // this object could be created from the function in the control handlers + + if (actionHandler) { + actionPerformed = actionHandler(e, transform, x, y); + } + 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: TPointerEvent, target?: FabricObject) { + if (!target) { + this.setCursor(this.defaultCursor); + return; + } + let hoverCursor = target.hoverCursor || this.hoverCursor; + const activeSelection = isActiveSelection(this._activeObject) + ? this._activeObject + : null, + // only show proper corner when group selection is not active + corner = + (!activeSelection || !activeSelection.contains(target)) && + // here we call findTargetCorner always with undefined for the touch parameter. + // we assume that if you are using a cursor you do not need to interact with + // the bigger touch area. + target._findTargetCorner(this.getPointer(e, true)); + + if (!corner) { + if ((target as Group).subTargetCheck) { + // hoverCursor should come from top-most subTarget, + // so we walk the array backwards + this.targets + .concat() + .reverse() + .map((_target) => { + hoverCursor = _target.hoverCursor || hoverCursor; + }); + } + this.setCursor(hoverCursor); + } else { + const control = target.controls[corner]; + this.setCursor(control.cursorStyleHandler(e, control, target)); + } + } + + // Grouping objects mixin + + /** + * Return true if the current mouse event that generated a new selection should generate a group + * @private + * @param {TPointerEvent} e Event object + * @param {FabricObject} target + * @return {Boolean} + */ + _shouldGroup(e: TPointerEvent, target?: FabricObject): boolean { + const activeObject = this._activeObject; + // check if an active object exists on canvas and if the user is pressing the `selectionKey` while canvas supports multi selection. + return ( + !!activeObject && + this._isSelectionKeyPressed(e) && + this.selection && + // on top of that the user also has to hit a target that is selectable. + !!target && + target.selectable && + // if all pre-requisite pass, the target is either something different from the current + // activeObject or if an activeSelection already exists + // TODO at time of writing why `activeObject.type === 'activeSelection'` matter is unclear. + // is a very old condition uncertain if still valid. + (activeObject !== target || activeObject.type === 'activeSelection') && + // make sure `activeObject` and `target` aren't ancestors of each other + !target.isDescendantOf(activeObject) && + !activeObject.isDescendantOf(target) && + // target accepts selection + !target.onSelect({ e: e }) + ); + } + + /** + * Handles active selection creation for user event + * @private + * @param {TPointerEvent} e Event object + * @param {FabricObject} target + */ + _handleGrouping(e: TPointerEvent, target: FabricObject) { + let groupingTarget: FabricObject | undefined = target; + // Called always a shouldGroup, meaning that we can trust this._activeObject exists. + const activeObject = this._activeObject!; + // avoid multi select when shift click on a corner + if (activeObject.__corner) { + return; + } + if (groupingTarget === activeObject) { + // if it's a group, find target again, using activeGroup objects + groupingTarget = this.findTarget(e, true); + // if even object is not found or we are on activeObjectCorner, bail out + if (!groupingTarget || !groupingTarget.selectable) { + return; + } + } + if (activeObject && activeObject.type === 'activeSelection') { + this._updateActiveSelection(e, groupingTarget); + } else { + this._createActiveSelection(e, groupingTarget); + } + } + + /** + * @private + */ + _updateActiveSelection(e: TPointerEvent, target: FabricObject) { + const activeSelection = this._activeObject! as ActiveSelection, + currentActiveObjects = activeSelection.getObjects(); + if (target.group === activeSelection) { + activeSelection.remove(target); + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); + if (activeSelection.size() === 1) { + // activate last remaining object + this._setActiveObject(activeSelection.item(0), e); + } + } else { + activeSelection.add(target); + this._hoveredTarget = activeSelection; + this._hoveredTargets = this.targets.concat(); + } + this._fireSelectionEvents(currentActiveObjects, e); + } + + /** + * Generates and set as active the active selection from user events + * @private + */ + _createActiveSelection(e: TPointerEvent, target: FabricObject) { + const currentActive = this.getActiveObject()!; + const groupObjects = target.isInFrontOf(currentActive) + ? [currentActive, target] + : [target, currentActive]; + // @ts-ignore + currentActive.isEditing && currentActive.exitEditing(); + // handle case: target is nested + const newActiveSelection = new ActiveSelection(groupObjects, { + canvas: this, + }); + this._hoveredTarget = newActiveSelection; + // ISSUE 4115: should we consider subTargets here? + // this._hoveredTargets = []; + // this._hoveredTargets = this.targets.concat(); + this._setActiveObject(newActiveSelection, e); + this._fireSelectionEvents([currentActive], e); + } + + /** + * Finds objects inside the selection rectangle and group them + * @private + * @param {Event} e mouse event + */ + _groupSelectedObjects(e: TPointerEvent) { + const group = this._collectObjects(e); + // do not create group for 1 element only + if (group.length === 1) { + this.setActiveObject(group[0], e); + } else if (group.length > 1) { + const aGroup = new ActiveSelection(group.reverse(), { + canvas: this, + }); + this.setActiveObject(aGroup, e); + } + } + + /** + * @private + */ + _collectObjects(e: TPointerEvent) { + const group = [], + _groupSelector = this._groupSelector, + point1 = new Point(_groupSelector.ex, _groupSelector.ey), + point2 = point1.add(new Point(_groupSelector.left, _groupSelector.top)), + selectionX1Y1 = point1.min(point2), + selectionX2Y2 = point1.max(point2), + allowIntersect = !this.selectionFullyContained, + isClick = point1.eq(point2); + // we iterate reverse order to collect top first in case of click. + for (let i = this._objects.length; i--; ) { + const currentObject = this._objects[i]; + + if ( + !currentObject || + !currentObject.selectable || + !currentObject.visible + ) { + continue; + } + + if ( + (allowIntersect && + currentObject.intersectsWithRect( + selectionX1Y1, + selectionX2Y2, + true + )) || + currentObject.isContainedWithinRect( + selectionX1Y1, + selectionX2Y2, + true + ) || + (allowIntersect && + currentObject.containsPoint(selectionX1Y1, undefined, true)) || + (allowIntersect && + currentObject.containsPoint(selectionX2Y2, undefined, true)) + ) { + group.push(currentObject); + // only add one object if it's a click + if (isClick) { + break; + } + } + } + + if (group.length > 1) { + return group.filter((object) => !object.onSelect({ e })); + } + + return group; + } + + /** + * @private + */ + _maybeGroupObjects(e: TPointerEvent) { + if (this.selection && this._groupSelector) { + this._groupSelectedObjects(e); + } + this.setCursor(this.defaultCursor); + // clear selection and current transformation + this._groupSelector = null; + } +} + +// there is an order execution bug if i put this as public property. +Object.assign(Canvas.prototype, { + eventsBound: false, +}); + +fabric.Canvas = Canvas; diff --git a/src/mixins/canvas_gestures.mixin.ts b/src/canvas/canvas_gestures.mixin.ts similarity index 77% rename from src/mixins/canvas_gestures.mixin.ts rename to src/canvas/canvas_gestures.mixin.ts index 90c97695e78..3b3528caa9f 100644 --- a/src/mixins/canvas_gestures.mixin.ts +++ b/src/canvas/canvas_gestures.mixin.ts @@ -1,6 +1,7 @@ //@ts-nocheck import { scalingEqually } from '../controls/actions'; +import { fireEvent } from '../util/fireEvent'; (function (global) { var fabric = global.fabric, @@ -129,6 +130,24 @@ import { scalingEqually } from '../controls/actions'; }); }, + /** + * @private + * @param {Event} [e] Event object fired on Event.js gesture + * @param {Event} [self] Inner Event object + */ + _onGesture: function (e, self) { + this.__onTransformGesture(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js drag + * @param {Event} [self] Inner Event object + */ + _onDrag: function (e, self) { + this.__onDrag(e, self); + }, + /** * Scales an object by a factor * @param {Number} s The scale factor to apply to the current scale level @@ -154,12 +173,39 @@ import { scalingEqually } from '../controls/actions'; return; } t.target.rotate(radiansToDegrees(degreesToRadians(curAngle) + t.theta)); - this._fire('rotating', { + fireEvent('rotating', { target: t.target, e: e, transform: t, }); }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js orientation change + * @param {Event} [self] Inner Event object + */ + _onOrientationChange: function (e, self) { + this.__onOrientationChange(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onShake: function (e, self) { + this.__onShake(e, self); + }, + + /** + * @private + * @param {Event} [e] Event object fired on Event.js shake + * @param {Event} [self] Inner Event object + */ + _onLongPress: function (e, self) { + this.__onLongPress(e, self); + }, } ); })(typeof exports !== 'undefined' ? exports : window); diff --git a/src/static_canvas.class.ts b/src/canvas/static_canvas.class.ts similarity index 97% rename from src/static_canvas.class.ts rename to src/canvas/static_canvas.class.ts index 4b21f9a6275..b287564ba3c 100644 --- a/src/static_canvas.class.ts +++ b/src/canvas/static_canvas.class.ts @@ -1,43 +1,43 @@ // @ts-nocheck -import { fabric } from '../HEADER'; -import { config } from './config'; -import { iMatrix, VERSION } from './constants'; -import type { StaticCanvasEvents } from './EventTypeDefs'; -import { Gradient } from './gradient'; -import { createCollectionMixin } from './mixins/collection.mixin'; -import { TSVGReviver } from './mixins/object.svg_export'; -import { CommonMethods } from './mixins/shared_methods.mixin'; -import { Pattern } from './pattern.class'; -import { Point } from './point.class'; -import type { FabricObject } from './shapes/Object/FabricObject'; -import { TCachedFabricObject } from './shapes/Object/Object'; -import { Rect } from './shapes/rect.class'; +import { fabric } from '../../HEADER'; +import { config } from '../config'; +import { iMatrix, VERSION } from '../constants'; +import type { CanvasEvents, StaticCanvasEvents } from '../EventTypeDefs'; +import { Gradient } from '../gradient'; +import { createCollectionMixin } from '../mixins/collection.mixin'; +import { TSVGReviver } from '../mixins/object.svg_export'; +import { CommonMethods } from '../mixins/shared_methods.mixin'; +import { Pattern } from '../pattern.class'; +import { Point } from '../point.class'; +import type { FabricObject } from '../shapes/Object/FabricObject'; +import { TCachedFabricObject } from '../shapes/Object/Object'; +import { Rect } from '../shapes/rect.class'; import type { TCornerPoint, TFiller, TMat2D, TSize, TValidToObjectMethod, -} from './typedefs'; -import { cancelAnimFrame, requestAnimFrame } from './util/animate'; +} from '../typedefs'; +import { cancelAnimFrame, requestAnimFrame } from '../util/animate'; import { cleanUpJsdomNode, getElementOffset, getNodeCanvas, -} from './util/dom_misc'; -import { removeFromArray } from './util/internals'; -import { uid } from './util/internals/uid'; -import { createCanvasElement, isHTMLCanvas } from './util/misc/dom'; -import { invertTransform, transformPoint } from './util/misc/matrix'; -import { pick } from './util/misc/pick'; -import { matrixToSVG } from './util/misc/svgParsing'; -import { toFixed } from './util/misc/toFixed'; +} from '../util/dom_misc'; +import { removeFromArray } from '../util/internals'; +import { uid } from '../util/internals/uid'; +import { createCanvasElement, isHTMLCanvas } from '../util/misc/dom'; +import { invertTransform, transformPoint } from '../util/misc/matrix'; +import { pick } from '../util/misc/pick'; +import { matrixToSVG } from '../util/misc/svgParsing'; +import { toFixed } from '../util/misc/toFixed'; import { isActiveSelection, isCollection, isFiller, isTextObject, -} from './util/types'; +} from '../util/types'; const CANVAS_INIT_ERROR = 'Could not initialize `canvas` element'; @@ -54,7 +54,7 @@ export type TSVGExportOptions = { width: number; height: number; }; - encoding?: 'UTF-8'; // test Econding type and see what happens + encoding?: 'UTF-8'; // test Encoding type and see what happens width?: string; height?: string; reviver?: TSVGReviver; @@ -69,10 +69,10 @@ export type TSVGExportOptions = { * @fires object:added * @fires object:removed */ -// eslint-disable-next-line max-len +// TODO: fix `EventSpec` inheritance https://github.com/microsoft/TypeScript/issues/26154#issuecomment-1366616260 export class StaticCanvas< EventSpec extends StaticCanvasEvents = StaticCanvasEvents -> extends createCollectionMixin(CommonMethods) { +> extends createCollectionMixin(CommonMethods) { /** * Background color of canvas instance. * @type {(String|TFiller)} diff --git a/src/controls/control.class.ts b/src/controls/control.class.ts index 3b295a11e3e..365cbaf3fd6 100644 --- a/src/controls/control.class.ts +++ b/src/controls/control.class.ts @@ -2,8 +2,8 @@ import { fabric } from '../../HEADER'; import { halfPI } from '../constants'; import { + ControlActionHandler, TPointerEvent, - TransformAction, TransformActionHandler, } from '../EventTypeDefs'; import { Point } from '../point.class'; @@ -146,7 +146,7 @@ export class Control { /** * The control actionHandler, provide one to handle action ( control being moved ) * @param {Event} eventData the native mouse event - * @param {Object} transformData properties of the current transform + * @param {Transform} transformData properties of the current transform * @param {Number} x x position of the cursor * @param {Number} y y position of the cursor * @return {Boolean} true if the action/event modified the object @@ -156,22 +156,22 @@ export class Control { /** * The control handler for mouse down, provide one to handle mouse down on control * @param {Event} eventData the native mouse event - * @param {Object} transformData properties of the current transform + * @param {Transform} transformData properties of the current transform * @param {Number} x x position of the cursor * @param {Number} y y position of the cursor * @return {Boolean} true if the action/event modified the object */ - mouseDownHandler?: TransformAction; + mouseDownHandler?: ControlActionHandler; /** * The control mouseUpHandler, provide one to handle an effect on mouse up. * @param {Event} eventData the native mouse event - * @param {Object} transformData properties of the current transform + * @param {Transform} transformData properties of the current transform * @param {Number} x x position of the cursor * @param {Number} y y position of the cursor * @return {Boolean} true if the action/event modified the object */ - mouseUpHandler?: TransformAction; + mouseUpHandler?: ControlActionHandler; /** * Returns control actionHandler @@ -184,7 +184,7 @@ export class Control { eventData: TPointerEvent, fabricObject: FabricObject, control: Control - ) { + ): TransformActionHandler | undefined { return this.actionHandler; } @@ -199,12 +199,13 @@ export class Control { eventData: TPointerEvent, fabricObject: FabricObject, control: Control - ) { + ): ControlActionHandler | undefined { return this.mouseDownHandler; } /** - * Returns control mouseUp handler + * Returns control mouseUp handler. + * During actions the fabricObject or the control can be of different obj * @param {Event} eventData the native mouse event * @param {FabricObject} fabricObject on which the control is displayed * @param {Control} control control for which the action handler is being asked @@ -214,7 +215,7 @@ export class Control { eventData: TPointerEvent, fabricObject: FabricObject, control: Control - ) { + ): ControlActionHandler | undefined { return this.mouseUpHandler; } diff --git a/src/controls/scale.ts b/src/controls/scale.ts index a8362fcbd61..5beffd93570 100644 --- a/src/controls/scale.ts +++ b/src/controls/scale.ts @@ -6,7 +6,7 @@ import { } from '../EventTypeDefs'; import type { FabricObject } from '../shapes/Object/FabricObject'; import { TAxis } from '../typedefs'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; import { findCornerQuadrant, getLocalPoint, @@ -37,7 +37,7 @@ export function scaleIsProportional( fabricObject: FabricObject ): boolean { const canvas = fabricObject.canvas as Canvas, - uniformIsToggled = eventData[canvas.uniScaleKey]; + uniformIsToggled = eventData[canvas.uniScaleKey!]; return ( (canvas.uniformScaling && !uniformIsToggled) || (!canvas.uniformScaling && uniformIsToggled) diff --git a/src/controls/scaleSkew.ts b/src/controls/scaleSkew.ts index 408ed59e791..1f0fffc321d 100644 --- a/src/controls/scaleSkew.ts +++ b/src/controls/scaleSkew.ts @@ -4,14 +4,13 @@ import { TPointerEvent, TransformActionHandler, } from '../EventTypeDefs'; -import type { FabricObject } from '../shapes/Object/Object'; +import type { FabricObject } from '../shapes/Object/FabricObject'; import { TAxisKey } from '../typedefs'; -import { Canvas } from '../__types__'; import { scaleCursorStyleHandler, scalingX, scalingY } from './scale'; import { skewCursorStyleHandler, skewHandlerX, skewHandlerY } from './skew'; function isAltAction(eventData: TPointerEvent, target: FabricObject) { - return eventData[(target.canvas as Canvas)?.altActionKey]; + return eventData[target.canvas!.altActionKey!]; } /** diff --git a/src/mixins/canvas_events.mixin.ts b/src/mixins/canvas_events.mixin.ts deleted file mode 100644 index d5b4e2b4bad..00000000000 --- a/src/mixins/canvas_events.mixin.ts +++ /dev/null @@ -1,1345 +0,0 @@ -//@ts-nocheck - -import { stopEvent } from '../util/dom_event'; -import { fireEvent } from '../util/fireEvent'; - -(function (global) { - var fabric = global.fabric, - addListener = fabric.util.addListener, - removeListener = fabric.util.removeListener, - RIGHT_CLICK = 3, - MIDDLE_CLICK = 2, - LEFT_CLICK = 1, - addEventOptions = { passive: false }; - - function checkClick(e, value) { - return e.button && e.button === value - 1; - } - - fabric.util.object.extend( - fabric.Canvas.prototype, - /** @lends fabric.Canvas.prototype */ { - /** - * Contains the id of the touch event that owns the fabric transform - * @type Number - * @private - */ - mainTouchId: null, - - /** - * Adds mouse listeners to canvas - * @private - */ - _initEventListeners: function () { - // in case we initialized the class twice. This should not happen normally - // but in some kind of applications where the canvas element may be changed - // this is a workaround to having double listeners. - this.removeListeners(); - this._bindEvents(); - this.addOrRemove(addListener, 'add'); - }, - - /** - * return an event prefix pointer or mouse. - * @private - */ - _getEventPrefix: function () { - return this.enablePointerEvents ? 'pointer' : 'mouse'; - }, - - addOrRemove: function (functor, eventjsFunctor) { - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - functor(fabric.window, '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); - functor(canvasElement, 'contextmenu', this._onContextMenu); - functor(canvasElement, 'dblclick', this._onDoubleClick); - 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 - ); - } - if (typeof eventjs !== 'undefined' && eventjsFunctor in eventjs) { - eventjs[eventjsFunctor](canvasElement, 'gesture', this._onGesture); - eventjs[eventjsFunctor](canvasElement, 'drag', this._onDrag); - eventjs[eventjsFunctor]( - canvasElement, - 'orientation', - this._onOrientationChange - ); - eventjs[eventjsFunctor](canvasElement, 'shake', this._onShake); - eventjs[eventjsFunctor]( - canvasElement, - 'longpress', - this._onLongPress - ); - } - }, - - /** - * Removes all event listeners - */ - removeListeners: function () { - this.addOrRemove(removeListener, 'remove'); - // if you dispose on a mouseDown, before mouse up, you need to clean document to... - var eventTypePrefix = this._getEventPrefix(); - removeListener( - fabric.document, - eventTypePrefix + 'up', - this._onMouseUp - ); - removeListener( - fabric.document, - 'touchend', - this._onTouchEnd, - addEventOptions - ); - removeListener( - fabric.document, - eventTypePrefix + 'move', - this._onMouseMove, - addEventOptions - ); - removeListener( - fabric.document, - 'touchmove', - this._onMouseMove, - addEventOptions - ); - }, - - /** - * @private - */ - _bindEvents: function () { - if (this.eventsBound) { - // for any reason we pass here twice we do not want to bind events twice. - return; - } - this._onMouseDown = this._onMouseDown.bind(this); - this._onTouchStart = this._onTouchStart.bind(this); - this._onMouseMove = this._onMouseMove.bind(this); - this._onMouseUp = this._onMouseUp.bind(this); - this._onTouchEnd = this._onTouchEnd.bind(this); - this._onResize = this._onResize.bind(this); - this._onGesture = this._onGesture.bind(this); - this._onDrag = this._onDrag.bind(this); - this._onShake = this._onShake.bind(this); - this._onLongPress = this._onLongPress.bind(this); - this._onOrientationChange = this._onOrientationChange.bind(this); - this._onMouseWheel = this._onMouseWheel.bind(this); - this._onMouseOut = this._onMouseOut.bind(this); - this._onMouseEnter = this._onMouseEnter.bind(this); - this._onContextMenu = this._onContextMenu.bind(this); - this._onDoubleClick = this._onDoubleClick.bind(this); - this._onDragStart = this._onDragStart.bind(this); - this._onDragEnd = this._onDragEnd.bind(this); - this._onDragProgress = this._onDragProgress.bind(this); - this._onDragOver = this._onDragOver.bind(this); - this._onDragEnter = this._onDragEnter.bind(this); - this._onDragLeave = this._onDragLeave.bind(this); - this._onDrop = this._onDrop.bind(this); - this.eventsBound = true; - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js gesture - * @param {Event} [self] Inner Event object - */ - _onGesture: function (e, self) { - this.__onTransformGesture && this.__onTransformGesture(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js drag - * @param {Event} [self] Inner Event object - */ - _onDrag: function (e, self) { - this.__onDrag && this.__onDrag(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on wheel event - */ - _onMouseWheel: function (e) { - this.__onMouseWheel(e); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onMouseOut: function (e) { - var target = this._hoveredTarget; - this.fire('mouse:out', { target: target, e: e }); - this._hoveredTarget = null; - target && target.fire('mouseout', { e: e }); - - this._hoveredTargets.forEach(function (nestedTarget) { - this.fire('mouse:out', { target: nestedTarget, e: e }); - nestedTarget && nestedTarget.fire('mouseout', { e: e }); - }, this); - this._hoveredTargets = []; - }, - - /** - * @private - * @param {Event} e Event object fired on mouseenter - */ - _onMouseEnter: function (e) { - // This find target and consequent 'mouse:over' is used to - // clear old instances on hovered target. - // calling findTarget has the side effect of killing target.__corner. - // as a short term fix we are not firing this if we are currently transforming. - // as a long term fix we need to separate the action of finding a target with the - // side effects we added to it. - if (!this._currentTransform && !this.findTarget(e)) { - this.fire('mouse:over', { target: null, e: e }); - this._hoveredTarget = null; - this._hoveredTargets = []; - } - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js orientation change - * @param {Event} [self] Inner Event object - */ - _onOrientationChange: function (e, self) { - this.__onOrientationChange && this.__onOrientationChange(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js shake - * @param {Event} [self] Inner Event object - */ - _onShake: function (e, self) { - this.__onShake && this.__onShake(e, self); - }, - - /** - * @private - * @param {Event} [e] Event object fired on Event.js shake - * @param {Event} [self] Inner Event object - */ - _onLongPress: function (e, self) { - this.__onLongPress && this.__onLongPress(e, self); - }, - - /** - * supports native like text dragging - * @private - * @param {DragEvent} e - */ - _onDragStart: function (e) { - var activeObject = this.getActiveObject(); - if ( - activeObject && - typeof activeObject.onDragStart === 'function' && - activeObject.onDragStart(e) - ) { - this._dragSource = activeObject; - var options = { e: e, target: activeObject }; - this.fire('dragstart', options); - activeObject.fire('dragstart', options); - addListener(this.upperCanvasEl, 'drag', this._onDragProgress); - return; - } - stopEvent(e); - }, - - /** - * @private - */ - _renderDragEffects: function (e, source, target) { - var ctx = this.contextTop; - if (source) { - source.clearContextTop(true); - source.renderDragSourceEffect(e); - } - if (target) { - if (target !== source) { - ctx.restore(); - ctx.save(); - target.clearContextTop(true); - } - target.renderDropTargetEffect(e); - } - ctx.restore(); - }, - - /** - * 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: function (e) { - var didDrop = e.dataTransfer.dropEffect !== 'none', - dropTarget = didDrop ? this._activeObject : undefined, - options = { - e: e, - target: this._dragSource, - subTargets: this.targets, - dragSource: this._dragSource, - didDrop: didDrop, - dropTarget: dropTarget, - }; - removeListener(this.upperCanvasEl, 'drag', this._onDragProgress); - this.fire('dragend', options); - this._dragSource && this._dragSource.fire('dragend', options); - delete this._dragSource; - // we need to call mouse up synthetically because the browser won't - this._onMouseUp(e); - }, - - /** - * fire `drag` event on canvas and drag source - * @private - * @param {DragEvent} e - */ - _onDragProgress: function (e) { - var options = { - e: e, - 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 {Event} [e] Event object fired on Event.js shake - */ - _onDragOver: function (e) { - var eventType = 'dragover', - target = this.findTarget(e), - targets = this.targets, - options = { - e: e, - target: target, - subTargets: targets, - dragSource: this._dragSource, - canDrop: false, - dropTarget: undefined, - }, - dropTarget; - // fire on canvas - this.fire(eventType, options); - // make sure we fire dragenter events before dragover - // if dragleave is needed, object will not fire dragover so we don't need to trouble ourselves with it - this._fireEnterLeaveEvents(target, options); - if (target) { - // render drag selection before rendering target cursor for correct visuals - if (target.canDrop(e)) { - dropTarget = target; - } - target.fire(eventType, options); - } - // propagate the event to subtargets - for (var i = 0; i < targets.length; i++) { - target = targets[i]; - // accept event only if previous targets didn't - if (!e.defaultPrevented && target.canDrop(e)) { - dropTarget = target; - } - target.fire(eventType, options); - } - // render drag effects now that relations between source and target is clear - this._renderDragEffects(e, this._dragSource, dropTarget); - }, - - /** - * fire `dragleave` on `dragover` targets - * @private - * @param {Event} [e] Event object fired on Event.js shake - */ - _onDragEnter: function (e) { - var target = this.findTarget(e); - var options = { - e: e, - target: target, - subTargets: this.targets, - dragSource: this._dragSource, - }; - this.fire('dragenter', options); - // fire dragenter on targets - this._fireEnterLeaveEvents(target, options); - }, - - /** - * fire `dragleave` on `dragover` targets - * @private - * @param {Event} [e] Event object fired on Event.js shake - */ - _onDragLeave: function (e) { - var options = { - e: e, - target: this._draggedoverTarget, - subTargets: this.targets, - dragSource: this._dragSource, - }; - this.fire('dragleave', options); - // fire dragleave on targets - this._fireEnterLeaveEvents(null, options); - // clear targets - this.targets = []; - 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: function (e) { - var options = this._simpleEventHandler('drop:before', e, { - dragSource: this._dragSource, - pointer: this.getPointer(e), - }); - // will be set by the drop target - options.didDrop = false; - // will be set by the drop target, used in case options.target refuses the drop - options.dropTarget = undefined; - // fire `drop` - this._basicEventHandler('drop', options); - // inform canvas of the drop - // we do this because canvas was unaware of what happened at the time the `drop` event was fired on it - // use for side effects - this.fire('drop:after', options); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onContextMenu: function (e) { - var options = this._simpleEventHandler('contextmenu:before', e); - if (this.stopContextMenu) { - e.stopPropagation(); - e.preventDefault(); - } - this._basicEventHandler('contextmenu', options); - return false; - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onDoubleClick: function (e) { - this._cacheTransformEventData(e); - this._handleEvent(e, 'dblclick'); - 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: function (evt) { - var 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: function (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: function (e) { - e.preventDefault(); - if (this.mainTouchId === null) { - this.mainTouchId = this.getPointerId(e); - } - this.__onMouseDown(e); - this._resetTransformEventData(); - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - addListener( - fabric.document, - 'touchend', - this._onTouchEnd, - addEventOptions - ); - addListener( - fabric.document, - 'touchmove', - this._onMouseMove, - addEventOptions - ); - // Unbind mousedown to prevent double triggers from touch devices - removeListener( - canvasElement, - eventTypePrefix + 'down', - this._onMouseDown - ); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onMouseDown: function (e) { - this.__onMouseDown(e); - this._resetTransformEventData(); - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - removeListener( - canvasElement, - eventTypePrefix + 'move', - this._onMouseMove, - addEventOptions - ); - addListener(fabric.document, eventTypePrefix + 'up', this._onMouseUp); - addListener( - fabric.document, - eventTypePrefix + 'move', - this._onMouseMove, - addEventOptions - ); - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onTouchEnd: function (e) { - if (e.touches.length > 0) { - // if there are still touches stop here - return; - } - this.__onMouseUp(e); - this._resetTransformEventData(); - this.mainTouchId = null; - var eventTypePrefix = this._getEventPrefix(); - removeListener( - fabric.document, - 'touchend', - this._onTouchEnd, - addEventOptions - ); - removeListener( - fabric.document, - 'touchmove', - this._onMouseMove, - addEventOptions - ); - var _this = this; - if (this._willAddMouseDown) { - clearTimeout(this._willAddMouseDown); - } - this._willAddMouseDown = setTimeout(function () { - // Wait 400ms before rebinding mousedown to prevent double triggers - // from touch devices - addListener( - _this.upperCanvasEl, - eventTypePrefix + 'down', - _this._onMouseDown - ); - _this._willAddMouseDown = 0; - }, 400); - }, - - /** - * @private - * @param {Event} e Event object fired on mouseup - */ - _onMouseUp: function (e) { - this.__onMouseUp(e); - this._resetTransformEventData(); - var canvasElement = this.upperCanvasEl, - eventTypePrefix = this._getEventPrefix(); - if (this._isMainEvent(e)) { - removeListener( - fabric.document, - eventTypePrefix + 'up', - this._onMouseUp - ); - removeListener( - fabric.document, - eventTypePrefix + 'move', - this._onMouseMove, - addEventOptions - ); - addListener( - canvasElement, - eventTypePrefix + 'move', - this._onMouseMove, - addEventOptions - ); - } - }, - - /** - * @private - * @param {Event} e Event object fired on mousemove - */ - _onMouseMove: function (e) { - var activeObject = this.getActiveObject(); - !this.allowTouchScrolling && - (!activeObject || !activeObject.__isDragging) && - e.preventDefault && - e.preventDefault(); - this.__onMouseMove(e); - }, - - /** - * @private - */ - _onResize: function () { - this.calcOffset(); - this._resetTransformEventData(); - }, - - /** - * Decides whether the canvas should be redrawn in mouseup and mousedown events. - * @private - * @param {Object} target - */ - _shouldRender: function (target) { - var activeObject = this._activeObject; - - if ( - !!activeObject !== !!target || - (activeObject && target && activeObject !== target) - ) { - // this covers: switch of target, from target to no target, selection of target - // multiSelection with key and mouse - return true; - } else if (activeObject && activeObject.isEditing) { - // if we mouse up/down over a editing textbox a cursor change, - // there is no need to re render - return false; - } - return false; - }, - - /** - * 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: function (e) { - var target, - transform = this._currentTransform, - groupSelector = this._groupSelector, - shouldRender = false, - isClick = - !groupSelector || - (groupSelector.left === 0 && groupSelector.top === 0); - this._cacheTransformEventData(e); - target = this._target; - this._handleEvent(e, 'up:before'); - // if right/middle click just fire events and return - // target undefined will make the _handleEvent search the target - if (checkClick(e, RIGHT_CLICK)) { - if (this.fireRightClick) { - this._handleEvent(e, 'up', RIGHT_CLICK, isClick); - } - return; - } - - if (checkClick(e, MIDDLE_CLICK)) { - if (this.fireMiddleClick) { - this._handleEvent(e, 'up', MIDDLE_CLICK, isClick); - } - this._resetTransformEventData(); - return; - } - - if (this.isDrawingMode && this._isCurrentlyDrawing) { - this._onMouseUpInDrawingMode(e); - return; - } - - if (!this._isMainEvent(e)) { - return; - } - if (transform) { - this._finalizeCurrentTransform(e); - shouldRender = transform.actionPerformed; - } - if (!isClick) { - var targetWasActive = target === this._activeObject; - this._maybeGroupObjects(e); - if (!shouldRender) { - shouldRender = - this._shouldRender(target) || - (!targetWasActive && target === this._activeObject); - } - } - var corner, pointer; - if (target) { - corner = target._findTargetCorner( - this.getPointer(e, true), - fabric.util.isTouchEvent(e) - ); - if ( - target.selectable && - target !== this._activeObject && - target.activeOn === 'up' - ) { - this.setActiveObject(target, e); - shouldRender = true; - } else { - var control = target.controls[corner], - mouseUpHandler = - control && control.getMouseUpHandler(e, target, control); - if (mouseUpHandler) { - pointer = this.getPointer(e); - mouseUpHandler(e, transform, pointer.x, pointer.y); - } - } - target.isMoving = false; - } - // if we are ending up a transform on a different control or a new object - // fire the original mouse up from the corner that started the transform - if ( - transform && - (transform.target !== target || transform.corner !== corner) - ) { - var originalControl = - transform.target && transform.target.controls[transform.corner], - originalMouseUpHandler = - originalControl && - originalControl.getMouseUpHandler(e, target, control); - pointer = pointer || this.getPointer(e); - originalMouseUpHandler && - originalMouseUpHandler(e, transform, pointer.x, pointer.y); - } - this._setCursorFromEvent(e, target); - this._handleEvent(e, 'up', LEFT_CLICK, isClick); - this._groupSelector = null; - this._currentTransform = null; - // reset the target information about which corner is selected - target && (target.__corner = 0); - if (shouldRender) { - this.requestRenderAll(); - } else if (!isClick) { - this.renderTop(); - } - }, - - /** - * @private - * Handle event firing for target and subtargets - * @param {Event} e event from mouse - * @param {String} eventType event to fire (up, down or move) - * @param {object} [data] event data overrides - * @return {object} options - */ - _simpleEventHandler: function (eventType, e, data) { - var target = this.findTarget(e), - subTargets = this.targets || []; - return this._basicEventHandler( - eventType, - Object.assign( - {}, - { - e: e, - target: target, - subTargets: subTargets, - }, - data - ) - ); - }, - - _basicEventHandler: function (eventType, options) { - var target = options.target, - subTargets = options.subTargets; - this.fire(eventType, options); - target && target.fire(eventType, options); - for (var i = 0; i < subTargets.length; i++) { - subTargets[i].fire(eventType, options); - } - return options; - }, - - /** - * @private - * Handle event firing for target and subtargets - * @param {Event} e event from mouse - * @param {String} eventType event to fire (up, down or move) - * @param {fabric.Object} targetObj receiving event - * @param {Number} [button] button used in the event 1 = left, 2 = middle, 3 = right - * @param {Boolean} isClick for left button only, indicates that the mouse up happened without move. - */ - _handleEvent: function (e, eventType, button, isClick) { - var target = this._target, - targets = this.targets || [], - options = { - e: e, - target: target, - subTargets: targets, - button: button || LEFT_CLICK, - isClick: isClick || false, - pointer: this._pointer, - absolutePointer: this._absolutePointer, - transform: this._currentTransform, - }; - if (eventType === 'up') { - options.currentTarget = this.findTarget(e); - options.currentSubTargets = this.targets; - } - this.fire('mouse:' + eventType, options); - target && target.fire('mouse' + eventType, options); - for (var i = 0; i < targets.length; i++) { - targets[i].fire('mouse' + eventType, options); - } - }, - - /** - * End the current transfrom. - * You don't usually need to call this method unless you are interupting a user initiated transform - * because of some other event ( a press of key combination, or something that block the user UX ) - * @param {Event} [e] send the mouse event that generate the finalize down, so it can be used in the event - */ - endCurrentTransform: function (e) { - var transform = this._currentTransform; - this._finalizeCurrentTransform(e); - if (transform && transform.target) { - // this could probably go inside _finalizeCurrentTransform - transform.target.isMoving = false; - } - this._currentTransform = null; - }, - - /** - * @private - * @param {Event} e send the mouse event that generate the finalize down, so it can be used in the event - */ - _finalizeCurrentTransform: function (e) { - var transform = this._currentTransform, - target = transform.target, - options = { - e: e, - target: target, - transform: transform, - action: transform.action, - }; - - if (target._scaling) { - target._scaling = false; - } - - target.setCoords(); - - if ( - transform.actionPerformed || - (this.stateful && target.hasStateChanged()) - ) { - this._fire('modified', options); - } - }, - - /** - * @private - * @param {Event} e Event object fired on mousedown - */ - _onMouseDownInDrawingMode: function (e) { - this._isCurrentlyDrawing = true; - if (this.getActiveObject()) { - this.discardActiveObject(e).requestRenderAll(); - } - var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseDown(pointer, { e: e, pointer: pointer }); - this._handleEvent(e, 'down'); - }, - - /** - * @private - * @param {Event} e Event object fired on mousemove - */ - _onMouseMoveInDrawingMode: function (e) { - if (this._isCurrentlyDrawing) { - var pointer = this.getPointer(e); - this.freeDrawingBrush.onMouseMove(pointer, { - e: e, - pointer: pointer, - }); - } - this.setCursor(this.freeDrawingCursor); - this._handleEvent(e, 'move'); - }, - - /** - * @private - * @param {Event} e Event object fired on mouseup - */ - _onMouseUpInDrawingMode: function (e) { - var pointer = this.getPointer(e); - this._isCurrentlyDrawing = this.freeDrawingBrush.onMouseUp({ - e: e, - pointer: pointer, - }); - 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: function (e) { - this._cacheTransformEventData(e); - this._handleEvent(e, 'down:before'); - var target = this._target; - // if right click just fire events - if (checkClick(e, RIGHT_CLICK)) { - if (this.fireRightClick) { - this._handleEvent(e, 'down', RIGHT_CLICK); - } - return; - } - - if (checkClick(e, MIDDLE_CLICK)) { - if (this.fireMiddleClick) { - this._handleEvent(e, 'down', MIDDLE_CLICK); - } - return; - } - - if (this.isDrawingMode) { - this._onMouseDownInDrawingMode(e); - return; - } - - if (!this._isMainEvent(e)) { - return; - } - - // ignore if some object is being transformed at this moment - if (this._currentTransform) { - return; - } - - var pointer = this._pointer; - // save pointer for check in __onMouseUp event - this._previousPointer = pointer; - var shouldRender = this._shouldRender(target), - shouldGroup = this._shouldGroup(e, target); - if (this._shouldClearSelection(e, target)) { - this.discardActiveObject(e); - } else if (shouldGroup) { - this._handleGrouping(e, target); - target = this._activeObject; - } - - if ( - this.selection && - (!target || - (!target.selectable && - !target.isEditing && - target !== this._activeObject)) - ) { - this._groupSelector = { - ex: this._absolutePointer.x, - ey: this._absolutePointer.y, - top: 0, - left: 0, - }; - } - - if (target) { - var alreadySelected = target === this._activeObject; - if (target.selectable && target.activeOn === 'down') { - this.setActiveObject(target, e); - } - var corner = target._findTargetCorner( - this.getPointer(e, true), - fabric.util.isTouchEvent(e) - ); - target.__corner = corner; - if (target === this._activeObject && (corner || !shouldGroup)) { - this._setupCurrentTransform(e, target, alreadySelected); - var control = target.controls[corner], - pointer = this.getPointer(e), - mouseDownHandler = - control && control.getMouseDownHandler(e, target, control); - if (mouseDownHandler) { - mouseDownHandler(e, this._currentTransform, pointer.x, pointer.y); - } - } - } - var invalidate = shouldRender || shouldGroup; - // we clear `_objectsToRender` in case of a change in order to repopulate it at rendering - // run before firing the `down` event to give the dev a chance to populate it themselves - invalidate && (this._objectsToRender = undefined); - this._handleEvent(e, 'down'); - // we must renderAll so that we update the visuals - invalidate && this.requestRenderAll(); - }, - - /** - * reset cache form common information needed during event processing - * @private - */ - _resetTransformEventData: function () { - this._target = null; - this._pointer = null; - this._absolutePointer = null; - }, - - /** - * Cache common information needed during event processing - * @private - * @param {Event} e Event object fired on event - */ - _cacheTransformEventData: function (e) { - // reset in order to avoid stale caching - this._resetTransformEventData(); - this._pointer = this.getPointer(e, true); - this._absolutePointer = this.restorePointerVpt(this._pointer); - this._target = this._currentTransform - ? this._currentTransform.target - : this.findTarget(e) || null; - }, - - /** - * @private - */ - _beforeTransform: function (e) { - var t = this._currentTransform; - this.stateful && t.target.saveState(); - this.fire('before:transform', { - e: e, - transform: t, - }); - }, - - /** - * 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: function (e) { - this._handleEvent(e, 'move:before'); - this._cacheTransformEventData(e); - var target, pointer; - - if (this.isDrawingMode) { - this._onMouseMoveInDrawingMode(e); - return; - } - - if (!this._isMainEvent(e)) { - return; - } - - var groupSelector = this._groupSelector; - - // We initially clicked in an empty area, so we draw a box for multiple selection - if (groupSelector) { - pointer = this._absolutePointer; - - groupSelector.left = pointer.x - groupSelector.ex; - groupSelector.top = pointer.y - groupSelector.ey; - - this.renderTop(); - } else if (!this._currentTransform) { - target = this.findTarget(e) || null; - this._setCursorFromEvent(e, target); - this._fireOverOutEvents(target, e); - } else { - this._transformObject(e); - } - this._handleEvent(e, 'move'); - this._resetTransformEventData(); - }, - - /** - * 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: function (target, e) { - var _hoveredTarget = this._hoveredTarget, - _hoveredTargets = this._hoveredTargets, - targets = this.targets, - length = Math.max(_hoveredTargets.length, targets.length); - - this.fireSyntheticInOutEvents( - target, - { e: e }, - { - oldTarget: _hoveredTarget, - evtOut: 'mouseout', - canvasEvtOut: 'mouse:out', - evtIn: 'mouseover', - canvasEvtIn: 'mouse:over', - } - ); - for (var i = 0; i < length; i++) { - this.fireSyntheticInOutEvents( - targets[i], - { e: e }, - { - oldTarget: _hoveredTargets[i], - evtOut: 'mouseout', - evtIn: 'mouseover', - } - ); - } - this._hoveredTarget = target; - this._hoveredTargets = this.targets.concat(); - }, - - /** - * 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: function (target, data) { - var _draggedoverTarget = this._draggedoverTarget, - _hoveredTargets = this._hoveredTargets, - targets = this.targets, - length = Math.max(_hoveredTargets.length, targets.length); - - this.fireSyntheticInOutEvents(target, data, { - oldTarget: _draggedoverTarget, - evtOut: 'dragleave', - evtIn: 'dragenter', - canvasEvtIn: 'drag:enter', - canvasEvtOut: 'drag:leave', - }); - for (var i = 0; i < length; i++) { - this.fireSyntheticInOutEvents(targets[i], data, { - oldTarget: _hoveredTargets[i], - evtOut: 'dragleave', - evtIn: 'dragenter', - }); - } - 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: function (target, data, config) { - var inOpt, - outOpt, - oldTarget = config.oldTarget, - outFires, - inFires, - targetChanged = oldTarget !== target, - canvasEvtIn = config.canvasEvtIn, - canvasEvtOut = config.canvasEvtOut; - if (targetChanged) { - inOpt = Object.assign({}, data, { - target: target, - previousTarget: oldTarget, - }); - outOpt = Object.assign({}, data, { - target: oldTarget, - nextTarget: target, - }); - } - inFires = target && targetChanged; - outFires = oldTarget && targetChanged; - if (outFires) { - canvasEvtOut && this.fire(canvasEvtOut, outOpt); - oldTarget.fire(config.evtOut, outOpt); - } - if (inFires) { - canvasEvtIn && this.fire(canvasEvtIn, inOpt); - target.fire(config.evtIn, inOpt); - } - }, - - /** - * Method that defines actions when an Event Mouse Wheel - * @param {Event} e Event object fired on mouseup - */ - __onMouseWheel: function (e) { - this._cacheTransformEventData(e); - this._handleEvent(e, 'wheel'); - this._resetTransformEventData(); - }, - - /** - * @private - * @param {Event} e Event fired on mousemove - */ - _transformObject: function (e) { - var pointer = this.getPointer(e), - transform = this._currentTransform, - target = transform.target, - // transform pointer to target's containing coordinate plane - // both pointer and object should agree on every point - localPointer = target.group - ? fabric.util.sendPointToPlane( - pointer, - undefined, - target.group.calcTransformMatrix() - ) - : pointer; - - transform.reset = false; - transform.shiftKey = e.shiftKey; - transform.altKey = e[this.centeredKey]; - - this._performTransformAction(e, transform, localPointer); - transform.actionPerformed && this.requestRenderAll(); - }, - - /** - * @private - */ - _performTransformAction: function (e, transform, pointer) { - var x = pointer.x, - y = pointer.y, - action = transform.action, - actionPerformed = false, - actionHandler = transform.actionHandler; - // this object could be created from the function in the control handlers - - if (actionHandler) { - actionPerformed = actionHandler(e, transform, x, y); - } - if (action === 'drag' && actionPerformed) { - transform.target.isMoving = true; - this.setCursor(transform.target.moveCursor || this.moveCursor); - } - transform.actionPerformed = - transform.actionPerformed || actionPerformed; - }, - - /** - * @private - */ - _fire: function (eventName, options) { - return fireEvent(eventName, options); - }, - - /** - * 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: function (e, target) { - if (!target) { - this.setCursor(this.defaultCursor); - return false; - } - var hoverCursor = target.hoverCursor || this.hoverCursor, - activeSelection = - this._activeObject && this._activeObject.type === 'activeSelection' - ? this._activeObject - : null, - // only show proper corner when group selection is not active - corner = - (!activeSelection || !activeSelection.contains(target)) && - // here we call findTargetCorner always with undefined for the touch parameter. - // we assume that if you are using a cursor you do not need to interact with - // the bigger touch area. - target._findTargetCorner(this.getPointer(e, true)); - - if (!corner) { - if (target.subTargetCheck) { - // hoverCursor should come from top-most subTarget, - // so we walk the array backwards - this.targets - .concat() - .reverse() - .map(function (_target) { - hoverCursor = _target.hoverCursor || hoverCursor; - }); - } - this.setCursor(hoverCursor); - } else { - this.setCursor(this.getCornerCursor(corner, target, e)); - } - }, - - /** - * @private - */ - getCornerCursor: function (corner, target, e) { - var control = target.controls[corner]; - return control.cursorStyleHandler(e, control, target); - }, - } - ); -})(typeof exports !== 'undefined' ? exports : window); diff --git a/src/mixins/canvas_grouping.mixin.ts b/src/mixins/canvas_grouping.mixin.ts deleted file mode 100644 index 5900838fbed..00000000000 --- a/src/mixins/canvas_grouping.mixin.ts +++ /dev/null @@ -1,213 +0,0 @@ -//@ts-nocheck -import { Point } from '../point.class'; - -(function (global) { - var fabric = global.fabric, - min = Math.min, - max = Math.max; - - fabric.util.object.extend( - fabric.Canvas.prototype, - /** @lends fabric.Canvas.prototype */ { - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - * @return {Boolean} - */ - _shouldGroup: function (e, target) { - var activeObject = this._activeObject; - // check if an active object exists on canvas and if the user is pressing the `selectionKey` while canvas supports multi selection. - return ( - !!activeObject && - this._isSelectionKeyPressed(e) && - this.selection && - // on top of that the user also has to hit a target that is selectable. - !!target && - target.selectable && - // if all pre-requisite pass, the target is either something different from the current - // activeObject or if an activeSelection already exists - // TODO at time of writing why `activeObject.type === 'activeSelection'` matter is unclear. - // is a very old condition uncertain if still valid. - (activeObject !== target || - activeObject.type === 'activeSelection') && - // make sure `activeObject` and `target` aren't ancestors of each other - !target.isDescendantOf(activeObject) && - !activeObject.isDescendantOf(target) && - // target accepts selection - !target.onSelect({ e: e }) - ); - }, - - /** - * @private - * @param {Event} e Event object - * @param {fabric.Object} target - */ - _handleGrouping: function (e, target) { - var activeObject = this._activeObject; - // avoid multi select when shift click on a corner - if (activeObject.__corner) { - return; - } - if (target === activeObject) { - // if it's a group, find target again, using activeGroup objects - target = this.findTarget(e, true); - // if even object is not found or we are on activeObjectCorner, bail out - if (!target || !target.selectable) { - return; - } - } - if (activeObject && activeObject.type === 'activeSelection') { - this._updateActiveSelection(target, e); - } else { - this._createActiveSelection(target, e); - } - }, - - /** - * @private - */ - _updateActiveSelection: function (target, e) { - var activeSelection = this._activeObject, - currentActiveObjects = activeSelection._objects.slice(0); - if (target.group === activeSelection) { - activeSelection.remove(target); - this._hoveredTarget = target; - this._hoveredTargets = this.targets.concat(); - if (activeSelection.size() === 1) { - // activate last remaining object - this._setActiveObject(activeSelection.item(0), e); - } - } else { - activeSelection.add(target); - this._hoveredTarget = activeSelection; - this._hoveredTargets = this.targets.concat(); - } - this._fireSelectionEvents(currentActiveObjects, e); - }, - - /** - * @private - */ - _createActiveSelection: function (target, e) { - var currentActives = this.getActiveObjects(), - group = this._createGroup(target); - this._hoveredTarget = group; - // ISSUE 4115: should we consider subTargets here? - // this._hoveredTargets = []; - // this._hoveredTargets = this.targets.concat(); - this._setActiveObject(group, e); - this._fireSelectionEvents(currentActives, e); - }, - - /** - * @private - * @param {Object} target - * @returns {fabric.ActiveSelection} - */ - _createGroup: function (target) { - var activeObject = this._activeObject; - var groupObjects = target.isInFrontOf(activeObject) - ? [activeObject, target] - : [target, activeObject]; - activeObject.isEditing && activeObject.exitEditing(); - // handle case: target is nested - return new fabric.ActiveSelection(groupObjects, { - canvas: this, - }); - }, - - /** - * @private - * @param {Event} e mouse event - */ - _groupSelectedObjects: function (e) { - var group = this._collectObjects(e), - aGroup; - - // do not create group for 1 element only - if (group.length === 1) { - this.setActiveObject(group[0], e); - } else if (group.length > 1) { - aGroup = new fabric.ActiveSelection(group.reverse(), { - canvas: this, - }); - this.setActiveObject(aGroup, e); - } - }, - - /** - * @private - */ - _collectObjects: function (e) { - var group = [], - currentObject, - x1 = this._groupSelector.ex, - y1 = this._groupSelector.ey, - x2 = x1 + this._groupSelector.left, - y2 = y1 + this._groupSelector.top, - selectionX1Y1 = new Point(min(x1, x2), min(y1, y2)), - selectionX2Y2 = new Point(max(x1, x2), max(y1, y2)), - allowIntersect = !this.selectionFullyContained, - isClick = x1 === x2 && y1 === y2; - // we iterate reverse order to collect top first in case of click. - for (var i = this._objects.length; i--; ) { - currentObject = this._objects[i]; - - if ( - !currentObject || - !currentObject.selectable || - !currentObject.visible - ) { - continue; - } - - if ( - (allowIntersect && - currentObject.intersectsWithRect( - selectionX1Y1, - selectionX2Y2, - true - )) || - currentObject.isContainedWithinRect( - selectionX1Y1, - selectionX2Y2, - true - ) || - (allowIntersect && - currentObject.containsPoint(selectionX1Y1, null, true)) || - (allowIntersect && - currentObject.containsPoint(selectionX2Y2, null, true)) - ) { - group.push(currentObject); - // only add one object if it's a click - if (isClick) { - break; - } - } - } - - if (group.length > 1) { - group = group.filter(function (object) { - return !object.onSelect({ e: e }); - }); - } - - return group; - }, - - /** - * @private - */ - _maybeGroupObjects: function (e) { - if (this.selection && this._groupSelector) { - this._groupSelectedObjects(e); - } - this.setCursor(this.defaultCursor); - // clear selection and current transformation - this._groupSelector = null; - }, - } - ); -})(typeof exports !== 'undefined' ? exports : window); diff --git a/src/mixins/itext_behavior.mixin.ts b/src/mixins/itext_behavior.mixin.ts index 4e6c2600080..e0b81f72c7b 100644 --- a/src/mixins/itext_behavior.mixin.ts +++ b/src/mixins/itext_behavior.mixin.ts @@ -9,7 +9,7 @@ import { setStyle } from '../util/dom_style'; import { removeFromArray } from '../util/internals'; import { createCanvasElement } from '../util/misc/dom'; import { transformPoint } from '../util/misc/matrix'; -import { Canvas } from '../__types__'; +import type { Canvas } from '../canvas/canvas_events'; import { TextStyleDeclaration } from './text_style.mixin'; // extend this regex to support non english languages @@ -65,7 +65,7 @@ export abstract class ITextBehaviorMixin< selectable: boolean; hoverCursor: string | null; defaultCursor: string; - moveCursor: string; + moveCursor: CSSStyleDeclaration['cursor']; }; protected _selectionDirection: 'left' | 'right' | null; diff --git a/src/shapes/Object/InteractiveObject.ts b/src/shapes/Object/InteractiveObject.ts index 9cc49428cac..475146f04e5 100644 --- a/src/shapes/Object/InteractiveObject.ts +++ b/src/shapes/Object/InteractiveObject.ts @@ -12,6 +12,7 @@ import { ObjectGeometry } from './ObjectGeometry'; import type { Control } from '../../controls/control.class'; import { sizeAfterTransform } from '../../util/misc/objectTransforms'; import { ObjectEvents, TPointerEvent } from '../../EventTypeDefs'; +import type { Canvas } from '../../canvas/canvas_events'; type TOCoord = IPoint & { corner: TCornerPoint; @@ -20,6 +21,10 @@ type TOCoord = IPoint & { type TControlSet = Record; +export type FabricObjectWithDragSupport = InteractiveFabricObject & { + onDragStart: (e: DragEvent) => boolean; +}; + export class InteractiveFabricObject< EventSpec extends ObjectEvents = ObjectEvents > extends FabricObject { @@ -73,10 +78,31 @@ export class InteractiveFabricObject< /** * internal boolean to signal the code that the object is - * part of the drag action. + * part of the move action. */ isMoving?: boolean; + /** + * internal boolean to signal the code that the object is + * part of the draggin action. + * @TODO: discuss isMoving and isDragging being not adequate enough + * they need to be either both private or more generic + * Canvas class needs to see this variable + */ + __isDragging?: boolean; + + /** + * A boolean used from the gesture module to keep tracking of a scaling + * action when there is no scaling transform in place. + * This is an edge case and is used twice in all codebase. + * Probably added to keep track of some performance issues + * @TODO use git blame to investigate why it was added + * DON'T USE IT. WE WILL TRY TO REMOVE IT + */ + _scaling?: boolean; + + declare canvas?: Canvas; + /** * Constructor * @param {Object} [options] Options object @@ -92,13 +118,13 @@ export class InteractiveFabricObject< * @param {boolean} forTouch indicates if we are looking for interaction area with a touch action * @return {String|Boolean} corner code (tl, tr, bl, br, etc.), or false if nothing is found */ - _findTargetCorner(pointer: Point, forTouch: boolean): false | string { + _findTargetCorner(pointer: Point, forTouch = false): 0 | string { if ( !this.hasControls || !this.canvas || - this.canvas._activeObject !== this + (this.canvas._activeObject as InteractiveFabricObject) !== this ) { - return false; + return 0; } this.__corner = 0; @@ -131,7 +157,7 @@ export class InteractiveFabricObject< // this.canvas.contextTop.fillRect(lines.rightline.d.x, lines.rightline.d.y, 2, 2); // this.canvas.contextTop.fillRect(lines.rightline.o.x, lines.rightline.o.y, 2, 2); } - return false; + return 0; } /** @@ -263,7 +289,8 @@ export class InteractiveFabricObject< if ( !this.selectionBackgroundColor || (this.canvas && !this.canvas.interactive) || - (this.canvas && this.canvas._activeObject !== this) + (this.canvas && + (this.canvas._activeObject as InteractiveFabricObject) !== this) ) { return; } @@ -558,7 +585,7 @@ export class InteractiveFabricObject< * @param {DragEvent} e * @returns {boolean} */ - renderDragSourceEffect() { + renderDragSourceEffect(e: DragEvent) { // for subclasses } diff --git a/src/shapes/Object/Object.ts b/src/shapes/Object/Object.ts index 7c345407222..d55cb7aca2b 100644 --- a/src/shapes/Object/Object.ts +++ b/src/shapes/Object/Object.ts @@ -115,17 +115,17 @@ export class FabricObject< /** * Default cursor value used when hovering over this object on canvas - * @type String + * @type CSSStyleDeclaration['cursor'] | null * @default null */ - hoverCursor: null; + hoverCursor: CSSStyleDeclaration['cursor'] | null; /** * Default cursor value used when moving this object on canvas - * @type String + * @type CSSStyleDeclaration['cursor'] | null * @default null */ - moveCursor: null; + moveCursor: CSSStyleDeclaration['cursor'] | null; /** * Color of controlling borders of an object (when it's active) diff --git a/src/shapes/Object/ObjectGeometry.ts b/src/shapes/Object/ObjectGeometry.ts index 27fae71206b..2062b5959bb 100644 --- a/src/shapes/Object/ObjectGeometry.ts +++ b/src/shapes/Object/ObjectGeometry.ts @@ -21,7 +21,8 @@ import { } from '../../util/misc/matrix'; import { degreesToRadians } from '../../util/misc/radiansDegreesConversion'; import { sin } from '../../util/misc/sin'; -import { Canvas, StaticCanvas } from '../../__types__'; +import type { Canvas } from '../../canvas/canvas_events'; +import type { StaticCanvas } from '../../canvas/static_canvas.class'; import { ObjectOrigin } from './ObjectOrigin'; import { ObjectEvents } from '../../EventTypeDefs'; @@ -271,7 +272,7 @@ export class ObjectGeometry< pointTL: Point, pointBR: Point, absolute: boolean, - calculate: boolean + calculate?: boolean ): boolean { const coords = this.getCoords(absolute, calculate), intersection = Intersection.intersectPolygonRectangle( @@ -342,7 +343,7 @@ export class ObjectGeometry< pointTL: Point, pointBR: Point, absolute: boolean, - calculate: boolean + calculate?: boolean ): boolean { const boundingRect = this.getBoundingRect(absolute, calculate); return ( diff --git a/src/shapes/Object/StackedObject.ts b/src/shapes/Object/StackedObject.ts index 1ab5cda5d4d..6b3fd54fd52 100644 --- a/src/shapes/Object/StackedObject.ts +++ b/src/shapes/Object/StackedObject.ts @@ -1,7 +1,8 @@ import { fabric } from '../../../HEADER'; import { ObjectEvents } from '../../EventTypeDefs'; import type { Group } from '../group.class'; -import type { Canvas, StaticCanvas } from '../../__types__'; +import type { Canvas } from '../../canvas/canvas_events'; +import type { StaticCanvas } from '../../canvas/static_canvas.class'; import { ObjectGeometry } from './ObjectGeometry'; type TAncestor = StackedObject | Canvas | StaticCanvas; @@ -52,7 +53,7 @@ export class StackedObject< // happens after all parents were traversed through without a match return false; } - parent = parent.group || parent.canvas; + parent = (parent as Group).group || (parent as Group).canvas; } return false; } @@ -67,7 +68,9 @@ export class StackedObject< let parent = this.group || (strict ? undefined : this.canvas); while (parent) { ancestors.push(parent); - parent = parent.group || (strict ? undefined : parent.canvas); + parent = + (parent as Group).group || + (strict ? undefined : (parent as Group).canvas); } return ancestors as Ancestors; } diff --git a/src/shapes/itext.class.ts b/src/shapes/itext.class.ts index fadfb268bc1..ec8cb4c738d 100644 --- a/src/shapes/itext.class.ts +++ b/src/shapes/itext.class.ts @@ -1,6 +1,6 @@ // @ts-nocheck import { fabric } from '../../HEADER'; -import { ObjectEvents, TransformEvent } from '../EventTypeDefs'; +import { ObjectEvents, TPointerEventInfo } from '../EventTypeDefs'; import { ITextClickBehaviorMixin } from '../mixins/itext_click_behavior.mixin'; import { ctrlKeysMapDown, @@ -14,7 +14,7 @@ import { TClassProperties, TFiller } from '../typedefs'; export type ITextEvents = ObjectEvents & { 'selection:changed': never; changed: never; - tripleclick: TransformEvent; + tripleclick: TPointerEventInfo; 'editing:entered': never; 'editing:exited': never; }; diff --git a/src/util/fireEvent.ts b/src/util/fireEvent.ts index 90c662beb64..1171ff1431e 100644 --- a/src/util/fireEvent.ts +++ b/src/util/fireEvent.ts @@ -1,5 +1,4 @@ -import { TModificationEvents, BasicTransformEvent } from '../EventTypeDefs'; -import { Canvas } from '../__types__'; +import { BasicTransformEvent, TModificationEvents } from '../EventTypeDefs'; export const fireEvent = ( eventName: TModificationEvents, @@ -8,7 +7,7 @@ export const fireEvent = ( const { transform: { target }, } = options; - (target.canvas as Canvas)?.fire(`object:${eventName}`, { + target.canvas?.fire(`object:${eventName}`, { ...options, target, }); diff --git a/src/util/misc/planeChange.ts b/src/util/misc/planeChange.ts index a808a47162d..8a486d5b12f 100644 --- a/src/util/misc/planeChange.ts +++ b/src/util/misc/planeChange.ts @@ -2,7 +2,7 @@ import { iMatrix } from '../../constants'; import type { Point } from '../../point.class'; import type { FabricObject } from '../../shapes/Object/FabricObject'; import type { TMat2D } from '../../typedefs'; -import { StaticCanvas } from '../../__types__'; +import type { StaticCanvas } from '../../canvas/static_canvas.class'; import { invertTransform, multiplyTransformMatrices } from './matrix'; import { applyTransformToObject } from './objectTransforms'; diff --git a/src/util/types.ts b/src/util/types.ts index c4e8ab9aa7a..a75d6313f85 100644 --- a/src/util/types.ts +++ b/src/util/types.ts @@ -4,6 +4,7 @@ import type { FabricObject, TCachedFabricObject, } from '../shapes/Object/Object'; +import type { FabricObjectWithDragSupport } from '../shapes/Object/InteractiveObject'; import type { TFiller } from '../typedefs'; import type { Text } from '../shapes/text.class'; import type { Pattern } from '../pattern.class'; @@ -23,19 +24,19 @@ export const isPattern = (filler: TFiller): filler is Pattern => { }; export const isCollection = ( - fabricObject: FabricObject | null + fabricObject?: FabricObject ): fabricObject is Group | ActiveSelection => { return !!fabricObject && Array.isArray((fabricObject as Group)._objects); }; export const isActiveSelection = ( - fabricObject: FabricObject | null + fabricObject?: FabricObject ): fabricObject is ActiveSelection => { return !!fabricObject && fabricObject.type === 'activeSelection'; }; export const isTextObject = ( - fabricObject: FabricObject + fabricObject?: FabricObject ): fabricObject is Text => { // we could use instanceof but that would mean pulling in Text code for a simple check // @todo discuss what to do and how to do @@ -43,7 +44,7 @@ export const isTextObject = ( }; export const isInteractiveTextObject = ( - fabricObject: FabricObject | null + fabricObject?: FabricObject ): fabricObject is IText | Textbox => { // we could use instanceof but that would mean pulling in Text code for a simple check // @todo discuss what to do and how to do @@ -55,3 +56,13 @@ export const isFabricObjectCached = ( ): fabricObject is TCachedFabricObject => { return fabricObject.shouldCache() && !!fabricObject._cacheCanvas; }; + +export const isFabricObjectWithDragSupport = ( + fabricObject?: FabricObject +): fabricObject is FabricObjectWithDragSupport => { + return ( + !!fabricObject && + typeof (fabricObject as FabricObjectWithDragSupport).onDragStart === + 'function' + ); +}; diff --git a/test/unit/canvas.js b/test/unit/canvas.js index 142d027d581..bdc25ef1e86 100644 --- a/test/unit/canvas.js +++ b/test/unit/canvas.js @@ -350,22 +350,57 @@ var isFired = false; var rect1 = new fabric.Rect(); var rect2 = new fabric.Rect(); + canvas.add(rect1, rect2); canvas.on('selection:created', function( ) { isFired = true; }); canvas.setActiveObject(rect1); - canvas._createActiveSelection(rect2, {}); + canvas._createActiveSelection({}, rect2); assert.equal(canvas._hoveredTarget, canvas.getActiveObject(), 'the created selection is also hovered'); assert.equal(isFired, true, 'selection:created fired'); canvas.off('selection:created'); + canvas.clear(); }); QUnit.test('create active selection fires selected on new object', function(assert) { var isFired = false; var rect1 = new fabric.Rect(); var rect2 = new fabric.Rect(); + canvas.add(rect1, rect2); rect2.on('selected', function( ) { isFired = true; }); canvas.setActiveObject(rect1); - canvas._createActiveSelection(rect2, {}); + canvas._createActiveSelection({}, rect2); + const activeSelection = canvas.getActiveObjects(); assert.equal(isFired, true, 'selected fired on rect2'); + assert.equal(activeSelection[0], rect1, 'first rec1'); + assert.equal(activeSelection[1], rect2, 'then rect2'); + canvas.clear(); + }); + + QUnit.test('create active selection selected in different order, but same result', function(assert) { + var isFired = false; + var rect1 = new fabric.Rect(); + var rect2 = new fabric.Rect(); + canvas.add(rect1, rect2); + rect2.on('selected', function( ) { isFired = true; }); + canvas.setActiveObject(rect2); + canvas._createActiveSelection({}, rect1); + const activeSelection = canvas.getActiveObjects(); + assert.equal(activeSelection[0], rect1, 'first rec1'); + assert.equal(activeSelection[1], rect2, 'then rect2'); + canvas.clear(); + }); + + QUnit.test('create active added in different order, selection selected in different order, different result', function(assert) { + var isFired = false; + var rect1 = new fabric.Rect(); + var rect2 = new fabric.Rect(); + canvas.add(rect2, rect1); + rect2.on('selected', function( ) { isFired = true; }); + canvas.setActiveObject(rect2); + canvas._createActiveSelection({}, rect1); + const activeSelection = canvas.getActiveObjects(); + assert.equal(activeSelection[1], rect1, 'then rect1'); + assert.equal(activeSelection[0], rect2, 'first rect2'); + canvas.clear(); }); QUnit.test('update active selection selection:updated', function(assert) { @@ -375,7 +410,7 @@ var rect3 = new fabric.Rect(); canvas.on('selection:updated', function( ) { isFired = true; }); canvas.setActiveObject(new fabric.ActiveSelection([rect1, rect2])); - canvas._updateActiveSelection(rect3, {}); + canvas._updateActiveSelection({}, rect3); assert.equal(isFired, true, 'selection:updated fired'); assert.equal(canvas._hoveredTarget, canvas.getActiveObject(), 'hovered target is updated'); canvas.off('selection:updated'); @@ -387,7 +422,7 @@ var rect2 = new fabric.Rect(); rect2.on('deselected', function( ) { isFired = true; }); canvas.setActiveObject(new fabric.ActiveSelection([rect1, rect2])); - canvas._updateActiveSelection(rect2, {}); + canvas._updateActiveSelection({}, rect2); assert.equal(isFired, true, 'deselected on rect2 fired'); }); @@ -398,7 +433,7 @@ var rect3 = new fabric.Rect(); rect3.on('selected', function( ) { isFired = true; }); canvas.setActiveObject(new fabric.ActiveSelection([rect1, rect2])); - canvas._updateActiveSelection(rect3, {}); + canvas._updateActiveSelection({}, rect3); assert.equal(isFired, true, 'selected on rect3 fired'); }); @@ -413,28 +448,6 @@ assert.equal(isFired, true, 'switching active group fires deselected'); }); - QUnit.test('_createGroup respect order of objects', function(assert) { - var rect1 = new fabric.Rect(); - var rect2 = new fabric.Rect(); - canvas.add(rect1); - canvas.add(rect2); - canvas.setActiveObject(rect1); - var selection = canvas._createGroup(rect2); - assert.equal(selection.getObjects().indexOf(rect1), 0, 'rect1 is the first object in the active selection'); - assert.equal(selection.getObjects().indexOf(rect2), 1, 'rect2 is the second object in the active selection'); - }); - - QUnit.test('_createGroup respect order of objects (inverted)', function(assert) { - var rect1 = new fabric.Rect(); - var rect2 = new fabric.Rect(); - canvas.add(rect1); - canvas.add(rect2); - canvas.setActiveObject(rect2); - var selection = canvas._createGroup(rect1); - assert.equal(selection.getObjects().indexOf(rect1), 0, 'rect1 is the first object in the active selection'); - assert.equal(selection.getObjects().indexOf(rect2), 1, 'rect2 is the second object in the active selection'); - }); - QUnit.test('_groupSelectedObjects fires selected for objects', function(assert) { var fired = 0; var rect1 = new fabric.Rect(); diff --git a/test/unit/canvas_events.js b/test/unit/canvas_events.js index 5ae00c1a588..80f46c45490 100644 --- a/test/unit/canvas_events.js +++ b/test/unit/canvas_events.js @@ -781,14 +781,14 @@ target.item(1).item(1), target.item(1).item(1).item(1) ]; - canvas._fireOverOutEvents(target, moveEvent); + canvas._fireOverOutEvents(moveEvent, target); assert.equal(counterOver, 4, 'mouseover fabric event fired 4 times for primary hoveredTarget & subTargets'); assert.equal(canvas._hoveredTarget, target, 'activeSelection is _hoveredTarget'); assert.equal(canvas._hoveredTargets.length, 3, '3 additional subTargets are captured as _hoveredTargets'); // perform MouseOut even on all hoveredTargets canvas.targets = []; - canvas._fireOverOutEvents(null, moveEvent); + canvas._fireOverOutEvents(moveEvent, null); assert.equal(counterOut, 4, 'mouseout fabric event fired 4 times for primary hoveredTarget & subTargets'); assert.equal(canvas._hoveredTarget, null, '_hoveredTarget has been set to null'); assert.equal(canvas._hoveredTargets.length, 0, '_hoveredTargets array is empty'); @@ -1025,127 +1025,191 @@ // assert.equal(!!canvas.actionIsDisabled('mtr', target, e), false, 'mtr action is not disabled lockSkewingX'); // }); - QUnit.test('getCornerCursor ', function(assert) { - assert.ok(typeof fabric.Canvas.prototype.getCornerCursor === 'function', 'getCornerCursor is a function'); + QUnit.test('_setCursorFromEvent ', function(assert) { var key = canvas.altActionKey; var key2 = canvas.uniScaleKey; - var target = new fabric.Object({ canvas: canvas }); - var e = { }; - e[key] = false; - assert.equal(canvas.getCornerCursor('mt', target, e), 'n-resize', 'n-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('mb', target, e), 's-resize', 's-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'w-resize', 'w-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'e-resize', 'e-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'nw-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'ne-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'sw-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'se-resize action is not disabled'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'crosshair action is not disabled'); - - target = new fabric.Object({ canvas: canvas }); + var target = new fabric.Rect({ width: 100, height: 100 }); + canvas.add(target); + canvas.setActiveObject(target); + target.setCoords(); + const expected = { + mt: 'n-resize', + mb: 's-resize', + ml: 'w-resize', + mr: 'e-resize', + tl: 'nw-resize', + tr: 'ne-resize', + bl: 'sw-resize', + br: 'se-resize', + mtr: 'crosshair', + }; + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expected[corner], `${expected[corner]} action is not disabled`); + }) + + const expectedLockScalinX = { + mt: 'n-resize', + mb: 's-resize', + ml: 'not-allowed', + mr: 'not-allowed', + tl: 'not-allowed', + tr: 'not-allowed', + bl: 'not-allowed', + br: 'not-allowed', + mtr: 'crosshair', + }; target.lockScalingX = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'n-resize', 'action is not disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('mb', target, e), 's-resize', 'action is not disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'not-allowed', 'action is disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'not-allowed', 'action is disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'not-allowed', 'action is disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'not-allowed', 'action is disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'not-allowed', 'action is disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('br', target, e), 'not-allowed', 'action is disabled lockScalingX'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'action is not disabled lockScalingX'); - e[key2] = true; - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'action is not disabled lockScalingX key2'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'action is not disabled lockScalingX key2'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'action is not disabled lockScalingX key2'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'action is not disabled lockScalingX key2'); - - var e = { }; - target = new fabric.Object({ canvas: canvas }); + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockScalinX[corner], `${corner} is ${expectedLockScalinX[corner]} for lockScalingX`); + }); + // when triggering the uniscaleKey corners are ok + const expectedUniScale = { + mt: 'ew-resize', // skewing + mb: 'ew-resize', // skewing + ml: 'ns-resize', // skewing + mr: 'ns-resize', // skewing + tl: 'nw-resize', + tr: 'ne-resize', + bl: 'sw-resize', + br: 'se-resize', + mtr: 'crosshair', + }; + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false, [key2]: true }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedUniScale[corner], `${corner} is ${expectedUniScale[corner]} for uniScaleKey pressed`); + }); + + const expectedLockScalinY = { + mt: 'not-allowed', + mb: 'not-allowed', + ml: 'w-resize', + mr: 'e-resize', + tl: 'not-allowed', + tr: 'not-allowed', + bl: 'not-allowed', + br: 'not-allowed', + mtr: 'crosshair', + }; + target.lockScalingX = false; target.lockScalingY = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'not-allowed', 'action is disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('mb', target, e), 'not-allowed', 'action is disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'w-resize', 'action is not disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'e-resize', 'action is not disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'not-allowed', 'action is disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'not-allowed', 'action is disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'not-allowed', 'action is disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('br', target, e), 'not-allowed', 'action is disabled lockScalingY'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'action is not disabled lockScalingY'); - e[key2] = true; - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'action is not disabled lockScalingY key2'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'action is not disabled lockScalingY key2'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'action is not disabled lockScalingY key2'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'action is not disabled lockScalingY key2'); - - var e = { }; - target = new fabric.Object({ canvas: canvas }); + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockScalinY[corner], `${corner} is ${expectedLockScalinY[corner]} for lockScalingY`); + }); + const expectedLockScalinYUniscaleKey = { + mt: 'ew-resize', // skewing + mb: 'ew-resize', // skewing + ml: 'ns-resize', // skewing + mr: 'ns-resize', // skewing + tl: 'nw-resize', + tr: 'ne-resize', + bl: 'sw-resize', + br: 'se-resize', + mtr: 'crosshair', + }; + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false, [key2]: true }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockScalinYUniscaleKey[corner], `${corner} is ${expectedLockScalinYUniscaleKey[corner]} for lockScalingY + uniscaleKey`); + }); + + const expectedAllLock = { + mt: 'not-allowed', + mb: 'not-allowed', + ml: 'not-allowed', + mr: 'not-allowed', + tl: 'not-allowed', + tr: 'not-allowed', + bl: 'not-allowed', + br: 'not-allowed', + mtr: 'crosshair', + }; target.lockScalingY = true; target.lockScalingX = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('mb', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('br', target, e), 'not-allowed', 'action is disabled lockScaling'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'action is not disabled lockScaling'); - e[key2] = true; - assert.equal(canvas.getCornerCursor('tl', target, e), 'not-allowed', 'action is disabled lockScaling key2'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'not-allowed', 'action is disabled lockScaling key2'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'not-allowed', 'action is disabled lockScaling key2'); - assert.equal(canvas.getCornerCursor('br', target, e), 'not-allowed', 'action is disabled lockScaling key2'); - - var e = { }; - target = new fabric.Object({ canvas: canvas }); + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedAllLock[corner], `${corner} is ${expectedAllLock[corner]} for all locked`); + }); + // when pressing uniscale key + const expectedAllLockUniscale = { + mt: 'ew-resize', // skewing + mb: 'ew-resize', // skewing + ml: 'ns-resize', // skewing + mr: 'ns-resize', // skewing + tl: 'not-allowed', + tr: 'not-allowed', + bl: 'not-allowed', + br: 'not-allowed', + mtr: 'crosshair', + }; + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false, [key2]: true }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedAllLockUniscale[corner], `${corner} is ${expectedAllLockUniscale[corner]} for all locked + uniscale`); + }); + target.lockRotation = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'n-resize', 'n-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('mb', target, e), 's-resize', 's-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'w-resize', 'w-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'e-resize', 'e-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'nw-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'ne-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'sw-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'se-resize action is not disabled lockRotation'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'not-allowed', 'mtr action is disabled lockRotation'); - - target = new fabric.Object({ canvas: canvas }); + target.lockScalingY = false; + target.lockScalingX = false; + const e = { clientX: target.oCoords.mtr.x, clientY: target.oCoords.mtr.y, [key]: false }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, 'not-allowed', `mtr is not allowed for locked rotation`); + target.lockSkewingX = true; target.lockSkewingY = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'n-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('mb', target, e), 's-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'w-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'e-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'action is not disabled'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'action is not disabled'); - - e[key] = true; - target = new fabric.Object({ canvas: canvas }); + target.lockRotation = false; + // with lock-skewing we are back at normal + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: false }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expected[corner], `${key} is ${expected[corner]} for both lockskewing`); + }); + + // trying to skew while lock skew Y target.lockSkewingY = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'ew-resize', 'lockSkewingY mt action is not disabled'); - assert.equal(canvas.getCornerCursor('mb', target, e), 'ew-resize', 'lockSkewingY mb action is not disabled'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'not-allowed', 'lockSkewingY ml action is disabled'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'not-allowed', 'lockSkewingY mr action is disabled'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'lockSkewingY tl action is not disabled'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'lockSkewingY tr action is not disabled'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'lockSkewingY bl action is not disabled'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'lockSkewingY br action is not disabled'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'lockSkewingY mtr action is not disabled'); - - e[key] = true; - target = new fabric.Object({ canvas: canvas }); + target.lockSkewingX = false; + const expectedLockSkewingY = { + mt: 'ew-resize', // skewing + mb: 'ew-resize', // skewing + ml: 'not-allowed', // skewing + mr: 'not-allowed', // skewing + tl: 'nw-resize', + tr: 'ne-resize', + bl: 'sw-resize', + br: 'se-resize', + mtr: 'crosshair', + }; + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: true }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockSkewingY[corner], `${corner} ${expectedLockSkewingY[corner]} for lockSkewingY`); + }); + + // trying to skew while lock skew X + target.lockSkewingY = false; target.lockSkewingX = true; - assert.equal(canvas.getCornerCursor('mt', target, e), 'not-allowed', 'lockSkewingX mt action is disabled'); - assert.equal(canvas.getCornerCursor('mb', target, e), 'not-allowed', 'lockSkewingX mb action is disabled'); - assert.equal(canvas.getCornerCursor('ml', target, e), 'ns-resize', 'lockSkewingX ml action is not disabled'); - assert.equal(canvas.getCornerCursor('mr', target, e), 'ns-resize', 'lockSkewingX mr action is not disabled'); - assert.equal(canvas.getCornerCursor('tl', target, e), 'nw-resize', 'lockSkewingX tl action is not disabled'); - assert.equal(canvas.getCornerCursor('tr', target, e), 'ne-resize', 'lockSkewingX tr action is not disabled'); - assert.equal(canvas.getCornerCursor('bl', target, e), 'sw-resize', 'lockSkewingX bl action is not disabled'); - assert.equal(canvas.getCornerCursor('br', target, e), 'se-resize', 'lockSkewingX br action is not disabled'); - assert.equal(canvas.getCornerCursor('mtr', target, e), 'crosshair', 'lockSkewingX mtr action is not disabled'); + const expectedLockSkewingX = { + mt: 'not-allowed', // skewing + mb: 'not-allowed', // skewing + ml: 'ns-resize', // skewing + mr: 'ns-resize', // skewing + tl: 'nw-resize', + tr: 'ne-resize', + bl: 'sw-resize', + br: 'se-resize', + mtr: 'crosshair', + }; + Object.entries(target.oCoords).forEach(([corner, coords]) => { + const e = { clientX: coords.x, clientY: coords.y, [key]: true }; + canvas._setCursorFromEvent(e, target); + assert.equal(canvas.upperCanvasEl.style.cursor, expectedLockSkewingX[corner], `${corner} is ${expectedLockSkewingX[corner]} for lockSkewingX`); + }); }); })();