Situation:
I have multiple Polygon, Polyline, Point layers overlapping each other on the map. And all of this have a different popups. When I click on one layer (overlapping with other layer), the click action propogates to all the layers.
I am using the code below but unable to stop event propagating on layer click events .
mapBox.on('click', layerId, function (e) {
console.log(e);
//e.stopPropagation(); which is not working
//e.originalEvent.stopPropagation()
var popupHtml = getPopupHtmlWrapper(e.features[0]);
new mapboxgl.Popup({closeButton:false})
.setLngLat(e.lngLat)
.setHTML(popupHtml)
.addTo(mapBox);
});
Can you provide a solution on how this can be achieved?
f5
i have the same probolem , did you get a solution?
This issue tracker is for tracking bugs and feature requests. For 'how do I... ?' questions, we recommend you direct them to StackOverflow (which I see you've already done https://stackoverflow.com/questions/47576527/how-to-manipulate-event-bubbling-when-registering-click-event-upon-a-layer-in-ma) or Mapbox support. Closing this in favor of your StackOverflow ticket.
Hi, I was unable to post a response in StackOverflow, so posting an answer here, even though the ticket is closed.
I have myself written a solution for this, by making my own eventHandler module, where I register 'click' event listeners to specific layers.
The event handler itself listens for all map clicks, and checks which layers was clicked and the order they appear in the map.
If I have manually registered an onclick listener for "layer 1" and "layer 2", but "layer 2" is above "layer 1", then the onclick listener function for layer 1 is called first, and that function can choose to return false if it wants to cancel the event bubbling.
So it is my custom event callback handler that calls each layer's onclick function in the same order as the layer order in the map. Thus, top-most layers are triggered first.
An example of the custom layer handler event module can be seen here:
https://api.mazemap.com/js/v2.0.0-beta.9/docs/#ex-layer-events
Hi @dagjomar, would you mind sharing your solution with us 馃憤 :)
@danpe enjoy
export class LayerEventHandler {
constructor (map) {
this.map = map;
this.handlers = {};
this.defaultHandlers = {};
this._onMapEvent = this._onMapEvent.bind(this);
}
getHandleLayers (eventName){
if(!this.handlers[eventName]){
return [];
}
return Object.keys(this.handlers[eventName]);
}
on (eventName, layerId, callback){
if(!this.defaultHandlers[eventName] && !this.handlers[eventName] ){
// Create new event name keys in our storage maps
this.defaultHandlers[eventName] = [];
this.handlers[eventName] = {};
// Register a map event for the given event name
this.map.on(eventName, this._onMapEvent);
}
if(!layerId){
// layerId is not specified, so this is a 'default handler' that will be called if no other events have cancelled the event
this.defaultHandlers[eventName].push( callback );
}else{
// layerId is specified, so this is a specific handler for that layer
this.handlers[eventName][layerId] = callback;
}
}
off( eventName, layerId ){
if(this.handlers[eventName] && this.handlers[eventName][layerId] ){
this.handlers[eventName][layerId] = null;
delete this.handlers[eventName][layerId];
}
}
_onMapEvent (event) {
var layers = this.getHandleLayers(event.type); //unordered list of layers to be checked
let eventName = event.type;
// This gets the features that was clicked in the correct layer order
var eventFeatures = this.map.queryRenderedFeatures(event.point, {layers: layers });
// This makes a sorted array of the layers that are clicked
var sortedLayers = eventFeatures.reduce( (sorted, next) => {
let nextLayerId = next.layer.id;
if(sorted.indexOf(nextLayerId) === -1 ){
return sorted.concat([nextLayerId]);
}
return sorted;
}, []);
// Add the layers and features info to the event
event.eventLayers = sortedLayers;
event.eventFeatures = eventFeatures;
let bubbleEvent = true;
// Loop through each of the sorted layers starting with the first (top-most clicked layer)
// Call the handler for each layer in order, and potentially stop propagating the event
for( let i = 0; i < sortedLayers.length; i++){
if(bubbleEvent !== true){ break; }
let layerId = sortedLayers[i];
// Filter out only the features for this layer
let layerFeatures = eventFeatures.filter( (feature) => { return feature.layer.id === layerId; } );
// Call the layer handler for this layer, giving the clicked features
bubbleEvent = this.handlers[eventName][layerId](event, layerFeatures);
}
if(bubbleEvent === true){ // No events has cancelled the bubbling
if(!this.defaultHandlers[eventName]){
return;
}
this.defaultHandlers[eventName].forEach((handler) => {
handler(event);
});
}
}
}
@dagjomar How would that work for a mouseenter event on a specific layer? In your example, mouseenter events are only triggered when the mouse enters the map, not when it enters a feature in a specific layer.
@everhardt I think this only works when you use a mousemove event and not mouseenter
See example here:
https://api.mazemap.com/js/v2.0.0-beta.9/docs/#ex-layer-events
However, with the latest changes in MapBox, I'm not sure if there is better functionality for mouseenter on features now?
I'm only aware of https://github.com/mapbox/mapbox-gl-js/issues/6215, which is not implemented yet.
I guess you could combine the setFeatureState functionality with my class above, such that a "mouseenter" is basically a "mousemove" that triggers a mouseenter event IF the featureState is not set to "entered". Not sure if I explained that very good, but maybe you got the idea.
Although quite a bit of work, that looks feasible!
Many months later, is there a better way of doing this?
@asheemmamoowala Personally i think this shouldn't be a closed issue (except from the how to argument made by @mollymerp ). The above solution and the stackoverflow answer really seems like a workaround/hack to something that should be a feature of MapBox. Is it possible to re-open this issue and use it as a feature request? Or is it preferred that I create a separate issue for such a feature request?
@erex1 how do you envision a feature like this working? i think it's not necessarily obvious how it should be controlled by GL JS. i'm also having trouble imagining what we could implement that would be significantly simpler than something like the Stack Overflow solution. i created an example of that solution in JSBin but i'll put it here too for easy access for anyone who finds this ticket.
map.on('click', 'box1', function (e) {
e.originalEvent.cancelBubble = true;
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML('layer 1')
.addTo(map);
});
map.on('click', 'box2', function (e) {
if (e.originalEvent.cancelBubble) {
return;
}
new mapboxgl.Popup()
.setLngLat(e.lngLat)
.setHTML('layer 2')
.addTo(map);
});
I think it's helpful as well to understand why stopPropagation doesn't work in this instance. Remember that GeoJSON layers in GL JS are not DOM elements, but shapes drawn inside a canvas element. stopPropagation will only stop events from bubbling up from the canvas to its container elements.
@ryanhamley The stopPropagation event is not necessarily what I am after.
I basically want what @dagjomar have implemented, or something similar. So my request would be to implement something similar in the mapbox library, so that I don't have to add that code to my own codebase and maintain it across our projects, handling updates to the library and so forth.
The best solution for integrating something like this to the Mapbox library should obviously be discussed, but the perfect solution for my usecase would be if the MapMouseEvent contained a ordered list of the layers that were clicked, and that list was null or empty if no layers were clicked.
Then you have a generic way to see if some other layers were clicked, what layers were clicked, and also if no layers were clicked.
You could also do with one click listener for the whole map. Instead of adding a click listener for every layer, worry about the order of when your adding those listeners, and adding data to the originalEvent and checking it in every click listener.
@erex1 That's interesting and justifiable. I'd suggest opening a new feature request ticket outlining that approach so this discussion isn't buried here.
Something like that does it too.
You could save the event coordinates of the click on the layer and then compare these coordinates with the underlying layer and its event coordinates.
let clickCoords = {};
this.map.on('click', 'points', event => {
clickCoords = event.point;
console.log('Layer click')
});
Now, detect a click on the map, not on the points layer
this.map.on('click', () => {
// check if coords are different, if they are different, execute whatever you need
if (clickCoords.x !== event.point.x && clickCoords.y !== event.point.y) {
console.log('Basemap click');
clickCoords = {};
}
});
I have a similar issue. And I found this solution. How about this way?
In my case, it works fine.
By using queryRederedFeatures to check if there are some features with the specific event under the clicked point.
map.on('click', function(e) {
let features = map.queryRenderedFeatures(e.point, {layers:['layer1', 'layer2']});
if(features.length == 0) {
// do something
}})
});
map.on('click', 'layer1', function(e) {
// do something for layer1
});
map.on('click', 'layer2', function(e) {
// do something for layer2
});
I had the same problem recently as Mapbox events don't allow for stopPropagation so what I did was add the click handlers in the order I want clicks to propagate starting with the bottom working my way up.
EG country > state > city > county > etc
As you zoom in and the smaller items become visible and then I have some code something like this.
const debounceClickHandler = debounce((event) => { console.log('click', event) }, 10)
map.on('click', 'country', event => {
if (map.queryRenderedFeatures(event.point).some(feature => feature.source === 'country')) {
debounceClickHandler(event)
}
})
map.on('click', 'state', event => {
if (map.queryRenderedFeatures(event.point).some(feature => feature.source === 'state')) {
debounceClickHandler(event)
}
})
That way when the click finds 2 layers it will trigger country first that will be denounced then the state event will be triggered and that one will be the final one to fire in the 10ms allotted to the debounce.
mapClick(e: MapMouseEvent) {
event.stopPropagation();
console.log(e);
}
Most helpful comment
@erex1 That's interesting and justifiable. I'd suggest opening a new feature request ticket outlining that approach so this discussion isn't buried here.