diff --git a/src/ui/anchor.js b/src/ui/anchor.js index 78b2bc2b99f..7a089a41b8b 100644 --- a/src/ui/anchor.js +++ b/src/ui/anchor.js @@ -22,11 +22,3 @@ export const anchorTranslate: {[_: Anchor]: string} = { 'left': 'translate(0,-50%)', 'right': 'translate(-100%,-50%)' }; - -export function applyAnchorClass(element: HTMLElement, anchor: Anchor, prefix: string) { - const classList = element.classList; - for (const key in anchorTranslate) { - classList.remove(`mapboxgl-${prefix}-anchor-${key}`); - } - classList.add(`mapboxgl-${prefix}-anchor-${anchor}`); -} diff --git a/src/ui/marker.js b/src/ui/marker.js index 1f3c3631a45..c4e234e57f4 100644 --- a/src/ui/marker.js +++ b/src/ui/marker.js @@ -6,7 +6,7 @@ import LngLat from '../geo/lng_lat.js'; import Point from '@mapbox/point-geometry'; import smartWrap from '../util/smart_wrap.js'; import {bindAll, extend} from '../util/util.js'; -import {type Anchor, anchorTranslate, applyAnchorClass} from './anchor.js'; +import {type Anchor, anchorTranslate} from './anchor.js'; import {Event, Evented} from '../util/evented.js'; import type Map from './map.js'; import type Popup from './popup.js'; @@ -233,7 +233,11 @@ export default class Marker extends Evented { // prevent focusing on click e.preventDefault(); }); - applyAnchorClass(this._element, this._anchor, 'marker'); + const classList = this._element.classList; + for (const key in anchorTranslate) { + classList.remove(`mapboxgl-marker-anchor-${key}`); + } + classList.add(`mapboxgl-marker-anchor-${this._anchor}`); this._popup = null; } diff --git a/src/ui/popup.js b/src/ui/popup.js index bc1e6f6582f..04f96e007a2 100644 --- a/src/ui/popup.js +++ b/src/ui/popup.js @@ -8,7 +8,7 @@ import LngLat from '../geo/lng_lat.js'; import Point from '@mapbox/point-geometry'; import window from '../util/window.js'; import smartWrap from '../util/smart_wrap.js'; -import {type Anchor, anchorTranslate, applyAnchorClass} from './anchor.js'; +import {type Anchor, anchorTranslate} from './anchor.js'; import type Map from './map.js'; import type {LngLatLike} from '../geo/lng_lat.js'; @@ -109,11 +109,15 @@ export default class Popup extends Evented { _lngLat: LngLat; _trackPointer: boolean; _pos: ?Point; + _anchor: Anchor; + _classList: Set; constructor(options: PopupOptions) { super(); this.options = extend(Object.create(defaultOptions), options); bindAll(['_update', '_onClose', 'remove', '_onMouseMove', '_onMouseUp', '_onDrag'], this); + this._classList = new Set(options && options.className ? + options.className.trim().split(/\s+/) : []); } /** @@ -150,9 +154,6 @@ export default class Popup extends Evented { if (this._trackPointer) { this._map.on('mousemove', this._onMouseMove); this._map.on('mouseup', this._onMouseUp); - if (this._container) { - this._container.classList.add('mapboxgl-popup-track-pointer'); - } this._map._canvasContainer.classList.add('mapboxgl-track-pointer'); } else { this._map.on('move', this._update); @@ -280,9 +281,6 @@ export default class Popup extends Evented { if (this._map) { this._map.on('move', this._update); this._map.off('mousemove', this._onMouseMove); - if (this._container) { - this._container.classList.remove('mapboxgl-popup-track-pointer'); - } this._map._canvasContainer.classList.remove('mapboxgl-track-pointer'); } @@ -308,9 +306,6 @@ export default class Popup extends Evented { this._map.off('move', this._update); this._map.on('mousemove', this._onMouseMove); this._map.on('drag', this._onDrag); - if (this._container) { - this._container.classList.add('mapboxgl-popup-track-pointer'); - } this._map._canvasContainer.classList.add('mapboxgl-track-pointer'); } @@ -451,15 +446,18 @@ export default class Popup extends Evented { * Adds a CSS class to the popup container element. * * @param {string} className Non-empty string with CSS class name to add to popup container. + * @returns {Popup} Returns itself to allow for method chaining. * * @example * const popup = new mapboxgl.Popup(); * popup.addClassName('some-class'); */ addClassName(className: string) { + this._classList.add(className); if (this._container) { - this._container.classList.add(className); + this._updateClassList(); } + return this; } /** @@ -467,14 +465,17 @@ export default class Popup extends Evented { * * @param {string} className Non-empty string with CSS class name to remove from popup container. * + * @returns {Popup} Returns itself to allow for method chaining. * @example - * const popup = new mapboxgl.Popup(); - * popup.removeClassName('some-class'); + * const popup = new mapboxgl.Popup({className: 'some classes'}); + * popup.removeClassName('some'); */ removeClassName(className: string) { + this._classList.delete(className); if (this._container) { - this._container.classList.remove(className); + this._updateClassList(); } + return this; } /** @@ -512,12 +513,20 @@ export default class Popup extends Evented { * * @example * const popup = new mapboxgl.Popup(); - * popup.toggleClassName('toggleClass'); + * popup.toggleClassName('highlighted'); */ toggleClassName(className: string) { + let finalState: boolean; + if (this._classList.delete(className)) { + finalState = false; + } else { + this._classList.add(className); + finalState = true; + } if (this._container) { - return this._container.classList.toggle(className); + this._updateClassList(); } + return finalState; } _createCloseButton() { @@ -542,6 +551,47 @@ export default class Popup extends Evented { this._update(event.point); } + _getAnchor(offset: any) { + if (this.options.anchor) { return this.options.anchor; } + + const pos: any = this._pos; + const width = this._container.offsetWidth; + const height = this._container.offsetHeight; + let anchorComponents; + + if (pos.y + offset.bottom.y < height) { + anchorComponents = ['top']; + } else if (pos.y > this._map.transform.height - height) { + anchorComponents = ['bottom']; + } else { + anchorComponents = []; + } + + if (pos.x < width / 2) { + anchorComponents.push('left'); + } else if (pos.x > this._map.transform.width - width / 2) { + anchorComponents.push('right'); + } + + if (anchorComponents.length === 0) { + return 'bottom'; + } + return ((anchorComponents.join('-'): any): Anchor); + + } + + _updateClassList() { + const classes = [...this._classList]; + classes.push('mapboxgl-popup'); + if (this._anchor) { + classes.push(`mapboxgl-popup-anchor-${this._anchor}`); + } + if (this._trackPointer) { + classes.push('mapboxgl-popup-track-pointer'); + } + this._container.className = classes.join(' '); + } + _update(cursor: ?PointLike) { const hasPosition = this._lngLat || this._trackPointer; @@ -551,14 +601,6 @@ export default class Popup extends Evented { this._container = DOM.create('div', 'mapboxgl-popup', this._map.getContainer()); this._tip = DOM.create('div', 'mapboxgl-popup-tip', this._container); this._container.appendChild(this._content); - if (this.options.className) { - this.options.className.split(' ').forEach(name => - this._container.classList.add(name)); - } - - if (this._trackPointer) { - this._container.classList.add('mapboxgl-popup-track-pointer'); - } } if (this.options.maxWidth && this._container.style.maxWidth !== this.options.maxWidth) { @@ -569,46 +611,21 @@ export default class Popup extends Evented { this._lngLat = smartWrap(this._lngLat, this._pos, this._map.transform); } - if (this._trackPointer && !cursor) return; + if (!this._trackPointer || cursor) { + const pos = this._pos = this._trackPointer && cursor ? cursor : this._map.project(this._lngLat); - const pos = this._pos = this._trackPointer && cursor ? cursor : this._map.project(this._lngLat); + const offset = normalizeOffset(this.options.offset); + const anchor = this._anchor = this._getAnchor(offset); - let anchor: ?Anchor = this.options.anchor; - const offset = normalizeOffset(this.options.offset); - - if (!anchor) { - const width = this._container.offsetWidth; - const height = this._container.offsetHeight; - let anchorComponents; - - if (pos.y + offset.bottom.y < height) { - anchorComponents = ['top']; - } else if (pos.y > this._map.transform.height - height) { - anchorComponents = ['bottom']; - } else { - anchorComponents = []; - } - - if (pos.x < width / 2) { - anchorComponents.push('left'); - } else if (pos.x > this._map.transform.width - width / 2) { - anchorComponents.push('right'); - } - - if (anchorComponents.length === 0) { - anchor = 'bottom'; - } else { - anchor = (anchorComponents.join('-'): any); - } + const offsetedPos = pos.add(offset[anchor]).round(); + this._map._requestDomTask(() => { + if (this._container && anchor) { + DOM.setTransform(this._container, `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`); + } + }); } - const offsetedPos = pos.add(offset[anchor]).round(); - this._map._requestDomTask(() => { - if (this._container && anchor) { - DOM.setTransform(this._container, `${anchorTranslate[anchor]} translate(${offsetedPos.x}px,${offsetedPos.y}px)`); - applyAnchorClass(this._container, anchor, 'popup'); - } - }); + this._updateClassList(); } _focusFirstElement() { @@ -630,10 +647,9 @@ export default class Popup extends Evented { } function normalizeOffset(offset: ?Offset) { - if (!offset) { - return normalizeOffset(new Point(0, 0)); + if (!offset) offset = (new Point(0, 0)); - } else if (typeof offset === 'number') { + if (typeof offset === 'number') { // input specifies a radius from which to calculate offsets at all positions const cornerOffset = Math.round(Math.sqrt(0.5 * Math.pow(offset, 2))); return { diff --git a/test/unit/ui/popup.test.js b/test/unit/ui/popup.test.js index be48373c1a2..2073e14d3b4 100644 --- a/test/unit/ui/popup.test.js +++ b/test/unit/ui/popup.test.js @@ -576,7 +576,7 @@ test('Popup#remove is idempotent (#2395)', (t) => { t.end(); }); -test('Popup adds classes from className option, methods for class manipulations works properly', (t) => { +test('Popup adds classes from className option, methods for class manipulation work properly', (t) => { const map = createMap(t); const popup = new Popup({className: 'some classes'}) .setText('Test') @@ -599,14 +599,74 @@ test('Popup adds classes from className option, methods for class manipulations popup.toggleClassName('toggle'); t.ok(!popupContainer.classList.contains('toggle')); - t.throws(() => popup.addClassName('should throw exception'), window.DOMException); - t.throws(() => popup.removeClassName('should throw exception'), window.DOMException); - t.throws(() => popup.toggleClassName('should throw exception'), window.DOMException); + t.end(); +}); + +test('Popup#addClassName adds classes when called before adding popup to map (#9677)', (t) => { + const map = createMap(t); + const popup = new Popup(); + popup.addClassName('some'); + popup.addClassName('classes'); + + popup.setText('Test') + .setLngLat([0, 0]) + .addTo(map); + + const popupContainer = popup.getElement(); + t.ok(popupContainer.classList.contains('some')); + t.ok(popupContainer.classList.contains('classes')); + t.end(); +}); +test('Popup className option and addClassName both add classes', (t) => { + const map = createMap(t); + const popup = new Popup({className: 'some classes'}); + popup.addClassName('even') + .addClassName('more'); + + popup.setText('Test') + .setLngLat([0, 0]) + .addTo(map); + + popup.addClassName('one-more'); + + const popupContainer = popup.getElement(); + t.ok(popupContainer.classList.contains('some')); + t.ok(popupContainer.classList.contains('classes')); + t.ok(popupContainer.classList.contains('even')); + t.ok(popupContainer.classList.contains('more')); + t.ok(popupContainer.classList.contains('one-more')); + t.end(); +}); + +test('Methods for class manipulation work properly when popup is not on map', (t) => { + const map = createMap(t); + const popup = new Popup() + .setText('Test') + .setLngLat([0, 0]) + .addClassName('some') + .addClassName('classes'); + + let popupContainer = popup.addTo(map).getElement(); + t.ok(popupContainer.classList.contains('some')); + t.ok(popupContainer.classList.contains('classes')); + + popup.remove(); + popup.removeClassName('some'); + popupContainer = popup.addTo(map).getElement(); + + t.ok(!popupContainer.classList.contains('some')); + + popup.remove(); + popup.toggleClassName('toggle'); + popupContainer = popup.addTo(map).getElement(); - t.throws(() => popup.addClassName(''), window.DOMException); - t.throws(() => popup.removeClassName(''), window.DOMException); - t.throws(() => popup.toggleClassName(''), window.DOMException); + t.ok(popupContainer.classList.contains('toggle')); + popup.remove(); + popup.toggleClassName('toggle'); + popupContainer = popup.addTo(map).getElement(); + + t.ok(!popupContainer.classList.contains('toggle')); t.end(); });