mirror of
https://github.com/Leaflet/Leaflet.git
synced 2025-07-23 00:34:55 +00:00
462 lines
13 KiB
JavaScript
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);
|
|
}
|
|
});
|