import * as Browser from '../core/Browser'; import {DivOverlay} from './DivOverlay'; import {toPoint} from '../geometry/Point'; import {Map} from '../map/Map'; import {Layer} from './Layer'; import {FeatureGroup} from './FeatureGroup'; import * as Util from '../core/Util'; import * as DomUtil from '../dom/DomUtil'; /* * @class Tooltip * @inherits DivOverlay * @aka L.Tooltip * Used to display small texts on top of map layers. * * @example * * ```js * marker.bindTooltip("my tooltip text").openTooltip(); * ``` * 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 var 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 interactive: Boolean = false // If true, the tooltip will listen to the feature events. interactive: false, // @option opacity: Number = 0.9 // Tooltip container opacity. opacity: 0.9 }, onAdd: function (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) { // @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: function (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) { // @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: function () { var events = DivOverlay.prototype.getEvents.call(this); if (Browser.touch && !this.options.permanent) { events.preclick = this._close; } return events; }, _close: function () { if (this._map) { this._map.closeTooltip(this); } }, _initLayout: function () { var prefix = 'leaflet-tooltip', className = prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-' + (this._zoomAnimated ? 'animated' : 'hide'); this._contentNode = this._container = DomUtil.create('div', className); }, _updateLayout: function () {}, _adjustPan: function () {}, _setPosition: function (pos) { var map = this._map, container = this._container, centerPoint = map.latLngToContainerPoint(map.getCenter()), tooltipPoint = map.layerPointToContainerPoint(pos), direction = this.options.direction, tooltipWidth = container.offsetWidth, tooltipHeight = container.offsetHeight, offset = toPoint(this.options.offset), anchor = this._getAnchor(); if (direction === 'top') { pos = pos.add(toPoint(-tooltipWidth / 2 + offset.x, -tooltipHeight + offset.y + anchor.y, true)); } else if (direction === 'bottom') { pos = pos.subtract(toPoint(tooltipWidth / 2 - offset.x, -offset.y, true)); } else if (direction === 'center') { pos = pos.subtract(toPoint(tooltipWidth / 2 + offset.x, tooltipHeight / 2 - anchor.y + offset.y, true)); } else if (direction === 'right' || direction === 'auto' && tooltipPoint.x < centerPoint.x) { direction = 'right'; pos = pos.add(toPoint(offset.x + anchor.x, anchor.y - tooltipHeight / 2 + offset.y, true)); } else { direction = 'left'; pos = pos.subtract(toPoint(tooltipWidth + anchor.x - offset.x, tooltipHeight / 2 - anchor.y - offset.y, true)); } DomUtil.removeClass(container, 'leaflet-tooltip-right'); DomUtil.removeClass(container, 'leaflet-tooltip-left'); DomUtil.removeClass(container, 'leaflet-tooltip-top'); DomUtil.removeClass(container, 'leaflet-tooltip-bottom'); DomUtil.addClass(container, 'leaflet-tooltip-' + direction); DomUtil.setPosition(container, pos); }, _updatePosition: function () { var pos = this._map.latLngToLayerPoint(this._latlng); this._setPosition(pos); }, setOpacity: function (opacity) { this.options.opacity = opacity; if (this._container) { DomUtil.setOpacity(this._container, opacity); } }, _animateZoom: function (e) { var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center); this._setPosition(pos); }, _getAnchor: function () { // Where should we anchor the tooltip on the source layer? return toPoint(this._source && 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. export var 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: function (tooltip, latlng, options) { if (!(tooltip instanceof Tooltip)) { tooltip = new Tooltip(options).setContent(tooltip); } if (latlng) { tooltip.setLatLng(latlng); } if (this.hasLayer(tooltip)) { return this; } return this.addLayer(tooltip); }, // @method closeTooltip(tooltip?: Tooltip): this // Closes the tooltip given as parameter. closeTooltip: function (tooltip) { if (tooltip) { this.removeLayer(tooltip); } return this; } }); /* * @namespace Layer * @section Tooltip methods example * * All layers share a set of methods convenient for binding tooltips to it. * * ```js * var 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: function (content, options) { if (content instanceof Tooltip) { Util.setOptions(content, options); this._tooltip = content; content._source = this; } else { if (!this._tooltip || options) { this._tooltip = new Tooltip(options, this); } this._tooltip.setContent(content); } 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: function () { if (this._tooltip) { this._initTooltipInteractions(true); this.closeTooltip(); this._tooltip = null; } return this; }, _initTooltipInteractions: function (remove) { if (!remove && this._tooltipHandlersAdded) { return; } var onOff = remove ? 'off' : 'on', events = { remove: this.closeTooltip, move: this._moveTooltip }; if (!this._tooltip.options.permanent) { events.mouseover = this._openTooltip; events.mouseout = this.closeTooltip; if (this._tooltip.options.sticky) { events.mousemove = this._moveTooltip; } if (Browser.touch) { events.click = this._openTooltip; } } else { events.add = this._openTooltip; } 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: function (layer, latlng) { if (!(layer instanceof Layer)) { latlng = layer; layer = this; } if (layer instanceof FeatureGroup) { for (var id in this._layers) { layer = this._layers[id]; break; } } if (!latlng) { latlng = layer.getCenter ? layer.getCenter() : layer.getLatLng(); } if (this._tooltip && this._map) { // set tooltip source to this layer this._tooltip._source = layer; // update the tooltip (content, layout, ect...) this._tooltip.update(); // open the tooltip on the map this._map.openTooltip(this._tooltip, latlng); // Tooltip container may not be defined if not permanent and never // opened. if (this._tooltip.options.interactive && this._tooltip._container) { DomUtil.addClass(this._tooltip._container, 'leaflet-clickable'); this.addInteractiveTarget(this._tooltip._container); } } return this; }, // @method closeTooltip(): this // Closes the tooltip bound to this layer if it is open. closeTooltip: function () { if (this._tooltip) { this._tooltip._close(); if (this._tooltip.options.interactive && this._tooltip._container) { DomUtil.removeClass(this._tooltip._container, 'leaflet-clickable'); this.removeInteractiveTarget(this._tooltip._container); } } return this; }, // @method toggleTooltip(): this // Opens or closes the tooltip bound to this layer depending on its current state. toggleTooltip: function (target) { if (this._tooltip) { if (this._tooltip._map) { this.closeTooltip(); } else { this.openTooltip(target); } } return this; }, // @method isTooltipOpen(): boolean // Returns `true` if the tooltip bound to this layer is currently open. isTooltipOpen: function () { return this._tooltip.isOpen(); }, // @method setTooltipContent(content: String|HTMLElement|Tooltip): this // Sets the content of the tooltip bound to this layer. setTooltipContent: function (content) { if (this._tooltip) { this._tooltip.setContent(content); } return this; }, // @method getTooltip(): Tooltip // Returns the tooltip bound to this layer. getTooltip: function () { return this._tooltip; }, _openTooltip: function (e) { var layer = e.layer || e.target; if (!this._tooltip || !this._map) { return; } this.openTooltip(layer, this._tooltip.options.sticky ? e.latlng : undefined); }, _moveTooltip: function (e) { var 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); } });