diff --git a/debug/tests/mousemove_on_polygons.html b/debug/tests/mousemove_on_polygons.html new file mode 100644 index 00000000..96c08acb --- /dev/null +++ b/debug/tests/mousemove_on_polygons.html @@ -0,0 +1,107 @@ + + + + Leaflet debug page + + + + + + + + + + + + + + +
Enter Move Exit Click
Triangle 1:
Triangle 2:
Map:
+ + + +
+ + + + diff --git a/debug/vector/vector-canvas.html b/debug/vector/vector-canvas.html index 7216a646..430ed130 100644 --- a/debug/vector/vector-canvas.html +++ b/debug/vector/vector-canvas.html @@ -29,7 +29,8 @@ for (var i = 0, latlngs = [], len = route.length; i < len; i++) { latlngs.push(new L.LatLng(route[i][0], route[i][1])); } - var path = new L.Polyline(latlngs); + var canvas = L.canvas(); + var path = new L.Polyline(latlngs, {renderer: canvas}); var map = new L.Map('map', {layers: [osm]}); @@ -41,7 +42,8 @@ circleOptions = { color: 'red', fillColor: 'yellow', - fillOpacity: 0.7 + fillOpacity: 0.7, + renderer: canvas }; var circle = new L.Circle(circleLocation, 500000, circleOptions), @@ -79,7 +81,8 @@ var polygon = new L.Polygon([polygonPoints, holePoints], { fillColor: "#333", - color: 'green' + color: 'green', + renderer: canvas }); group.addLayer(polygon); diff --git a/spec/suites/map/MapSpec.js b/spec/suites/map/MapSpec.js index eeb033fe..43f692c7 100644 --- a/spec/suites/map/MapSpec.js +++ b/spec/suites/map/MapSpec.js @@ -619,4 +619,132 @@ describe("Map", function () { }); + describe('#DOM events', function () { + + var c, map; + + beforeEach(function () { + c = document.createElement('div'); + c.style.width = '400px'; + c.style.height = '400px'; + map = new L.Map(c); + map.setView(new L.LatLng(0, 0), 0); + document.body.appendChild(c); + }); + + afterEach(function () { + document.body.removeChild(c); + }); + + it("DOM events propagate from polygon to map", function () { + var spy = sinon.spy(); + map.on("mousemove", spy); + var layer = new L.Polygon([[1, 2], [3, 4], [5, 6]]).addTo(map); + happen.mousemove(layer._path); + expect(spy.calledOnce).to.be.ok(); + }); + + it("DOM events propagate from canvas polygon to map", function () { + var spy = sinon.spy(); + map.on("mousemove", spy); + var layer = new L.Polygon([[1, 2], [3, 4], [5, 6]], {rendered: L.canvas()}).addTo(map); + happen.mousemove(layer._path); + expect(spy.calledOnce).to.be.ok(); + }); + + it("DOM events propagate from marker to map", function () { + var spy = sinon.spy(); + map.on("mousemove", spy); + var layer = new L.Marker([1, 2]).addTo(map); + happen.mousemove(layer._icon); + expect(spy.calledOnce).to.be.ok(); + }); + + it("DOM events fired on marker can be cancelled before being caught by the map", function () { + var mapSpy = sinon.spy(); + var layerSpy = sinon.spy(); + map.on("mousemove", mapSpy); + var layer = new L.Marker([1, 2]).addTo(map); + layer.on("mousemove", L.DomEvent.stopPropagation).on("mousemove", layerSpy); + happen.mousemove(layer._icon); + expect(layerSpy.calledOnce).to.be.ok(); + expect(mapSpy.called).not.to.be.ok(); + }); + + it("DOM events fired on polygon can be cancelled before being caught by the map", function () { + var mapSpy = sinon.spy(); + var layerSpy = sinon.spy(); + map.on("mousemove", mapSpy); + var layer = new L.Polygon([[1, 2], [3, 4], [5, 6]]).addTo(map); + layer.on("mousemove", L.DomEvent.stopPropagation).on("mousemove", layerSpy); + happen.mousemove(layer._path); + expect(layerSpy.calledOnce).to.be.ok(); + expect(mapSpy.called).not.to.be.ok(); + }); + + it("DOM events fired on canvas polygon can be cancelled before being caught by the map", function () { + var mapSpy = sinon.spy(); + var layerSpy = sinon.spy(); + map.on("mousemove", mapSpy); + var layer = new L.Polygon([[1, 2], [3, 4], [5, 6]], {rendered: L.canvas()}).addTo(map); + layer.on("mousemove", L.DomEvent.stopPropagation).on("mousemove", layerSpy); + happen.mousemove(layer._path); + expect(layerSpy.calledOnce).to.be.ok(); + expect(mapSpy.called).not.to.be.ok(); + }); + + it("mouseout is only forwared if fired on the original target", function () { + var mapSpy = sinon.spy(), + layerSpy = sinon.spy(), + otherSpy = sinon.spy(); + var layer = new L.Polygon([[1, 2], [3, 4], [5, 6]]).addTo(map); + var other = new L.Polygon([[10, 20], [30, 40], [50, 60]]).addTo(map); + map.on("mouseout", mapSpy); + layer.on("mouseout", layerSpy); + other.on("mouseout", otherSpy); + happen.mouseout(layer._path); + expect(mapSpy.called).not.to.be.ok(); + expect(otherSpy.called).not.to.be.ok(); + expect(layerSpy.calledOnce).to.be.ok(); + }); + + it("mouseout is not forwared to layers if fired on the map", function () { + var mapSpy = sinon.spy(), + layerSpy = sinon.spy(), + otherSpy = sinon.spy(); + var layer = new L.Polygon([[1, 2], [3, 4], [5, 6]]).addTo(map); + var other = new L.Polygon([[10, 20], [30, 40], [50, 60]]).addTo(map); + map.on("mouseout", mapSpy); + layer.on("mouseout", layerSpy); + other.on("mouseout", otherSpy); + happen.mouseout(map._container); + expect(otherSpy.called).not.to.be.ok(); + expect(layerSpy.called).not.to.be.ok(); + expect(mapSpy.calledOnce).to.be.ok(); + }); + + it("preclick is fired before click on marker and map", function () { + var called = 0; + var layer = new L.Marker([1, 2]).addTo(map); + layer.on("preclick", function (e) { + expect(called++).to.eql(0); + expect(e.latlng).to.ok(); + }); + layer.on("click", function (e) { + expect(called++).to.eql(2); + expect(e.latlng).to.ok(); + }); + map.on("preclick", function (e) { + expect(called++).to.eql(1); + expect(e.latlng).to.ok(); + }); + map.on("click", function (e) { + expect(called++).to.eql(3); + expect(e.latlng).to.ok(); + }); + happen.click(layer._icon); + }); + + }); + }); diff --git a/src/dom/DomEvent.js b/src/dom/DomEvent.js index 9d67761f..2a87a5e6 100644 --- a/src/dom/DomEvent.js +++ b/src/dom/DomEvent.js @@ -130,6 +130,8 @@ L.DomEvent = { if (e.stopPropagation) { e.stopPropagation(); + } else if (e.originalEvent) { // In case of Leaflet event. + e.originalEvent._stopped = true; } else { e.cancelBubble = true; } diff --git a/src/layer/vector/Canvas.js b/src/layer/vector/Canvas.js index 67ea31b4..ec2c0042 100644 --- a/src/layer/vector/Canvas.js +++ b/src/layer/vector/Canvas.js @@ -233,7 +233,7 @@ L.Canvas = L.Renderer.extend({ }, _fireEvent: function (layer, e, type) { - this._map._fireDOMEvent(layer, e, type || e.type); + this._map._fireDOMEvent(e, type || e.type, [layer]); }, // TODO _bringToFront & _bringToBack, pretty tricky diff --git a/src/map/Map.js b/src/map/Map.js index 21274dc8..663e963b 100644 --- a/src/map/Map.js +++ b/src/map/Map.js @@ -601,6 +601,7 @@ L.Map = L.Evented.extend({ if (!L.DomEvent) { return; } this._targets = {}; + this._targets[L.stamp(this._container)] = this; var onOff = remove ? 'off' : 'on'; @@ -623,62 +624,81 @@ L.Map = L.Evented.extend({ this._container.scrollLeft = 0; }, - _findEventTarget: function (src) { + _findEventTargets: function (src, bubble) { + var targets = [], target; while (src) { - var target = this._targets[L.stamp(src)]; + target = this._targets[L.stamp(src)]; if (target) { - return target; + targets.push(target); + if (!bubble) { break; } } if (src === this._container) { break; } src = src.parentNode; } - return null; + return targets; }, _handleDOMEvent: function (e) { if (!this._loaded || L.DomEvent._skipped(e)) { return; } - // find the layer the event is propagating from - var target = this._findEventTarget(e.target || e.srcElement), - type = e.type === 'keypress' && e.keyCode === 13 ? 'click' : e.type; + // find the layer the event is propagating from and its parents + var type = e.type === 'keypress' && e.keyCode === 13 ? 'click' : e.type; - // special case for map mouseover/mouseout events so that they're actually mouseenter/mouseleave - if (!target && (type === 'mouseover' || type === 'mouseout') && - !L.DomEvent._checkMouse(this._container, e)) { return; } + if (e.type === 'click') { + // Fire a synthetic 'preclick' event which propagates up (mainly for closing popups). + var synth = L.Util.extend({}, e); + synth.type = 'preclick'; + this._handleDOMEvent(synth); + } - // prevents outline when clicking on keyboard-focusable element if (type === 'mousedown') { + // prevents outline when clicking on keyboard-focusable element L.DomUtil.preventOutline(e.target || e.srcElement); } - this._fireDOMEvent(target || this, e, type); + this._fireDOMEvent(e, type); }, - _fireDOMEvent: function (target, e, type) { - if (!target.listens(type, true) && (type !== 'click' || !target.listens('preclick', true))) { return; } + _fireDOMEvent: function (e, type, targets) { if (type === 'contextmenu') { L.DomEvent.preventDefault(e); } + var isHover = type === 'mouseover' || type === 'mouseout'; + targets = (targets || []).concat(this._findEventTargets(e.target || e.srcElement, !isHover)); + + if (!targets.length) { + targets = [this]; + + // special case for map mouseover/mouseout events so that they're actually mouseenter/mouseleave + if (isHover && !L.DomEvent._checkMouse(this._container, e)) { return; } + } + + var target = targets[0]; + // prevents firing click after you just dragged an object if (e.type === 'click' && !e._simulated && this._draggableMoved(target)) { return; } var data = { originalEvent: e }; + if (e.type !== 'keypress') { data.containerPoint = target instanceof L.Marker ? this.latLngToContainerPoint(target.getLatLng()) : this.mouseEventToContainerPoint(e); data.layerPoint = this.containerPointToLayerPoint(data.containerPoint); data.latlng = this.layerPointToLatLng(data.layerPoint); } - if (type === 'click') { - target.fire('preclick', data, true); + + for (var i = 0; i < targets.length; i++) { + if (targets[i].listens(type, true)) { + targets[i].fire(type, data, true); + if (data.originalEvent._stopped) { return; } + } } - target.fire(type, data, true); }, _draggableMoved: function (obj) {