Skip to content

Commit

Permalink
hovering state
Browse files Browse the repository at this point in the history
cleanup

Update canvas_events.js

hovering state
  • Loading branch information
ShaMan123 committed Sep 13, 2023
1 parent f8798c6 commit b1e5144
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 81 deletions.
71 changes: 33 additions & 38 deletions src/canvas/Canvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { Point } from '../Point';
import type { Group } from '../shapes/Group';
import type { IText } from '../shapes/IText/IText';
import type { FabricObject } from '../shapes/Object/FabricObject';
import { removeFromArray } from '../util';
import { removeFromArray } from '../util/internals/removeFromArray';
import { isTouchEvent, stopEvent } from '../util/dom_event';
import { getDocumentFromElement, getWindowFromElement } from '../util/dom_misc';
import { sendPointToPlane } from '../util/misc/planeChange';
Expand Down Expand Up @@ -83,13 +83,6 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
*/
private declare _willAddMouseDown: number;

/**
* Holds a reference to an object on the canvas that is receiving the drag over event.
* @type FabricObject
* @private
*/
private declare _draggedoverTarget?: FabricObject;

/**
* Holds a reference to an object on the canvas from where the drag operation started
* @type FabricObject
Expand All @@ -99,8 +92,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {

/**
* Holds a reference to an object on the canvas that is the current drop target
* May differ from {@link _draggedoverTarget}
* @todo inspect whether {@link _draggedoverTarget} and {@link _dropTarget} should be merged somehow
* May differ from {@link _dragSource}
* @type FabricObject
* @private
*/
Expand Down Expand Up @@ -242,21 +234,23 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
* @param {Event} e Event object fired on mousedown
*/
private _onMouseOut(e: TPointerEvent) {
const target = this._hoveredTarget;
const { target, targets } = this.hoveringState;
const shared = {
e,
isClick: false,
pointer: this.getPointer(e, true),
absolutePointer: this.getPointer(e),
};
this.fire('mouse:out', { ...shared, target });
this._hoveredTarget = undefined;
this.clearHoveringState();
target && target.fire('mouseout', { ...shared });
this._hoveredTargets.forEach((nestedTarget) => {
targets.forEach((nestedTarget) => {
if (!nestedTarget || nestedTarget === target) {
return;
}
this.fire('mouse:out', { ...shared, target: nestedTarget });
nestedTarget && nestedTarget.fire('mouseout', { ...shared });
nestedTarget.fire('mouseout', { ...shared });
});
this._hoveredTargets = [];
}

/**
Expand All @@ -277,8 +271,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
pointer: this.getPointer(e, true),
absolutePointer: this.getPointer(e),
});
this._hoveredTarget = undefined;
this._hoveredTargets = [];
this.clearHoveringState();
}
}

Expand All @@ -290,7 +283,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
private _onDragStart(e: DragEvent) {
this._isClick = false;
const activeObject = this.getActiveObject();
this._hoveredTargets = [];
this.clearHoveringState();
if (
isFabricObjectWithDragSupport(activeObject) &&
activeObject.onDragStart(e)
Expand Down Expand Up @@ -378,7 +371,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
delete this._dragSource;
// we need to call mouse up synthetically because the browser won't
this._onMouseUp(e);
this._hoveredTargets = [];
this.clearHoveringState();
}

/**
Expand All @@ -391,7 +384,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
e,
target: this._dragSource as FabricObject | undefined,
dragSource: this._dragSource as FabricObject | undefined,
dropTarget: this._draggedoverTarget as FabricObject,
dropTarget: this.hoveringState.target,
};
this.fire('drag', options);
this._dragSource && this._dragSource.fire('drag', options);
Expand Down Expand Up @@ -474,7 +467,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
};
this.fire('dragenter', options);
// fire dragenter on targets
this._hoveredTargets = [];
this.clearHoveringState();
this.handleSyntheticInOutEvents('drag', options);
}

Expand All @@ -484,10 +477,11 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
* @param {Event} [e] Event object fired on Event.js shake
*/
private _onDragLeave(e: DragEvent) {
const { target, targets } = this.hoveringState;
const options = {
e,
target: this._draggedoverTarget,
subTargets: this.targets,
target,
subTargets: targets,
dragSource: this._dragSource,
};
this.fire('dragleave', options);
Expand All @@ -498,7 +492,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
this._dropTarget = undefined;
// clear targets
this.targets = [];
this._hoveredTargets = [];
this.clearHoveringState();
}

/**
Expand Down Expand Up @@ -1230,7 +1224,7 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
}

/**
* Manage the mouseout/mouseover and dragenter/dragleave events for the fabric object on the canvas
* Handle mouseout/mouseover and dragenter/dragleave events
* @param {Fabric.Object} target the target where the target from the mousemove event
* @param {Event} e Event object fired on mousemove
* @private
Expand All @@ -1242,9 +1236,17 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
...context
}: TSyntheticEventContext[T] & { target?: FabricObject }
) {
const prevTarget = this._hoveredTarget,
prevTargets = this._hoveredTargets,
targets = removeFromArray([...this.targets], target) as FabricObject[];
const { target: prevTarget, targets: verbosePrevTargets } =
this.hoveringState;
// targets/prevTargets may contain target/prevTarget respectively so we remove them
const prevTargets = removeFromArray(
[...verbosePrevTargets],
prevTarget
) as FabricObject[];
const targets = removeFromArray(
[...this.targets],
target
) as FabricObject[];

this.fireSyntheticInOutEvents(type, {
...(context as TSyntheticEventContext[T]),
Expand All @@ -1259,8 +1261,6 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
oldTarget: prevTargets[i],
});
}
this._hoveredTarget = target;
this._hoveredTargets = targets;
}

/**
Expand Down Expand Up @@ -1481,27 +1481,22 @@ export class Canvas extends SelectableCanvas implements CanvasOptions {
if (target.group === activeSelection) {
// `target` is part of active selection => remove it
activeSelection.remove(target);
this._hoveredTarget = target;
this._hoveredTargets = removeFromArray([...this.targets], target);
this.setHoveringState(target);
if (activeSelection.size() === 1) {
// activate last remaining object
this._setActiveObject(activeSelection.item(0) as FabricObject, e);
}
} else {
// `target` isn't part of active selection => add it
activeSelection.multiSelectAdd(target);
this._hoveredTarget = activeSelection;
this._hoveredTargets = removeFromArray([...this.targets], target);
this.setHoveringState(activeSelection);
}
this._fireSelectionEvents(prevActiveObjects, e);
} else {
isInteractiveTextObject(activeObject) && activeObject.exitEditing();
// add the active object and the target to the active selection and set it as the active object
activeSelection.multiSelectAdd(activeObject, target);
this._hoveredTarget = activeSelection;
// ISSUE 4115: should we consider subTargets here?
// this._hoveredTargets = [];
// this._hoveredTargets = removeFromArray([...this.targets], target);
this.setHoveringState(activeSelection);
this._setActiveObject(activeSelection, e);
this._fireSelectionEvents([activeObject], e);
}
Expand Down
33 changes: 16 additions & 17 deletions src/canvas/SelectableCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import type { BaseBrush } from '../brushes/BaseBrush';
import { pick } from '../util/misc/pick';
import { sendPointToPlane } from '../util/misc/planeChange';
import { ActiveSelection } from '../shapes/ActiveSelection';
import { createCanvasElement } from '../util';
import { createCanvasElement } from '../util/misc/dom';
import { CanvasDOMManager } from './DOMManagers/CanvasDOMManager';
import { BOTTOM, CENTER, LEFT, RIGHT, TOP } from '../constants';
import type { CanvasOptions, TCanvasOptions } from './CanvasOptions';
Expand Down Expand Up @@ -194,19 +194,11 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
*/
targets: FabricObject[] = [];

/**
* Keep track of the hovered target
* @type FabricObject | null
* @private
*/
declare _hoveredTarget?: FabricObject;

/**
* hold the list of nested targets hovered
* @type FabricObject[]
* @private
*/
_hoveredTargets: FabricObject[] = [];
protected hoveringState: { target?: FabricObject; targets: FabricObject[] } =
{
target: undefined,
targets: [],
};

/**
* hold the list of objects to render
Expand Down Expand Up @@ -313,6 +305,14 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
this._createCacheCanvas();
}

protected setHoveringState(target?: FabricObject) {
this.hoveringState = { target, targets: [...this.targets] };
}

protected clearHoveringState() {
this.hoveringState = { target: undefined, targets: [] };
}

/**
* @private
* @param {FabricObject} obj Object that was added
Expand All @@ -337,9 +337,8 @@ export class SelectableCanvas<EventSpec extends CanvasEvents = CanvasEvents>
target: obj,
});
}
if (obj === this._hoveredTarget) {
this._hoveredTarget = undefined;
this._hoveredTargets = [];
if (obj === this.hoveringState.target) {
this.clearHoveringState();
}
super._onObjectRemoved(obj);
}
Expand Down
8 changes: 4 additions & 4 deletions test/unit/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -275,9 +275,9 @@
QUnit.test('remove actual hovered target', function(assert) {
var rect1 = makeRect();
canvas.add(rect1);
canvas._hoveredTarget = rect1;
canvas.hoveringState = { target: rect1, targets: [rect1] };
canvas.remove(rect1);
assert.equal(canvas._hoveredTarget, null, 'reference to hovered target should be removed');
assert.deepEqual(canvas.hoveringState, { target: undefined, targets: [] }, 'hovering state should be removed');
});

QUnit.test('before:selection:cleared', function(assert) {
Expand Down Expand Up @@ -357,7 +357,7 @@
canvas.add(rect1, rect2);
canvas.on('selection:created', function( ) { isFired = true; });
initActiveSelection(canvas, rect1, rect2, 'selection-order');
assert.equal(canvas._hoveredTarget, canvas.getActiveObject(), 'the created selection is also hovered');
assert.equal(canvas.hoveringState.target, canvas.getActiveObject(), 'the created selection is also hovered');
assert.equal(isFired, true, 'selection:created fired');
canvas.off('selection:created');
canvas.clear();
Expand Down Expand Up @@ -406,7 +406,7 @@
canvas.on('selection:updated', function( ) { isFired = true; });
updateActiveSelection(canvas, [rect1, rect2], rect3, 'selection-order');
assert.equal(isFired, true, 'selection:updated fired');
assert.equal(canvas._hoveredTarget, canvas.getActiveObject(), 'hovered target is updated');
assert.equal(canvas.hoveringState.target, canvas.getActiveObject(), 'hovered target is updated');
});

QUnit.test('update active selection fires deselected on an object', function(assert) {
Expand Down
46 changes: 24 additions & 22 deletions test/unit/canvas_events.js
Original file line number Diff line number Diff line change
Expand Up @@ -648,10 +648,11 @@
var o3 = new fabric.Object();
var control = [];
var targetControl = [];
[o1, o2, o3].forEach(target => {
Object.entries({ o1, o2, o3 }).forEach(([key, target]) => {
target.on(canvasEventName.replace(':', ''), () => {
targetControl.push(target);
});
target.toJSON = () => key;
});
canvas.add(o1, o2, o3);
c.on(canvasEventName, function (ev) {
Expand All @@ -661,13 +662,14 @@
event.initEvent(eventName, true, true);

// with targets
c._hoveredTarget = o3;
c._hoveredTargets = [o2, o1];
c.hoveringState = {
target: o3,
targets: [o2, o1, o3]
};
c.upperCanvasEl.dispatchEvent(event);
assert.equal(c._hoveredTarget, null, 'should clear `_hoveredTarget` ref');
assert.deepEqual(c._hoveredTargets, [], 'should clear `_hoveredTargets` ref');
assert.deepEqual(c.hoveringState, { target: undefined, targets: [] }, 'should clear hovering state');
const expected = [o3, o2, o1];
assert.deepEqual(control.map(ev => ev.target), expected, 'should equal control');
assert.deepEqual(control.map(ev => ev.target.toJSON()), expected.map(target => target.toJSON()), 'should equal control');
assert.deepEqual(targetControl, expected, 'should equal target control');

// without targets
Expand Down Expand Up @@ -708,17 +710,16 @@
target.item(1).item(1),
target.item(1).item(1).item(1)
];
canvas.handleSyntheticInOutEvents({ e: moveEvent, target });
canvas.handleSyntheticInOutEvents('mouse', { e: 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');
assert.equal(canvas.hoveringState.target, target, 'activeSelection is hovered target');
assert.equal(canvas.hoveringState.targets.length, 3, '3 additional subTargets are captured as hovered targets');

// perform MouseOut even on all hoveredTargets
canvas.targets = [];
canvas.handleSyntheticInOutEvents({ e: moveEvent });
canvas.handleSyntheticInOutEvents('mouse', { e: moveEvent });
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');
assert.deepEqual(canvas.hoveringState, { target: undefined, targets: [] }, 'should clear hovering state');
done();
});
});
Expand All @@ -735,24 +736,25 @@
assert.deepEqual(canvas._groupSelector, expectedGroupSelector, 'groupSelector is updated');
});

QUnit.test('mouseEnter removes _hoveredTarget', function(assert) {
var event = fabric.getFabricDocument().createEvent('MouseEvent');
QUnit.test('mouseEnter removes clears hovering state', function (assert) {
const event = fabric.getFabricDocument().createEvent('MouseEvent');
event.initEvent('mouseenter', true, true);
var c = new fabric.Canvas();
c._hoveredTarget = new fabric.Object();
const c = new fabric.Canvas();
const target = new fabric.Object();
c.hoveringState = { target, targets: [target] };
c.upperCanvasEl.dispatchEvent(event);
assert.equal(c._hoveredTarget, null, '_hoveredTarget has been removed');
assert.deepEqual(c.hoveringState, { target: undefined, targets: [] }, 'should clear hovering state');
});

QUnit.test('mouseEnter does not remove _hoveredTarget if a transform is happening', function(assert) {
QUnit.test('mouseEnter does not clear hovering state if a transform is happening', function(assert) {
var event = fabric.getFabricDocument().createEvent('MouseEvent');
event.initEvent('mouseenter', true, true);
var c = new fabric.Canvas();
var obj = new fabric.Object();
c._hoveredTarget = obj;
var target = new fabric.Object();
const state = c.hoveringState = { target, targets: [target] };
c._currentTransform = {};
c.upperCanvasEl.dispatchEvent(event);
assert.equal(c._hoveredTarget, obj, '_hoveredTarget has been not removed');
assert.equal(c.hoveringState, state, 'hoveringState has been not removed');
});

QUnit.test('mouseEnter removes __corner', function(assert) {
Expand Down

0 comments on commit b1e5144

Please sign in to comment.