UNPKG

leaflet

Version:

JavaScript library for mobile-friendly interactive maps

452 lines (383 loc) 12.9 kB
import {DivOverlay} from './DivOverlay.js'; import {Point} from '../geometry/Point.js'; import {Map} from '../map/Map.js'; import {Layer} from './Layer.js'; import * as DomUtil from '../dom/DomUtil.js'; import * as DomEvent from '../dom/DomEvent.js'; import * as Util from '../core/Util.js'; import {FeatureGroup} from './FeatureGroup.js'; /* * @class Tooltip * @inherits DivOverlay * Used to display small texts on top of map layers. * * @example * If you want to just bind a tooltip to marker: * * ```js * marker.bindTooltip("my tooltip text").openTooltip(); * ``` * Path overlays like polylines also have a `bindTooltip` method. * * A tooltip can be also standalone: * * ```js * const tooltip = new Tooltip() * .setLatLng(latlng) * .setContent('Hello world!<br />This is a nice tooltip.') * .addTo(map); * ``` * or * ```js * const tooltip = new Tooltip(latlng, {content: 'Hello world!<br />This is a nice tooltip.'}) * .addTo(map); * ``` * * * Note about tooltip offset. Leaflet takes two options in consideration * for computing tooltip offsetting: * - the `offset` Tooltip option: it defaults to [0, 0], and it's specific to one tooltip. * Add a positive x offset to move the tooltip to the right, and a positive y offset to * move it to the bottom. Negatives will move to the left and top. * - the `tooltipAnchor` Icon option: this will only be considered for Marker. You * should adapt this value if you use a custom icon. */ // @namespace Tooltip // @constructor Tooltip(options?: Tooltip options, source?: Layer) // Instantiates a `Tooltip` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the tooltip with a reference to the Layer to which it refers. // @alternative // @constructor Tooltip(latlng: LatLng, options?: Tooltip options) // Instantiates a `Tooltip` object given `latlng` where the tooltip will open and an optional `options` object that describes its appearance and location. export class Tooltip extends DivOverlay { static { // @section // @aka Tooltip options this.setDefaultOptions({ // @option pane: String = 'tooltipPane' // `Map pane` where the tooltip will be added. pane: 'tooltipPane', // @option offset: Point = Point(0, 0) // Optional offset of the tooltip position. offset: [0, 0], // @option direction: String = 'auto' // Direction where to open the tooltip. Possible values are: `right`, `left`, // `top`, `bottom`, `center`, `auto`. // `auto` will dynamically switch between `right` and `left` according to the tooltip // position on the map. direction: 'auto', // @option permanent: Boolean = false // Whether to open the tooltip permanently or only on pointerover. permanent: false, // @option sticky: Boolean = false // If true, the tooltip will follow the pointer instead of being fixed at the feature center. sticky: false, // @option opacity: Number = 0.9 // Tooltip container opacity. opacity: 0.9 }); } onAdd(map) { DivOverlay.prototype.onAdd.call(this, map); this.setOpacity(this.options.opacity); // @namespace Map // @section Tooltip events // @event tooltipopen: TooltipEvent // Fired when a tooltip is opened in the map. map.fire('tooltipopen', {tooltip: this}); if (this._source) { this.addEventParent(this._source); // @namespace Layer // @section Tooltip events // @event tooltipopen: TooltipEvent // Fired when a tooltip bound to this layer is opened. this._source.fire('tooltipopen', {tooltip: this}, true); } } onRemove(map) { DivOverlay.prototype.onRemove.call(this, map); // @namespace Map // @section Tooltip events // @event tooltipclose: TooltipEvent // Fired when a tooltip in the map is closed. map.fire('tooltipclose', {tooltip: this}); if (this._source) { this.removeEventParent(this._source); // @namespace Layer // @section Tooltip events // @event tooltipclose: TooltipEvent // Fired when a tooltip bound to this layer is closed. this._source.fire('tooltipclose', {tooltip: this}, true); } } getEvents() { const events = DivOverlay.prototype.getEvents.call(this); if (!this.options.permanent) { events.preclick = this.close; } return events; } _initLayout() { const prefix = 'leaflet-tooltip', className = `${prefix} ${this.options.className || ''} leaflet-zoom-${this._zoomAnimated ? 'animated' : 'hide'}`; this._contentNode = this._container = DomUtil.create('div', className); this._container.setAttribute('role', 'tooltip'); this._container.setAttribute('id', `leaflet-tooltip-${Util.stamp(this)}`); } _updateLayout() {} _adjustPan() {} _setPosition(pos) { let subX, subY, direction = this.options.direction; const map = this._map, container = this._container, centerPoint = map.latLngToContainerPoint(map.getCenter()), tooltipPoint = map.layerPointToContainerPoint(pos), tooltipWidth = container.offsetWidth, tooltipHeight = container.offsetHeight, offset = new Point(this.options.offset), anchor = this._getAnchor(); if (direction === 'top') { subX = tooltipWidth / 2; subY = tooltipHeight; } else if (direction === 'bottom') { subX = tooltipWidth / 2; subY = 0; } else if (direction === 'center') { subX = tooltipWidth / 2; subY = tooltipHeight / 2; } else if (direction === 'right') { subX = 0; subY = tooltipHeight / 2; } else if (direction === 'left') { subX = tooltipWidth; subY = tooltipHeight / 2; } else if (tooltipPoint.x < centerPoint.x) { direction = 'right'; subX = 0; subY = tooltipHeight / 2; } else { direction = 'left'; subX = tooltipWidth + (offset.x + anchor.x) * 2; subY = tooltipHeight / 2; } pos = pos.subtract(new Point(subX, subY, true)).add(offset).add(anchor); container.classList.remove( 'leaflet-tooltip-right', 'leaflet-tooltip-left', 'leaflet-tooltip-top', 'leaflet-tooltip-bottom' ); container.classList.add(`leaflet-tooltip-${direction}`); DomUtil.setPosition(container, pos); } _updatePosition() { const pos = this._map.latLngToLayerPoint(this._latlng); this._setPosition(pos); } setOpacity(opacity) { this.options.opacity = opacity; if (this._container) { this._container.style.opacity = opacity; } } _animateZoom(e) { const pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); this._setPosition(pos); } _getAnchor() { // Where should we anchor the tooltip on the source layer? return new Point(this._source?._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]); } } // @namespace Map // @section Methods for Layers and Controls Map.include({ // @method openTooltip(tooltip: Tooltip): this // Opens the specified tooltip. // @alternative // @method openTooltip(content: String|HTMLElement, latlng: LatLng, options?: Tooltip options): this // Creates a tooltip with the specified content and options and open it. openTooltip(tooltip, latlng, options) { this._initOverlay(Tooltip, tooltip, latlng, options) .openOn(this); return this; }, // @method closeTooltip(tooltip: Tooltip): this // Closes the tooltip given as parameter. closeTooltip(tooltip) { tooltip.close(); return this; } }); /* * @namespace Layer * @section Tooltip methods example * * All layers share a set of methods convenient for binding tooltips to it. * * ```js * const layer = new Polygon(latlngs).bindTooltip('Hi There!').addTo(map); * layer.openTooltip(); * layer.closeTooltip(); * ``` */ // @section Tooltip methods Layer.include({ // @method bindTooltip(content: String|HTMLElement|Function|Tooltip, options?: Tooltip options): this // Binds a tooltip to the layer with the passed `content` and sets up the // necessary event listeners. If a `Function` is passed it will receive // the layer as the first argument and should return a `String` or `HTMLElement`. bindTooltip(content, options) { if (this._tooltip && this.isTooltipOpen()) { this.unbindTooltip(); } this._tooltip = this._initOverlay(Tooltip, this._tooltip, content, options); this._initTooltipInteractions(); if (this._tooltip.options.permanent && this._map && this._map.hasLayer(this)) { this.openTooltip(); } return this; }, // @method unbindTooltip(): this // Removes the tooltip previously bound with `bindTooltip`. unbindTooltip() { if (this._tooltip) { this._initTooltipInteractions(true); this.closeTooltip(); this._tooltip = null; } return this; }, _initTooltipInteractions(remove) { if (!remove && this._tooltipHandlersAdded) { return; } const onOff = remove ? 'off' : 'on', events = { remove: this.closeTooltip, move: this._moveTooltip }; if (!this._tooltip.options.permanent) { events.pointerover = this._openTooltip; events.pointerout = this.closeTooltip; events.click = this._openTooltip; if (this._map) { this._addFocusListeners(remove); } else { events.add = () => this._addFocusListeners(remove); } } else { events.add = this._openTooltip; } if (this._tooltip.options.sticky) { events.pointermove = this._moveTooltip; } this[onOff](events); this._tooltipHandlersAdded = !remove; }, // @method openTooltip(latlng?: LatLng): this // Opens the bound tooltip at the specified `latlng` or at the default tooltip anchor if no `latlng` is passed. openTooltip(latlng) { if (this._tooltip) { if (!(this instanceof FeatureGroup)) { this._tooltip._source = this; } if (this._tooltip._prepareOpen(latlng)) { // open the tooltip on the map this._tooltip.openOn(this._map); if (this.getElement) { this._setAriaDescribedByOnLayer(this); } else if (this.eachLayer) { this.eachLayer(this._setAriaDescribedByOnLayer, this); } } } return this; }, // @method closeTooltip(): this // Closes the tooltip bound to this layer if it is open. closeTooltip() { if (this._tooltip) { return this._tooltip.close(); } }, // @method toggleTooltip(): this // Opens or closes the tooltip bound to this layer depending on its current state. toggleTooltip() { this._tooltip?.toggle(this); return this; }, // @method isTooltipOpen(): boolean // Returns `true` if the tooltip bound to this layer is currently open. isTooltipOpen() { return this._tooltip.isOpen(); }, // @method setTooltipContent(content: String|HTMLElement|Tooltip): this // Sets the content of the tooltip bound to this layer. setTooltipContent(content) { this._tooltip?.setContent(content); return this; }, // @method getTooltip(): Tooltip // Returns the tooltip bound to this layer. getTooltip() { return this._tooltip; }, _addFocusListeners(remove) { if (this.getElement) { this._addFocusListenersOnLayer(this, remove); } else if (this.eachLayer) { this.eachLayer(layer => this._addFocusListenersOnLayer(layer, remove), this); } }, _addFocusListenersOnLayer(layer, remove) { const el = typeof layer.getElement === 'function' && layer.getElement(); if (el) { const onOff = remove ? 'off' : 'on'; if (!remove) { // Remove focus listener, if already existing el._leaflet_focus_handler && DomEvent.off(el, 'focus', el._leaflet_focus_handler, this); // eslint-disable-next-line camelcase el._leaflet_focus_handler = () => { if (this._tooltip) { this._tooltip._source = layer; this.openTooltip(); } }; } el._leaflet_focus_handler && DomEvent[onOff](el, 'focus', el._leaflet_focus_handler, this); DomEvent[onOff](el, 'blur', this.closeTooltip, this); if (remove) { delete el._leaflet_focus_handler; } } }, _setAriaDescribedByOnLayer(layer) { const el = typeof layer.getElement === 'function' && layer.getElement(); el?.setAttribute?.('aria-describedby', this._tooltip._container.id); }, _openTooltip(e) { if (!this._tooltip || !this._map) { return; } // If the map is moving, we will show the tooltip after it's done. if (this._map.dragging?.moving()) { if (e.type === 'add' && !this._moveEndOpensTooltip) { this._moveEndOpensTooltip = true; this._map.once('moveend', () => { this._moveEndOpensTooltip = false; this._openTooltip(e); }); } return; } this._tooltip._source = e.propagatedFrom ?? e.target; this.openTooltip(this._tooltip.options.sticky ? e.latlng : undefined); }, _moveTooltip(e) { let latlng = e.latlng, containerPoint, layerPoint; if (this._tooltip.options.sticky && e.originalEvent) { containerPoint = this._map.pointerEventToContainerPoint(e.originalEvent); layerPoint = this._map.containerPointToLayerPoint(containerPoint); latlng = this._map.layerPointToLatLng(layerPoint); } this._tooltip.setLatLng(latlng); } });