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);
}
}
});