diff --git a/src/canvas.class.js b/src/canvas.class.js index 94268c72cfa..eb39c6cb068 100644 --- a/src/canvas.class.js +++ b/src/canvas.class.js @@ -330,6 +330,26 @@ */ fireMiddleClick: false, + /** + * Keep track of the subTargets for Mouse Events + * @type fabric.Object[] + */ + targets: [], + + /** + * Keep track of the hovered target + * @type fabric.Object + * @private + */ + _hoveredTarget: null, + + /** + * hold the list of nested targets hovered + * @type fabric.Object[] + * @private + */ + _hoveredTargets: [], + /** * @private */ @@ -1477,8 +1497,9 @@ this.fire('selection:cleared', { target: obj }); obj.fire('deselected'); } - if (this._hoveredTarget === obj) { + if (obj === this._hoveredTarget){ this._hoveredTarget = null; + this._hoveredTargets = []; } this.callSuper('_onObjectRemoved', obj); }, diff --git a/src/mixins/canvas_events.mixin.js b/src/mixins/canvas_events.mixin.js index c88ad098deb..ae99d86905d 100644 --- a/src/mixins/canvas_events.mixin.js +++ b/src/mixins/canvas_events.mixin.js @@ -170,6 +170,14 @@ this.fire('mouse:out', { target: target, e: e }); this._hoveredTarget = null; target && target.fire('mouseout', { e: e }); + + var _this = this; + this._hoveredTargets.forEach(function(_target){ + _this.fire('mouse:out', { target: target, e: e }); + _target && target.fire('mouseout', { e: e }); + }); + this._hoveredTargets = []; + if (this._iTextInstances) { this._iTextInstances.forEach(function(obj) { if (obj.isEditing) { @@ -193,6 +201,7 @@ if (!this.currentTransform && !this.findTarget(e)) { this.fire('mouse:over', { target: null, e: e }); this._hoveredTarget = null; + this._hoveredTargets = []; } }, @@ -818,13 +827,25 @@ * @private */ _fireOverOutEvents: function(target, e) { - this.fireSyntheticInOutEvents(target, e, { - targetName: '_hoveredTarget', - canvasEvtOut: 'mouse:out', - evtOut: 'mouseout', - canvasEvtIn: 'mouse:over', - evtIn: 'mouseover', + var _this = this, _hoveredTarget = this._hoveredTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + diff = _hoveredTargets.length - targets.length, + diffArrayLength = diff > 0 ? diff : 0, + diffArray = []; + for (var i = 0; i < diffArrayLength; i++){ + diffArray.push(null); + } + [target].concat(targets, diffArray).forEach(function(_target, index) { + _this.fireSyntheticInOutEvents(_target, e, { + oldTarget: index === 0 ? _hoveredTarget : _hoveredTargets[index - 1], + canvasEvtOut: 'mouse:out', + evtOut: 'mouseout', + canvasEvtIn: 'mouse:over', + evtIn: 'mouseover', + }); }); + this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); }, /** @@ -834,11 +855,22 @@ * @private */ _fireEnterLeaveEvents: function(target, e) { - this.fireSyntheticInOutEvents(target, e, { - targetName: '_draggedoverTarget', - evtOut: 'dragleave', - evtIn: 'dragenter', + var _this = this, _draggedoverTarget = this._draggedoverTarget, + _hoveredTargets = this._hoveredTargets, targets = this.targets, + diff = _hoveredTargets.length - targets.length, + diffArrayLength = diff > 0 ? diff : 0, + diffArray = []; + for (var i = 0; i < diffArrayLength; i++){ + diffArray.push(null); + } + [target].concat(targets, diffArray).forEach(function(_target, index) { + _this.fireSyntheticInOutEvents(_target, e, { + oldTarget: index === 0 ? _draggedoverTarget : _hoveredTargets[index - 1], + evtOut: 'dragleave', + evtIn: 'dragenter', + }); }); + this._draggedoverTarget = target; }, /** @@ -854,12 +886,11 @@ * @private */ fireSyntheticInOutEvents: function(target, e, config) { - var inOpt, outOpt, oldTarget = this[config.targetName], outFires, inFires, + var inOpt, outOpt, oldTarget = config.oldTarget, outFires, inFires, targetChanged = oldTarget !== target, canvasEvtIn = config.canvasEvtIn, canvasEvtOut = config.canvasEvtOut; if (targetChanged) { inOpt = { e: e, target: target, previousTarget: oldTarget }; outOpt = { e: e, target: oldTarget, nextTarget: target }; - this[config.targetName] = target; } inFires = target && targetChanged; outFires = oldTarget && targetChanged; @@ -1024,6 +1055,13 @@ && 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 { diff --git a/src/mixins/canvas_grouping.mixin.js b/src/mixins/canvas_grouping.mixin.js index b1e95334e35..a639f8f66f4 100644 --- a/src/mixins/canvas_grouping.mixin.js +++ b/src/mixins/canvas_grouping.mixin.js @@ -53,6 +53,7 @@ if (activeSelection.contains(target)) { activeSelection.removeWithUpdate(target); this._hoveredTarget = target; + this._hoveredTargets = this.targets.concat(); if (activeSelection.size() === 1) { // activate last remaining object this._setActiveObject(activeSelection.item(0), e); @@ -61,6 +62,7 @@ else { activeSelection.addWithUpdate(target); this._hoveredTarget = activeSelection; + this._hoveredTargets = this.targets.concat(); } this._fireSelectionEvents(currentActiveObjects, e); }, @@ -71,6 +73,9 @@ _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); }, diff --git a/src/shapes/group.class.js b/src/shapes/group.class.js index 2b622ec6245..e4a92375348 100644 --- a/src/shapes/group.class.js +++ b/src/shapes/group.class.js @@ -35,7 +35,7 @@ strokeWidth: 0, /** - * Indicates if click events should also check for subtargets + * Indicates if click, mouseover, mouseout events & hoverCursor should also check for subtargets * @type Boolean * @default */ diff --git a/src/shapes/object.class.js b/src/shapes/object.class.js index 10d94f7c5c0..13996116650 100644 --- a/src/shapes/object.class.js +++ b/src/shapes/object.class.js @@ -2115,5 +2115,4 @@ * @type Number */ fabric.Object.__uid = 0; - })(typeof exports !== 'undefined' ? exports : this); diff --git a/test/unit/canvas_events.js b/test/unit/canvas_events.js index 369ed91ab2e..cfbc5c6f95e 100644 --- a/test/unit/canvas_events.js +++ b/test/unit/canvas_events.js @@ -1,5 +1,7 @@ (function() { + var SUB_TARGETS_JSON = '{"version":"' + fabric.version + '","objects":[{"type":"activeSelection","left":-152,"top":656.25,"width":356.5,"height":356.5,"scaleX":0.45,"scaleY":0.45,"objects":[]},{"type":"group","left":11,"top":6,"width":511.5,"height":511.5,"objects":[{"type":"rect","left":-255.75,"top":-255.75,"width":50,"height":50,"fill":"#6ce798","scaleX":10.03,"scaleY":10.03,"opacity":0.8},{"type":"group","left":-179.75,"top":22,"width":356.5,"height":356.5,"scaleX":0.54,"scaleY":0.54,"objects":[{"type":"rect","left":-178.25,"top":-178.25,"width":50,"height":50,"fill":"#4862cc","scaleX":6.99,"scaleY":6.99,"opacity":0.8},{"type":"group","left":-163.25,"top":-161.25,"width":177.5,"height":177.5,"objects":[{"type":"rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]},{"type":"group","left":-34.25,"top":-31.25,"width":177.5,"height":177.5,"scaleX":1.08,"scaleY":1.08,"objects":[{"type":"rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]}]},{"type":"group","left":-202.75,"top":-228.5,"width":356.5,"height":356.5,"scaleX":0.61,"scaleY":0.61,"objects":[{"type":"rect","left":-178.25,"top":-178.25,"width":50,"height":50,"fill":"#4862cc","scaleX":6.99,"scaleY":6.99,"opacity":0.8},{"type":"group","left":-163.25,"top":-161.25,"width":177.5,"height":177.5,"objects":[{"type":"rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]},{"type":"group","left":-34.25,"top":-31.25,"width":177.5,"height":177.5,"scaleX":1.08,"scaleY":1.08,"objects":[{"type":"rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]}]},{"type":"group","left":138.3,"top":-90.22,"width":356.5,"height":356.5,"scaleX":0.42,"scaleY":0.42,"angle":62.73,"objects":[{"type":"rect","left":-178.25,"top":-178.25,"width":50,"height":50,"fill":"#4862cc","scaleX":6.99,"scaleY":6.99,"opacity":0.8},{"type":"group","left":-163.25,"top":-161.25,"width":177.5,"height":177.5,"objects":[{"type":"rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]},{"type":"group","left":-34.25,"top":-31.25,"width":177.5,"height":177.5,"scaleX":1.08,"scaleY":1.08,"objects":[{"type":"rect","left":-88.75,"top":-88.75,"width":50,"height":50,"fill":"#5fe909","scaleX":3.48,"scaleY":3.48,"opacity":0.8},{"type":"rect","left":-59.75,"top":-68.75,"width":50,"height":50,"fill":"#f3529c","opacity":0.8},{"type":"triangle","left":36.03,"top":-38.12,"width":50,"height":50,"fill":"#c1124e","angle":39.07,"opacity":0.8},{"type":"rect","left":-65.75,"top":17.25,"width":50,"height":50,"fill":"#9c5120","opacity":0.8}]}]}]}]}'; + var canvas = this.canvas = new fabric.Canvas(null, {enableRetinaScaling: false, width: 600, height: 600}); var upperCanvasEl = canvas.upperCanvasEl; @@ -481,6 +483,53 @@ }); }); + QUnit.test('Fabric mouseover, mouseout events fire for subTargets when subTargetCheck is enabled', function(assert){ + var counterOver = 0, counterOut = 0, canvas = new fabric.Canvas(); + function setSubTargetCheckRecursive(obj) { + if (obj._objects) { + obj._objects.forEach(setSubTargetCheckRecursive); + } + obj.subTargetCheck = true; + obj.on('mouseover', function() { + counterOver++; + }); + obj.on('mouseout', function() { + counterOut++; + }); + } + canvas.loadFromJSON(SUB_TARGETS_JSON, function() { + var activeSelection = new fabric.ActiveSelection(canvas.getObjects(), { + canvas: canvas + }); + canvas.setActiveObject(activeSelection); + setSubTargetCheckRecursive(activeSelection); + + // perform MouseOver event on a deeply nested subTarget + var moveEvent = fabric.document.createEvent('HTMLEvents'); + moveEvent.initEvent('mousemove', true, true); + var target = canvas.item(1); + canvas.targets = [ + target.item(1), + target.item(1).item(1), + target.item(1).item(1).item(1) + ]; + canvas._fireOverOutEvents(target, moveEvent); + 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); + 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'); + }); + }); + + // TODO: QUnit.test('mousemove: subTargetCheck: setCursorFromEvent considers subTargets') + // TODO: QUnit.test('mousemove: subTargetCheck: setCursorFromEvent considers subTargets in reverse order, so the top-most subTarget's .hoverCursor takes precedence') + ['MouseDown', 'MouseMove', 'MouseOut', 'MouseEnter', 'MouseWheel', 'DoubleClick'].forEach(function(eventType) { QUnit.test('avoid multiple registration - ' + eventType, function(assert) { var funcName = '_on' + eventType; @@ -550,7 +599,7 @@ c._hoveredTarget = obj; c.currentTransform = {}; c.upperCanvasEl.dispatchEvent(event); - assert.equal(c._hoveredTarget, obj, '_hoveredTarget has been removed'); + assert.equal(c._hoveredTarget, obj, '_hoveredTarget has been not removed'); }); QUnit.test('mouseEnter removes __corner', function(assert) {