UNPKG

15.9 kBJavaScriptView Raw
1import {DivOverlay} from './DivOverlay';
2import * as DomEvent from '../dom/DomEvent';
3import * as DomUtil from '../dom/DomUtil';
4import {Point, toPoint} from '../geometry/Point';
5import {Map} from '../map/Map';
6import {Layer} from './Layer';
7import {Path} from './vector/Path';
8import {FeatureGroup} from './FeatureGroup';
9
10/*
11 * @class Popup
12 * @inherits DivOverlay
13 * @aka L.Popup
14 * Used to open popups in certain places of the map. Use [Map.openPopup](#map-openpopup) to
15 * open popups while making sure that only one popup is open at one time
16 * (recommended for usability), or use [Map.addLayer](#map-addlayer) to open as many as you want.
17 *
18 * @example
19 *
20 * If you want to just bind a popup to marker click and then open it, it's really easy:
21 *
22 * ```js
23 * marker.bindPopup(popupContent).openPopup();
24 * ```
25 * Path overlays like polylines also have a `bindPopup` method.
26 *
27 * A popup can be also standalone:
28 *
29 * ```js
30 * var popup = L.popup()
31 * .setLatLng(latlng)
32 * .setContent('<p>Hello world!<br />This is a nice popup.</p>')
33 * .openOn(map);
34 * ```
35 * or
36 * ```js
37 * var popup = L.popup(latlng, {content: '<p>Hello world!<br />This is a nice popup.</p>')
38 * .openOn(map);
39 * ```
40 */
41
42
43// @namespace Popup
44export var Popup = DivOverlay.extend({
45
46 // @section
47 // @aka Popup options
48 options: {
49 // @option pane: String = 'popupPane'
50 // `Map pane` where the popup will be added.
51 pane: 'popupPane',
52
53 // @option offset: Point = Point(0, 7)
54 // The offset of the popup position.
55 offset: [0, 7],
56
57 // @option maxWidth: Number = 300
58 // Max width of the popup, in pixels.
59 maxWidth: 300,
60
61 // @option minWidth: Number = 50
62 // Min width of the popup, in pixels.
63 minWidth: 50,
64
65 // @option maxHeight: Number = null
66 // If set, creates a scrollable container of the given height
67 // inside a popup if its content exceeds it.
68 // The scrollable container can be styled using the
69 // `leaflet-popup-scrolled` CSS class selector.
70 maxHeight: null,
71
72 // @option autoPan: Boolean = true
73 // Set it to `false` if you don't want the map to do panning animation
74 // to fit the opened popup.
75 autoPan: true,
76
77 // @option autoPanPaddingTopLeft: Point = null
78 // The margin between the popup and the top left corner of the map
79 // view after autopanning was performed.
80 autoPanPaddingTopLeft: null,
81
82 // @option autoPanPaddingBottomRight: Point = null
83 // The margin between the popup and the bottom right corner of the map
84 // view after autopanning was performed.
85 autoPanPaddingBottomRight: null,
86
87 // @option autoPanPadding: Point = Point(5, 5)
88 // Equivalent of setting both top left and bottom right autopan padding to the same value.
89 autoPanPadding: [5, 5],
90
91 // @option keepInView: Boolean = false
92 // Set it to `true` if you want to prevent users from panning the popup
93 // off of the screen while it is open.
94 keepInView: false,
95
96 // @option closeButton: Boolean = true
97 // Controls the presence of a close button in the popup.
98 closeButton: true,
99
100 // @option autoClose: Boolean = true
101 // Set it to `false` if you want to override the default behavior of
102 // the popup closing when another popup is opened.
103 autoClose: true,
104
105 // @option closeOnEscapeKey: Boolean = true
106 // Set it to `false` if you want to override the default behavior of
107 // the ESC key for closing of the popup.
108 closeOnEscapeKey: true,
109
110 // @option closeOnClick: Boolean = *
111 // Set it if you want to override the default behavior of the popup closing when user clicks
112 // on the map. Defaults to the map's [`closePopupOnClick`](#map-closepopuponclick) option.
113
114 // @option className: String = ''
115 // A custom CSS class name to assign to the popup.
116 className: ''
117 },
118
119 // @namespace Popup
120 // @method openOn(map: Map): this
121 // Alternative to `map.openPopup(popup)`.
122 // Adds the popup to the map and closes the previous one.
123 openOn: function (map) {
124 map = arguments.length ? map : this._source._map; // experimental, not the part of public api
125
126 if (!map.hasLayer(this) && map._popup && map._popup.options.autoClose) {
127 map.removeLayer(map._popup);
128 }
129 map._popup = this;
130
131 return DivOverlay.prototype.openOn.call(this, map);
132 },
133
134 onAdd: function (map) {
135 DivOverlay.prototype.onAdd.call(this, map);
136
137 // @namespace Map
138 // @section Popup events
139 // @event popupopen: PopupEvent
140 // Fired when a popup is opened in the map
141 map.fire('popupopen', {popup: this});
142
143 if (this._source) {
144 // @namespace Layer
145 // @section Popup events
146 // @event popupopen: PopupEvent
147 // Fired when a popup bound to this layer is opened
148 this._source.fire('popupopen', {popup: this}, true);
149 // For non-path layers, we toggle the popup when clicking
150 // again the layer, so prevent the map to reopen it.
151 if (!(this._source instanceof Path)) {
152 this._source.on('preclick', DomEvent.stopPropagation);
153 }
154 }
155 },
156
157 onRemove: function (map) {
158 DivOverlay.prototype.onRemove.call(this, map);
159
160 // @namespace Map
161 // @section Popup events
162 // @event popupclose: PopupEvent
163 // Fired when a popup in the map is closed
164 map.fire('popupclose', {popup: this});
165
166 if (this._source) {
167 // @namespace Layer
168 // @section Popup events
169 // @event popupclose: PopupEvent
170 // Fired when a popup bound to this layer is closed
171 this._source.fire('popupclose', {popup: this}, true);
172 if (!(this._source instanceof Path)) {
173 this._source.off('preclick', DomEvent.stopPropagation);
174 }
175 }
176 },
177
178 getEvents: function () {
179 var events = DivOverlay.prototype.getEvents.call(this);
180
181 if (this.options.closeOnClick !== undefined ? this.options.closeOnClick : this._map.options.closePopupOnClick) {
182 events.preclick = this.close;
183 }
184
185 if (this.options.keepInView) {
186 events.moveend = this._adjustPan;
187 }
188
189 return events;
190 },
191
192 _initLayout: function () {
193 var prefix = 'leaflet-popup',
194 container = this._container = DomUtil.create('div',
195 prefix + ' ' + (this.options.className || '') +
196 ' leaflet-zoom-animated');
197
198 var wrapper = this._wrapper = DomUtil.create('div', prefix + '-content-wrapper', container);
199 this._contentNode = DomUtil.create('div', prefix + '-content', wrapper);
200
201 DomEvent.disableClickPropagation(container);
202 DomEvent.disableScrollPropagation(this._contentNode);
203 DomEvent.on(container, 'contextmenu', DomEvent.stopPropagation);
204
205 this._tipContainer = DomUtil.create('div', prefix + '-tip-container', container);
206 this._tip = DomUtil.create('div', prefix + '-tip', this._tipContainer);
207
208 if (this.options.closeButton) {
209 var closeButton = this._closeButton = DomUtil.create('a', prefix + '-close-button', container);
210 closeButton.setAttribute('role', 'button'); // overrides the implicit role=link of <a> elements #7399
211 closeButton.setAttribute('aria-label', 'Close popup');
212 closeButton.href = '#close';
213 closeButton.innerHTML = '<span aria-hidden="true">&#215;</span>';
214
215 DomEvent.on(closeButton, 'click', function (ev) {
216 DomEvent.preventDefault(ev);
217 this.close();
218 }, this);
219 }
220 },
221
222 _updateLayout: function () {
223 var container = this._contentNode,
224 style = container.style;
225
226 style.width = '';
227 style.whiteSpace = 'nowrap';
228
229 var width = container.offsetWidth;
230 width = Math.min(width, this.options.maxWidth);
231 width = Math.max(width, this.options.minWidth);
232
233 style.width = (width + 1) + 'px';
234 style.whiteSpace = '';
235
236 style.height = '';
237
238 var height = container.offsetHeight,
239 maxHeight = this.options.maxHeight,
240 scrolledClass = 'leaflet-popup-scrolled';
241
242 if (maxHeight && height > maxHeight) {
243 style.height = maxHeight + 'px';
244 DomUtil.addClass(container, scrolledClass);
245 } else {
246 DomUtil.removeClass(container, scrolledClass);
247 }
248
249 this._containerWidth = this._container.offsetWidth;
250 },
251
252 _animateZoom: function (e) {
253 var pos = this._map._latLngToNewLayerPoint(this._latlng, e.zoom, e.center),
254 anchor = this._getAnchor();
255 DomUtil.setPosition(this._container, pos.add(anchor));
256 },
257
258 _adjustPan: function () {
259 if (!this.options.autoPan) { return; }
260 if (this._map._panAnim) { this._map._panAnim.stop(); }
261
262 // We can endlessly recurse if keepInView is set and the view resets.
263 // Let's guard against that by exiting early if we're responding to our own autopan.
264 if (this._autopanning) {
265 this._autopanning = false;
266 return;
267 }
268
269 var map = this._map,
270 marginBottom = parseInt(DomUtil.getStyle(this._container, 'marginBottom'), 10) || 0,
271 containerHeight = this._container.offsetHeight + marginBottom,
272 containerWidth = this._containerWidth,
273 layerPos = new Point(this._containerLeft, -containerHeight - this._containerBottom);
274
275 layerPos._add(DomUtil.getPosition(this._container));
276
277 var containerPos = map.layerPointToContainerPoint(layerPos),
278 padding = toPoint(this.options.autoPanPadding),
279 paddingTL = toPoint(this.options.autoPanPaddingTopLeft || padding),
280 paddingBR = toPoint(this.options.autoPanPaddingBottomRight || padding),
281 size = map.getSize(),
282 dx = 0,
283 dy = 0;
284
285 if (containerPos.x + containerWidth + paddingBR.x > size.x) { // right
286 dx = containerPos.x + containerWidth - size.x + paddingBR.x;
287 }
288 if (containerPos.x - dx - paddingTL.x < 0) { // left
289 dx = containerPos.x - paddingTL.x;
290 }
291 if (containerPos.y + containerHeight + paddingBR.y > size.y) { // bottom
292 dy = containerPos.y + containerHeight - size.y + paddingBR.y;
293 }
294 if (containerPos.y - dy - paddingTL.y < 0) { // top
295 dy = containerPos.y - paddingTL.y;
296 }
297
298 // @namespace Map
299 // @section Popup events
300 // @event autopanstart: Event
301 // Fired when the map starts autopanning when opening a popup.
302 if (dx || dy) {
303 // Track that we're autopanning, as this function will be re-ran on moveend
304 if (this.options.keepInView) {
305 this._autopanning = true;
306 }
307
308 map
309 .fire('autopanstart')
310 .panBy([dx, dy]);
311 }
312 },
313
314 _getAnchor: function () {
315 // Where should we anchor the popup on the source layer?
316 return toPoint(this._source && this._source._getPopupAnchor ? this._source._getPopupAnchor() : [0, 0]);
317 }
318
319});
320
321// @namespace Popup
322// @factory L.popup(options?: Popup options, source?: Layer)
323// 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.
324// @alternative
325// @factory L.popup(latlng: LatLng, options?: Popup options)
326// Instantiates a `Popup` object given `latlng` where the popup will open and an optional `options` object that describes its appearance and location.
327export var popup = function (options, source) {
328 return new Popup(options, source);
329};
330
331
332/* @namespace Map
333 * @section Interaction Options
334 * @option closePopupOnClick: Boolean = true
335 * Set it to `false` if you don't want popups to close when user clicks the map.
336 */
337Map.mergeOptions({
338 closePopupOnClick: true
339});
340
341
342// @namespace Map
343// @section Methods for Layers and Controls
344Map.include({
345 // @method openPopup(popup: Popup): this
346 // Opens the specified popup while closing the previously opened (to make sure only one is opened at one time for usability).
347 // @alternative
348 // @method openPopup(content: String|HTMLElement, latlng: LatLng, options?: Popup options): this
349 // Creates a popup with the specified content and options and opens it in the given point on a map.
350 openPopup: function (popup, latlng, options) {
351 this._initOverlay(Popup, popup, latlng, options)
352 .openOn(this);
353
354 return this;
355 },
356
357 // @method closePopup(popup?: Popup): this
358 // Closes the popup previously opened with [openPopup](#map-openpopup) (or the given one).
359 closePopup: function (popup) {
360 popup = arguments.length ? popup : this._popup;
361 if (popup) {
362 popup.close();
363 }
364 return this;
365 }
366});
367
368/*
369 * @namespace Layer
370 * @section Popup methods example
371 *
372 * All layers share a set of methods convenient for binding popups to it.
373 *
374 * ```js
375 * var layer = L.Polygon(latlngs).bindPopup('Hi There!').addTo(map);
376 * layer.openPopup();
377 * layer.closePopup();
378 * ```
379 *
380 * 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.
381 */
382
383// @section Popup methods
384Layer.include({
385
386 // @method bindPopup(content: String|HTMLElement|Function|Popup, options?: Popup options): this
387 // Binds a popup to the layer with the passed `content` and sets up the
388 // necessary event listeners. If a `Function` is passed it will receive
389 // the layer as the first argument and should return a `String` or `HTMLElement`.
390 bindPopup: function (content, options) {
391 this._popup = this._initOverlay(Popup, this._popup, content, options);
392 if (!this._popupHandlersAdded) {
393 this.on({
394 click: this._openPopup,
395 keypress: this._onKeyPress,
396 remove: this.closePopup,
397 move: this._movePopup
398 });
399 this._popupHandlersAdded = true;
400 }
401
402 return this;
403 },
404
405 // @method unbindPopup(): this
406 // Removes the popup previously bound with `bindPopup`.
407 unbindPopup: function () {
408 if (this._popup) {
409 this.off({
410 click: this._openPopup,
411 keypress: this._onKeyPress,
412 remove: this.closePopup,
413 move: this._movePopup
414 });
415 this._popupHandlersAdded = false;
416 this._popup = null;
417 }
418 return this;
419 },
420
421 // @method openPopup(latlng?: LatLng): this
422 // Opens the bound popup at the specified `latlng` or at the default popup anchor if no `latlng` is passed.
423 openPopup: function (latlng) {
424 if (this._popup) {
425 if (!(this instanceof FeatureGroup)) {
426 this._popup._source = this;
427 }
428 if (this._popup._prepareOpen(latlng || this._latlng)) {
429 // open the popup on the map
430 this._popup.openOn(this._map);
431 }
432 }
433 return this;
434 },
435
436 // @method closePopup(): this
437 // Closes the popup bound to this layer if it is open.
438 closePopup: function () {
439 if (this._popup) {
440 this._popup.close();
441 }
442 return this;
443 },
444
445 // @method togglePopup(): this
446 // Opens or closes the popup bound to this layer depending on its current state.
447 togglePopup: function () {
448 if (this._popup) {
449 this._popup.toggle(this);
450 }
451 return this;
452 },
453
454 // @method isPopupOpen(): boolean
455 // Returns `true` if the popup bound to this layer is currently open.
456 isPopupOpen: function () {
457 return (this._popup ? this._popup.isOpen() : false);
458 },
459
460 // @method setPopupContent(content: String|HTMLElement|Popup): this
461 // Sets the content of the popup bound to this layer.
462 setPopupContent: function (content) {
463 if (this._popup) {
464 this._popup.setContent(content);
465 }
466 return this;
467 },
468
469 // @method getPopup(): Popup
470 // Returns the popup bound to this layer.
471 getPopup: function () {
472 return this._popup;
473 },
474
475 _openPopup: function (e) {
476 if (!this._popup || !this._map) {
477 return;
478 }
479 // prevent map click
480 DomEvent.stop(e);
481
482 var target = e.layer || e.target;
483 if (this._popup._source === target && !(target instanceof Path)) {
484 // treat it like a marker and figure out
485 // if we should toggle it open/closed
486 if (this._map.hasLayer(this._popup)) {
487 this.closePopup();
488 } else {
489 this.openPopup(e.latlng);
490 }
491 return;
492 }
493 this._popup._source = target;
494 this.openPopup(e.latlng);
495 },
496
497 _movePopup: function (e) {
498 this._popup.setLatLng(e.latlng);
499 },
500
501 _onKeyPress: function (e) {
502 if (e.originalEvent.keyCode === 13) {
503 this._openPopup(e);
504 }
505 }
506});