UNPKG

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