Files
leaflet/src/layer/Tooltip.js
2025-05-06 20:07:11 +02:00

462 lines
13 KiB
JavaScript

import {DivOverlay} from './DivOverlay.js';
import {toPoint} 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
* @aka L.Tooltip
* 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
export const Tooltip = DivOverlay.extend({
// @section
// @aka Tooltip options
options: {
// @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 mouseover.
permanent: false,
// @option sticky: Boolean = false
// If true, the tooltip will follow the mouse 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 = toPoint(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(toPoint(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 toPoint(this._source?._getTooltipAnchor && !this.options.sticky ? this._source._getTooltipAnchor() : [0, 0]);
}
});
// @namespace Tooltip
// @factory L.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
// @factory L.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 const tooltip = function (options, source) {
return new Tooltip(options, source);
};
// @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 = L.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.mouseover = this._openTooltip;
events.mouseout = 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.mousemove = 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() {
if (this._tooltip) {
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) {
if (this._tooltip) {
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();
if (el) {
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 && 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.mouseEventToContainerPoint(e.originalEvent);
layerPoint = this._map.containerPointToLayerPoint(containerPoint);
latlng = this._map.layerPointToLatLng(layerPoint);
}
this._tooltip.setLatLng(latlng);
}
});