diff --git a/debug/markers.html b/debug/markers.html index 52bbe38ab39..2651054f671 100644 --- a/debug/markers.html +++ b/debug/markers.html @@ -46,20 +46,22 @@ var lng = w + (e - w) * Math.random(); var lat = s + (n - s) * Math.random(); + var popup = new mapboxgl.Popup().setText('hello hi'); var marker = new mapboxgl.Marker() .setLngLat([lng, lat]) + .setPopup(popup) .addTo(map); marker.getElement().onclick = function(e) { - alert('Hello world'); - e.stopPropagation(); + alert('clicked me'); }; } for (var i = 0; i < 100; i++) addRandomMarker(); map.addControl(new mapboxgl.Navigation()); + diff --git a/js/ui/marker.js b/js/ui/marker.js index 6a6e34510e5..d3915f81baf 100644 --- a/js/ui/marker.js +++ b/js/ui/marker.js @@ -4,8 +4,10 @@ module.exports = Marker; var DOM = require('../util/dom'); +var util = require('../util/util'); var LngLat = require('../geo/lng_lat'); var Point = require('point-geometry'); +var Popup = require('./popup'); /** * Creates a marker component @@ -36,7 +38,7 @@ Marker.prototype = { * @param {Map} map * @returns {Marker} `this` */ - addTo: function(map) { + addTo: function (map) { this.remove(); this._map = map; map.getCanvasContainer().appendChild(this._el); @@ -52,13 +54,14 @@ Marker.prototype = { * marker.remove(); * @returns {Marker} `this` */ - remove: function() { + remove: function () { if (this._map) { this._map.off('move', this._update); this._map = null; } var parent = this._el.parentNode; if (parent) parent.removeChild(this._el); + if (this._popup) this._closePopup(); return this; }, @@ -66,7 +69,7 @@ Marker.prototype = { * Get the marker's geographical location * @returns {LngLat} */ - getLngLat: function() { + getLngLat: function () { return this._lngLat; }, @@ -75,19 +78,91 @@ Marker.prototype = { * @param {LngLat} lnglat * @returns {Marker} `this` */ - setLngLat: function(lnglat) { + setLngLat: function (lnglat) { this._lngLat = LngLat.convert(lnglat); + if (this._popup) this._popup.setLngLat(this._lngLat); this._update(); return this; }, - getElement: function() { + getElement: function () { return this._el; }, - _update: function() { + /** + * Binds a Popup to the Marker + * @param {Popup=} popup an instance of the `Popup` class. If undefined or null, any popup + * set on this `Marker` instance is unset + * @returns {Marker} `this` + */ + + setPopup: function (popup) { + if (popup == null) { + this._closePopup(); + delete this._popupHandlersAdded; + delete this._popup; + } else if (popup instanceof Popup) { + this._popup = popup; + } else { + util.warnOnce('Marker.setPopup only accepts an instance of the Popup class as an argument. If no argument is provided, the popup is unset from this Marker instance'); + } + + if (this._popup && this._lngLat) this._popup.setLngLat(this._lngLat); + + if (!this._popupHandlersAdded) { + this.getElement().addEventListener('click', this._openPopup.bind(this)); + this._popupHandlersAdded = true; + } + return this; + }, + + /** + * Returns the Popup instance that is bound to the Marker + * @returns {Popup} popup + */ + getPopup: function () { + return this._popup; + }, + + /** + * Opens or closes the bound popup, depending on the current state + * @returns {Marker} `this` + */ + togglePopup: function () { + if (this._popup) { + if (this._popup._map) { + this._closePopup(); + } else { + this._openPopup(); + } + } + }, + + _openPopup: function (e) { + // prevent event from bubbling up to the map canvas + e.stopPropagation(); + + if (!this._popup || !this._map) return; + + if (!this._popup._map) { + this._popup.addTo(this._map); + } + + return this; + }, + + _closePopup: function () { + if (this._popup) { + this._popup.remove(); + } + + return this; + }, + + _update: function () { if (!this._map) return; var pos = this._map.project(this._lngLat)._add(this._offset); DOM.setTransform(this._el, 'translate(' + pos.x + 'px,' + pos.y + 'px)'); } }; + diff --git a/js/util/dom.js b/js/util/dom.js index 6ad303b1d4e..f05e47de5d9 100644 --- a/js/util/dom.js +++ b/js/util/dom.js @@ -2,12 +2,15 @@ exports.create = function (tagName, className, container) { return { - offsetWidth: container.offsetWidth, - offsetHeight: container.offsetHeight, + offsetWidth: container ? container.offsetWidth : null, + offsetHeight: container ? container.offsetHeight : null, remove: function () {}, addEventListener: function() {}, classList: { add: function () {} - } + }, + appendChild: function () {} }; }; + +exports.setTransform = function() {}; diff --git a/package.json b/package.json index 4bc6094db0f..720582aa100 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,8 @@ "handlebars": "4.0.5", "highlight.js": "9.3.0", "istanbul": "^0.4.2", + "jsdom": "^9.4.2", + "jsdom-global": "^2.1.0", "json-loader": "^0.5.4", "lodash": "^4.13.1", "mapbox-gl-test-suite": "mapbox/mapbox-gl-test-suite#3e36b193a0c442a3fd863119f101afa6db97b32d", @@ -93,15 +95,12 @@ "build-token": "browserify debug/access-token.js --debug --transform envify > debug/access-token-generated.js", "watch-bench": "node bench/download-data.js && watchify bench/index.js --plugin [minifyify --no-map] --transform [babelify --presets react] --transform unassertify --transform envify --outfile bench/index-generated.js --verbose", "start-server": "st --no-cache --localhost --port 9966 --index index.html .", - "start": "run-p build-token watch-dev watch-bench start-server", "start-debug": "run-p build-token watch-dev start-server", "start-bench": "run-p build-token watch-bench start-server", - "build-docs": "documentation build --github --format html --config documentation.yml --theme ./docs/_theme --output docs/api/", "build": "npm run build-docs # invoked by publisher when publishing docs on the mb-pages branch", "start-docs": "npm run build-min && npm run build-docs && jekyll serve --watch", - "lint": "eslint --ignore-path .gitignore js test bench docs/_posts/examples/*.html", "open-changed-examples": "git diff --name-only mb-pages HEAD -- docs/_posts/examples/*.html | awk '{print \"http://127.0.0.1:4000/mapbox-gl-js/example/\" substr($0,33,length($0)-37)}' | xargs open", "test-suite": "node test/render.test.js && node test/query.test.js", diff --git a/test/js/ui/marker.test.js b/test/js/ui/marker.test.js new file mode 100644 index 00000000000..bf9d8cd91d8 --- /dev/null +++ b/test/js/ui/marker.test.js @@ -0,0 +1,90 @@ +'use strict'; + +var test = require('tap').test; +var jsdomGlobal = require('jsdom-global'); + +var extend = require('../../../js/util/util').extend; +var Map = require('../../../js/ui/map'); +var Marker = require('../../../js/ui/marker'); +var Popup = require('../../../js/ui/popup'); + +function prepareDOM() { + var cleanup = jsdomGlobal(); + var mapdiv = document.createElement('div'); + mapdiv.id = "map"; + document.body.appendChild(mapdiv); + return cleanup; +} + +function createMap(options, callback) { + var map = new Map(extend({ + container: 'map', + attributionControl: false, + trackResize: true, + style: { + "version": 8, + "sources": {}, + "layers": [] + } + }, options)); + + if (callback) map.on('load', function () { + callback(null, map); + }); + + return map; +} + + + +test('Marker', function (t) { + t.test('constructor', function (t) { + var cleanup = prepareDOM(); + var el = document.createElement('div'); + var marker = new Marker(el); + t.ok(marker.getElement(), 'marker element is created'); + cleanup(); + t.end(); + }); + + t.test('marker is added to map', function (t) { + var cleanup = prepareDOM(); + var map = createMap(); + var el = document.createElement('div'); + map.on('load', function () { + var marker = new Marker(el).setLngLat([-77.01866, 38.888]); + t.ok(marker.addTo(map) instanceof Marker, 'marker.addTo(map) returns Marker instance'); + t.ok(marker._map, 'marker instance is bound to map instance'); + cleanup(); + t.end(); + }); + }); + + t.test('popups can be bound to marker instance', function (t) { + var cleanup = prepareDOM(); + var map = createMap(); + var el = document.createElement('div'); + var popup = new Popup(); + var marker = new Marker(el).setLngLat([-77.01866, 38.888]).addTo(map); + marker.setPopup(popup); + t.ok(marker.getPopup() instanceof Popup, 'popup created with Popup instance'); + cleanup(); + t.end(); + }); + + t.test('popups can be unbound from a marker instance', function (t) { + var cleanup = prepareDOM(); + var map = createMap(); + var el = document.createElement('div'); + var marker = new Marker(el).setLngLat([-77.01866, 38.888]).addTo(map); + marker.setPopup(new Popup()); + t.ok(marker.getPopup() instanceof Popup); + t.ok(marker.setPopup() instanceof Marker, 'passing no argument to Marker.setPopup() is valid'); + t.ok(!marker.getPopup(), 'Calling setPopup with no argument successfully removes Popup instance from Marker instance'); + t.end(); + cleanup(); + }); + + t.end(); +}); +