import {DivOverlay} from './DivOverlay'; import * as DomEvent from '../dom/DomEvent'; import * as DomUtil from '../dom/DomUtil'; import {Point, toPoint} from '../geometry/Point'; import {Map} from '../map/Map'; import {Layer} from './Layer'; import {FeatureGroup} from './FeatureGroup'; import * as Util from '../core/Util'; import {Path} from './vector/Path'; /* * @class Popup * @inherits DivOverlay * @aka L.Popup * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to * open popups while making sure that only one popup is open at one time * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want. * * @example * * If you want to just bind a popup to marker click and then open it, it's really easy: * * ```js * marker.bindPopup(popupContent).openPopup(); * ``` * Path overlays like polylines also have a `bindPopup` method. * Here's a more complicated way to open a popup on a map: * * ```js * var popup = L.popup() * .setLatLng(latlng) * .setContent('

Hello world!
This is a nice popup.

') * .openOn(map); * ``` */ // @namespace Popup export var Popup = DivOverlay.extend({ // @section // @aka Popup options options: { // @option maxWidth: Number = 300 // Max width of the popup, in pixels. maxWidth: 300, // @option minWidth: Number = 50 // Min width of the popup, in pixels. minWidth: 50, // @option maxHeight: Number = null // If set, creates a scrollable container of the given height // inside a popup if its content exceeds it. maxHeight: null, // @option autoPan: Boolean = true // Set it to `false` if you don't want the map to do panning animation // to fit the opened popup. autoPan: true, // @option autoPanPaddingTopLeft: Point = null // The margin between the popup and the top left corner of the map // view after autopanning was performed. autoPanPaddingTopLeft: null, // @option autoPanPaddingBottomRight: Point = null // The margin between the popup and the bottom right corner of the map // view after autopanning was performed. autoPanPaddingBottomRight: null, // @option autoPanPadding: Point = Point(5, 5) // Equivalent of setting both top left and bottom right autopan padding to the same value. autoPanPadding: [5, 5], // @option keepInView: Boolean = false // Set it to `true` if you want to prevent users from panning the popup // off of the screen while it is open. keepInView: false, // @option closeButton: Boolean = true // Controls the presence of a close button in the popup. closeButton: true, // @option autoClose: Boolean = true // Set it to `false` if you want to override the default behavior of // the popup closing when another popup is opened. autoClose: true, // @option closeOnEscapeKey: Boolean = true // Set it to `false` if you want to override the default behavior of // the ESC key for closing of the popup. closeOnEscapeKey: true, // @option closeOnClick: Boolean = * // Set it if you want to override the default behavior of the popup closing when user clicks // on the map. Defaults to the map's [`closePopupOnClick`](#map-closepopuponclick) option. // @option className: String = '' // A custom CSS class name to assign to the popup. className: '' }, // @namespace Popup // @method openOn(map: Map): this // Adds the popup to the map and closes the previous one. The same as `map.openPopup(popup)`. openOn: function (map) { map.openPopup(this); return this; }, onAdd: function (map) { DivOverlay.prototype.onAdd.call(this, map); // @namespace Map // @section Popup events // @event popupopen: PopupEvent // Fired when a popup is opened in the map map.fire('popupopen', {popup: this}); if (this._source) { // @namespace Layer // @section Popup events // @event popupopen: PopupEvent // Fired when a popup bound to this layer is opened this._source.fire('popupopen', {popup: this}, true); // For non-path layers, we toggle the popup when clicking // again the layer, so prevent the map to reopen it. if (!(this._source instanceof Path)) { this._source.on('preclick', DomEvent.stopPropagation); } } }, onRemove: function (map) { DivOverlay.prototype.onRemove.call(this, map); // @namespace Map // @section Popup events // @event popupclose: PopupEvent // Fired when a popup in the map is closed map.fire('popupclose', {popup: this}); if (this._source) { // @namespace Layer // @section Popup events // @event popupclose: PopupEvent // Fired when a popup bound to this layer is closed this._source.fire('popupclose', {popup: this}, true); if (!(this._source instanceof Path)) { this._source.off('preclick', DomEvent.stopPropagation); } } }, getEvents: function () { var events = DivOverlay.prototype.getEvents.call(this); if (this.options.closeOnClick !== undefined ? this.options.closeOnClick : this._map.options.closePopupOnClick) { events.preclick = this._close; } if (this.options.keepInView) { events.moveend = this._adjustPan; } return events; }, _close: function () { if (this._map) { this._map.closePopup(this); } }, _initLayout: function () { var prefix = 'leaflet-popup', container = this._container = DomUtil.create('div', prefix + ' ' + (this.options.className || '') + ' leaflet-zoom-animated'); var wrapper = this._wrapper = DomUtil.create('div', prefix + '-content-wrapper', container); this._contentNode = DomUtil.create('div', prefix + '-content', wrapper); DomEvent.disableClickPropagation(wrapper); DomEvent.disableScrollPropagation(this._contentNode); DomEvent.on(wrapper, 'contextmenu', DomEvent.stopPropagation); this._tipContainer = DomUtil.create('div', prefix + '-tip-container', container); this._tip = DomUtil.create('div', prefix + '-tip', this._tipContainer); if (this.options.closeButton) { var closeButton = this._closeButton = DomUtil.create('a', prefix + '-close-button', container); closeButton.href = '#close'; closeButton.innerHTML = '×'; DomEvent.on(closeButton, 'click', this._onCloseButtonClick, this); } }, _updateLayout: function () { var container = this._contentNode, style = container.style; style.width = ''; style.whiteSpace = 'nowrap'; var width = container.offsetWidth; width = Math.min(width, this.options.maxWidth); width = Math.max(width, this.options.minWidth); style.width = (width + 1) + 'px'; style.whiteSpace = ''; style.height = ''; var height = container.offsetHeight, maxHeight = this.options.maxHeight, scrolledClass = 'leaflet-popup-scrolled'; if (maxHeight && height > maxHeight) { style.height = maxHeight + 'px'; DomUtil.addClass(container, scrolledClass); } else { DomUtil.removeClass(container, scrolledClass); } this._containerWidth = this._container.offsetWidth; }, _animateZoom: function (e) { var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center), anchor = this._getAnchor(); DomUtil.setPosition(this._container, pos.add(anchor)); }, _adjustPan: function () { if (!this.options.autoPan || (this._map._panAnim && this._map._panAnim._inProgress)) { return; } var map = this._map, marginBottom = parseInt(DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0, containerHeight = this._container.offsetHeight + marginBottom, containerWidth = this._containerWidth, layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom); layerPos._add(DomUtil.getPosition(this._container)); var containerPos = map.layerPointToContainerPoint(layerPos), padding = toPoint(this.options.autoPanPadding), paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding), paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding), size = map.getSize(), dx = 0, dy = 0; if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right dx = containerPos.x + containerWidth - size.x + paddingBR.x; } if (containerPos.x - dx - paddingTL.x < 0) { // left dx = containerPos.x - paddingTL.x; } if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom dy = containerPos.y + containerHeight - size.y + paddingBR.y; } if (containerPos.y - dy - paddingTL.y < 0) { // top dy = containerPos.y - paddingTL.y; } // @namespace Map // @section Popup events // @event autopanstart: Event // Fired when the map starts autopanning when opening a popup. if (dx || dy) { map .fire('autopanstart') .panBy([dx, dy]); } }, _onCloseButtonClick: function (e) { this._close(); DomEvent.stop(e); }, _getAnchor: function () { // Where should we anchor the popup on the source layer? return toPoint(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]); } }); // @namespace Popup // @factory L.popup(options?: Popup options, source?: Layer) // Instantiates a `Popup` object given an optional `options` object that describes its appearance and location and an optional `source` object that is used to tag the popup with a reference to the Layer to which it refers. export var popup = function (options, source) { return new Popup(options, source); }; /* @namespace Map * @section Interaction Options * @option closePopupOnClick: Boolean = true * Set it to `false` if you don't want popups to close when user clicks the map. */ Map.mergeOptions({ closePopupOnClick: true }); // @namespace Map // @section Methods for Layers and Controls Map.include({ // @method openPopup(popup: Popup): this // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability). // @alternative // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this // Creates a popup with the specified content and options and opens it in the given point on a map. openPopup: function (popup, latlng, options) { if (!(popup instanceof Popup)) { popup = new Popup(options).setContent(popup); } if (latlng) { popup.setLatLng(latlng); } if (this.hasLayer(popup)) { return this; } if (this._popup && this._popup.options.autoClose) { this.closePopup(); } this._popup = popup; return this.addLayer(popup); }, // @method closePopup(popup?: Popup): this // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one). closePopup: function (popup) { if (!popup || popup === this._popup) { popup = this._popup; this._popup = null; } if (popup) { this.removeLayer(popup); } return this; } }); /* * @namespace Layer * @section Popup methods example * * All layers share a set of methods convenient for binding popups to it. * * ```js * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map); * layer.openPopup(); * layer.closePopup(); * ``` * * Popups will also be automatically opened when the layer is clicked on and closed when the layer is removed from the map or another popup is opened. */ // @section Popup methods Layer.include({ // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this // Binds a popup 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`. bindPopup: function (content, options) { if (content instanceof Popup) { Util.setOptions(content, options); this._popup = content; content._source = this; } else { if (!this._popup || options) { this._popup = new Popup(options, this); } this._popup.setContent(content); } if (!this._popupHandlersAdded) { this.on({ click: this._openPopup, keypress: this._onKeyPress, remove: this.closePopup, move: this._movePopup }); this._popupHandlersAdded = true; } return this; }, // @method unbindPopup(): this // Removes the popup previously bound with `bindPopup`. unbindPopup: function () { if (this._popup) { this.off({ click: this._openPopup, keypress: this._onKeyPress, remove: this.closePopup, move: this._movePopup }); this._popupHandlersAdded = false; this._popup = null; } return this; }, // @method openPopup(latlng?: LatLng): this // Opens the bound popup at the specified `latlng` or at the default popup anchor if no `latlng` is passed. openPopup: 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._popup && this._map) { // set popup source to this layer this._popup._source = layer; // update the popup (content, layout, ect...) this._popup.update(); // open the popup on the map this._map.openPopup(this._popup, latlng); } return this; }, // @method closePopup(): this // Closes the popup bound to this layer if it is open. closePopup: function () { if (this._popup) { this._popup._close(); } return this; }, // @method togglePopup(): this // Opens or closes the popup bound to this layer depending on its current state. togglePopup: function (target) { if (this._popup) { if (this._popup._map) { this.closePopup(); } else { this.openPopup(target); } } return this; }, // @method isPopupOpen(): boolean // Returns `true` if the popup bound to this layer is currently open. isPopupOpen: function () { return (this._popup ? this._popup.isOpen() : false); }, // @method setPopupContent(content: String|HTMLElement|Popup): this // Sets the content of the popup bound to this layer. setPopupContent: function (content) { if (this._popup) { this._popup.setContent(content); } return this; }, // @method getPopup(): Popup // Returns the popup bound to this layer. getPopup: function () { return this._popup; }, _openPopup: function (e) { var layer = e.layer || e.target; if (!this._popup) { return; } if (!this._map) { return; } // prevent map click DomEvent.stop(e); // if this inherits from Path its a vector and we can just // open the popup at the new location if (layer instanceof Path) { this.openPopup(e.layer || e.target, e.latlng); return; } // otherwise treat it like a marker and figure out // if we should toggle it open/closed if (this._map.hasLayer(this._popup) && this._popup._source === layer) { this.closePopup(); } else { this.openPopup(layer, e.latlng); } }, _movePopup: function (e) { this._popup.setLatLng(e.latlng); }, _onKeyPress: function (e) { if (e.originalEvent.keyCode === 13) { this._openPopup(e); } } });