Skip to content
Open
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
11 changes: 11 additions & 0 deletions jest.stub.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,31 @@

globalThis.mockMapGL = {
on: jest.fn(),
once: jest.fn(),
off: jest.fn(),
addLayer: jest.fn(),
addSource: jest.fn(),
addControl: jest.fn(),
removeLayer: jest.fn(),
removeSource: jest.fn(),
getLayer: jest.fn(),
getSource: jest.fn(),
getStyle: jest.fn(() => ({ layers: [] })),
setFeatureState: jest.fn(),
setFilter: jest.fn(),
setPaintProperty: jest.fn(),
setLayoutProperty: jest.fn(),
moveLayer: jest.fn(),
getCanvas: jest.fn(() => ({ style: {} })),
_getUIString: jest.fn(),
}

globalThis.mockMap = {
getMapGL: () => globalThis.mockMapGL,
setHoverState: jest.fn(),
setSelectedState: jest.fn(),
getBeforeLayerId: jest.fn(),
invalidateInteractiveLayerIds: jest.fn(),
styleIsLoaded: () => true,
}

Expand Down
142 changes: 113 additions & 29 deletions src/Map.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export class MapGL extends Evented {
this._glyphs = glyphs
this._renderTimeout = null
this._mouseMoveEnabled = true
this._hoveredLayer = null
this._hoveredFeatureId = null
this._selectedFeatures = null
this._hoverStateKey = null
this._selectedStateKey = null

// Translate strings
if (locale) {
Expand All @@ -68,7 +73,7 @@ export class MapGL extends Evented {
mapgl.on('load', this.onLoad)
mapgl.on('click', this.onClick)
mapgl.on('contextmenu', this.onContextMenu)
mapgl.on('mousemove', this.onMouseMove.bind(this))
mapgl.on('mousemove', this.onMouseMove)
mapgl.on('mouseout', this.onMouseOut)
mapgl.on('error', this.onError)
/* Data and dataloading events are an indication that
Expand Down Expand Up @@ -266,32 +271,65 @@ export class MapGL extends Evented {
}

const feature = this.getEventFeature(evt)
let layer
const hoverTarget = feature
? this._resolveHoverTarget(evt, feature)
: null

if (feature) {
layer = this.getLayerFromId(feature.layer.id)
if (!feature) {
this._hideLabelUnlessOverOverlay(evt)
}

if (layer) {
layer.onMouseMove(evt, feature)
}
} else {
const target = evt && evt.originalEvent && evt.originalEvent.target
if (
!target ||
!target.closest ||
!OVERLAY_SELECTORS.some(sel => target.closest(sel))
) {
this.hideLabel()
}
this._updateHoveredLayer(hoverTarget, feature)

this.getMapGL().getCanvas().style.cursor = feature ? 'pointer' : ''
}

_resolveHoverTarget(evt, feature) {
const layer = this.getLayerFromId(feature.layer.id)

if (!layer) {
return null
}

this.setHoverState(
layer && feature?.properties?.id
? layer.getFeaturesById(feature.properties.id)
: null
layer.onMouseMove(evt, feature)

return (
(typeof layer.getSubLayerFromId === 'function' &&
layer.getSubLayerFromId(feature.layer.id)) ||
layer
)
}

this.getMapGL().getCanvas().style.cursor = feature ? 'pointer' : ''
_hideLabelUnlessOverOverlay(evt) {
const target = evt?.originalEvent?.target
const isOverOverlay =
target?.closest &&
OVERLAY_SELECTORS.some(sel => target.closest(sel))

if (!isOverOverlay) {
this.hideLabel()
}
}

_updateHoveredLayer(hoverTarget, feature) {
const featureId = feature?.properties?.id ?? null
const nextHover = hoverTarget && featureId !== null ? hoverTarget : null

if (
nextHover === this._hoveredLayer &&
featureId === this._hoveredFeatureId
) {
return
}

if (this._hoveredLayer) {
this._hoveredLayer.fire('mouseleave')
}
if (nextHover) {
nextHover.fire('mouseenter', { feature })
}
this._hoveredLayer = nextHover
this._hoveredFeatureId = nextHover ? featureId : null
}

// Remove rendered class if rendering is happening
Expand All @@ -310,12 +348,12 @@ export class MapGL extends Evented {
}

// Set hover state for features
setHoverState(features) {
// Only set hover state when features are changed
if (
getFeaturesString(features) !==
getFeaturesString(this._hoverFeatures)
) {
setHoverState(features, color) {
const key = `${getFeaturesString(features)}|${color ?? ''}`

if (key !== this._hoverStateKey) {
this._hoverStateKey = key

if (this._hoverFeatures) {
// Clear state for existing hover features
this._hoverFeatures.forEach(feature =>
Expand All @@ -327,7 +365,40 @@ export class MapGL extends Evented {
if (Array.isArray(features)) {
this._hoverFeatures = features
features.forEach(feature =>
this.setFeatureState(feature, { hover: true })
this.setFeatureState(
feature,
color !== undefined
? { hover: true, highlightColor: color }
: { hover: true }
)
)
}
}
}

// Set selected state for features
setSelectedState(features, color) {
const key = `${getFeaturesString(features)}|${color ?? ''}`

if (key !== this._selectedStateKey) {
this._selectedStateKey = key

if (this._selectedFeatures) {
this._selectedFeatures.forEach(feature =>
this.setFeatureState(feature, { selected: false })
)
this._selectedFeatures = null
}

if (Array.isArray(features)) {
this._selectedFeatures = features
features.forEach(feature =>
this.setFeatureState(
feature,
color !== undefined
? { selected: true, highlightColor: color }
: { selected: true }
)
)
}
}
Expand All @@ -354,6 +425,12 @@ export class MapGL extends Evented {
return
}
this.hideLabel()

if (this._hoveredLayer) {
this._hoveredLayer.fire('mouseleave')
this._hoveredLayer = null
this._hoveredFeatureId = null
}
}

onError = evt => {
Expand All @@ -373,6 +450,11 @@ export class MapGL extends Evented {
return this.getMapGL().getZoom()
}

// Clears the cached interactive layer ids so they're recomputed on next use
invalidateInteractiveLayerIds() {
this._interactiveLayerIds = null
}

getEventFeature(evt) {
if (!this._interactiveLayerIds) {
this._interactiveLayerIds = this.getLayers()
Expand Down Expand Up @@ -513,8 +595,10 @@ export class MapGL extends Evented {
const { x, y } = this.getMapGL().project(lngLat)
const position = [x, y]
const feature = this.getEventFeature(evt)
// Lets consumers distinguish a plain click from a ctrl/cmd-click.
const { ctrlKey = false, metaKey = false } = evt.originalEvent || {}

return { type, coordinates, position, feature }
return { type, coordinates, position, feature, ctrlKey, metaKey }
}

_setRenderTimeout() {
Expand Down
Loading
Loading