Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bindPopup API to the Marker class #3056

Merged
merged 5 commits into from
Aug 25, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions debug/markers.html
Original file line number Diff line number Diff line change
Expand Up @@ -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());

</script>

</body>
Expand Down
87 changes: 81 additions & 6 deletions js/ui/marker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand All @@ -52,21 +54,22 @@ 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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this._closePopup has an internal if (this._popup) guard -- either call it unguarded here, or add external guards everywhere it's called and remove the internal guard.

return this;
},

/**
* Get the marker's geographical location
* @returns {LngLat}
*/
getLngLat: function() {
getLngLat: function () {
return this._lngLat;
},

Expand All @@ -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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of tracking state in _popupHandlersAdded, callremoveEventListener when the popup is removed. This will prevent memory leaks and unexpected behavior if the Marker continues to be used without a popup. You'll need to save the this-bound version of _openPopup, say by using util.bindAll(['_openPopup'], this), so that it can be passed to removeEventListener.

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();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw an error because _openPopup is expecting an event argument.

}
}
},

_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)');
}
};

9 changes: 6 additions & 3 deletions js/util/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {};
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
90 changes: 90 additions & 0 deletions test/js/ui/marker.test.js
Original file line number Diff line number Diff line change
@@ -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;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you refactor this so that we don't need to use global variables? You should be able to do everything you need with access to the window object within test-scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was having issues with the functions run by the tests failing because document + window weren't in scope. I refactored to use the jsdom-global module which provides a cleanup function that removes the created variables from the global scope and can be called when the tests are complete. Is this acceptable?

Otherwise we can take the approach that mapbox-gl-directions uses (browserify + smokestack + tap) but that would require some refactoring of the test suite.


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();
});