]> git.openstreetmap.org Git - rails.git/blob - vendor/assets/leaflet/leaflet.contextmenu.js
Merge remote-tracking branch 'upstream/pull/3789'
[rails.git] / vendor / assets / leaflet / leaflet.contextmenu.js
1 /*
2         Leaflet.contextmenu, a context menu for Leaflet.
3         (c) 2015, Adam Ratcliffe, GeoSmart Maps Limited
4
5         @preserve
6 */
7
8 (function(factory) {
9         // Packaging/modules magic dance
10         var L;
11         if (typeof define === 'function' && define.amd) {
12                 // AMD
13                 define(['leaflet'], factory);
14         } else if (typeof module === 'object' && typeof module.exports === 'object') {
15                 // Node/CommonJS
16                 L = require('leaflet');
17                 module.exports = factory(L);
18         } else {
19                 // Browser globals
20                 if (typeof window.L === 'undefined') {
21                         throw new Error('Leaflet must be loaded first');
22                 }
23                 factory(window.L);
24         }
25 })(function(L) {
26 L.Map.mergeOptions({
27     contextmenuItems: []
28 });
29
30 L.Map.ContextMenu = L.Handler.extend({
31     _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart',
32
33     statics: {
34         BASE_CLS: 'leaflet-contextmenu'
35     },
36
37     initialize: function (map) {
38         L.Handler.prototype.initialize.call(this, map);
39
40         this._items = [];
41         this._visible = false;
42
43         var container = this._container = L.DomUtil.create('div', L.Map.ContextMenu.BASE_CLS, map._container);
44         container.style.zIndex = 10000;
45         container.style.position = 'absolute';
46
47         if (map.options.contextmenuWidth) {
48             container.style.width = map.options.contextmenuWidth + 'px';
49         }
50
51         this._createItems();
52
53         L.DomEvent
54             .on(container, 'click', L.DomEvent.stop)
55             .on(container, 'mousedown', L.DomEvent.stop)
56             .on(container, 'dblclick', L.DomEvent.stop)
57             .on(container, 'contextmenu', L.DomEvent.stop);
58     },
59
60     addHooks: function () {
61         var container = this._map.getContainer();
62
63         L.DomEvent
64             .on(container, 'mouseleave', this._hide, this)
65             .on(document, 'keydown', this._onKeyDown, this);
66
67         if (L.Browser.touch) {
68             L.DomEvent.on(document, this._touchstart, this._hide, this);
69         }
70
71         this._map.on({
72             contextmenu: this._show,
73             mousedown: this._hide,
74             zoomstart: this._hide
75         }, this);
76     },
77
78     removeHooks: function () {
79         var container = this._map.getContainer();
80
81         L.DomEvent
82             .off(container, 'mouseleave', this._hide, this)
83             .off(document, 'keydown', this._onKeyDown, this);
84
85         if (L.Browser.touch) {
86             L.DomEvent.off(document, this._touchstart, this._hide, this);
87         }
88
89         this._map.off({
90             contextmenu: this._show,
91             mousedown: this._hide,
92             zoomstart: this._hide
93         }, this);
94     },
95
96     showAt: function (point, data) {
97         if (point instanceof L.LatLng) {
98             point = this._map.latLngToContainerPoint(point);
99         }
100         this._showAtPoint(point, data);
101     },
102
103     hide: function () {
104         this._hide();
105     },
106
107     addItem: function (options) {
108         return this.insertItem(options);
109     },
110
111     insertItem: function (options, index) {
112         index = index !== undefined ? index: this._items.length;
113
114         var item = this._createItem(this._container, options, index);
115
116         this._items.push(item);
117
118         this._sizeChanged = true;
119
120         this._map.fire('contextmenu.additem', {
121             contextmenu: this,
122             el: item.el,
123             index: index
124         });
125
126         return item.el;
127     },
128
129     removeItem: function (item) {
130         var container = this._container;
131
132         if (!isNaN(item)) {
133             item = container.children[item];
134         }
135
136         if (item) {
137             this._removeItem(L.Util.stamp(item));
138
139             this._sizeChanged = true;
140
141             this._map.fire('contextmenu.removeitem', {
142                 contextmenu: this,
143                 el: item
144             });
145
146             return item;
147         }
148
149         return null;
150     },
151
152     removeAllItems: function () {
153         var items = this._container.children,
154             item;
155
156         while (items.length) {
157             item = items[0];
158             this._removeItem(L.Util.stamp(item));
159         }
160         return items;
161     },
162
163     hideAllItems: function () {
164         var item, i, l;
165
166         for (i = 0, l = this._items.length; i < l; i++) {
167             item = this._items[i];
168             item.el.style.display = 'none';
169         }
170     },
171
172     showAllItems: function () {
173         var item, i, l;
174
175         for (i = 0, l = this._items.length; i < l; i++) {
176             item = this._items[i];
177             item.el.style.display = '';
178         }
179     },
180
181     setDisabled: function (item, disabled) {
182         var container = this._container,
183         itemCls = L.Map.ContextMenu.BASE_CLS + '-item';
184
185         if (!isNaN(item)) {
186             item = container.children[item];
187         }
188
189         if (item && L.DomUtil.hasClass(item, itemCls)) {
190             if (disabled) {
191                 L.DomUtil.addClass(item, itemCls + '-disabled');
192                 this._map.fire('contextmenu.disableitem', {
193                     contextmenu: this,
194                     el: item
195                 });
196             } else {
197                 L.DomUtil.removeClass(item, itemCls + '-disabled');
198                 this._map.fire('contextmenu.enableitem', {
199                     contextmenu: this,
200                     el: item
201                 });
202             }
203         }
204     },
205
206     isVisible: function () {
207         return this._visible;
208     },
209
210     _createItems: function () {
211         var itemOptions = this._map.options.contextmenuItems,
212             item,
213             i, l;
214
215         for (i = 0, l = itemOptions.length; i < l; i++) {
216             this._items.push(this._createItem(this._container, itemOptions[i]));
217         }
218     },
219
220     _createItem: function (container, options, index) {
221         if (options.separator || options === '-') {
222             return this._createSeparator(container, index);
223         }
224
225         var itemCls = L.Map.ContextMenu.BASE_CLS + '-item',
226             cls = options.disabled ? (itemCls + ' ' + itemCls + '-disabled') : itemCls,
227             el = this._insertElementAt('a', cls, container, index),
228             callback = this._createEventHandler(el, options.callback, options.context, options.hideOnSelect),
229             icon = this._getIcon(options),
230             iconCls = this._getIconCls(options),
231             html = '';
232
233         if (icon) {
234             html = '<img class="' + L.Map.ContextMenu.BASE_CLS + '-icon" src="' + icon + '"/>';
235         } else if (iconCls) {
236             html = '<span class="' + L.Map.ContextMenu.BASE_CLS + '-icon ' + iconCls + '"></span>';
237         }
238
239         el.innerHTML = html + options.text;
240         el.href = '#';
241
242         L.DomEvent
243             .on(el, 'mouseover', this._onItemMouseOver, this)
244             .on(el, 'mouseout', this._onItemMouseOut, this)
245             .on(el, 'mousedown', L.DomEvent.stopPropagation)
246             .on(el, 'click', callback);
247
248         if (L.Browser.touch) {
249             L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation);
250         }
251
252         // Devices without a mouse fire "mouseover" on tap, but never â€œmouseout"
253         if (!L.Browser.pointer) {
254             L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
255         }
256
257         return {
258             id: L.Util.stamp(el),
259             el: el,
260             callback: callback
261         };
262     },
263
264     _removeItem: function (id) {
265         var item,
266             el,
267             i, l, callback;
268
269         for (i = 0, l = this._items.length; i < l; i++) {
270             item = this._items[i];
271
272             if (item.id === id) {
273                 el = item.el;
274                 callback = item.callback;
275
276                 if (callback) {
277                     L.DomEvent
278                         .off(el, 'mouseover', this._onItemMouseOver, this)
279                         .off(el, 'mouseover', this._onItemMouseOut, this)
280                         .off(el, 'mousedown', L.DomEvent.stopPropagation)
281                         .off(el, 'click', callback);
282
283                     if (L.Browser.touch) {
284                         L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation);
285                     }
286
287                     if (!L.Browser.pointer) {
288                         L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
289                     }
290                 }
291
292                 this._container.removeChild(el);
293                 this._items.splice(i, 1);
294
295                 return item;
296             }
297         }
298         return null;
299     },
300
301     _createSeparator: function (container, index) {
302         var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index);
303
304         return {
305             id: L.Util.stamp(el),
306             el: el
307         };
308     },
309
310     _createEventHandler: function (el, func, context, hideOnSelect) {
311         var me = this,
312             map = this._map,
313             disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled',
314             hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true;
315
316         return function (e) {
317             if (L.DomUtil.hasClass(el, disabledCls)) {
318                 return;
319             }
320
321             var map = me._map,
322                 containerPoint = me._showLocation.containerPoint,
323                 layerPoint = map.containerPointToLayerPoint(containerPoint),
324                 latlng = map.layerPointToLatLng(layerPoint),
325                 relatedTarget = me._showLocation.relatedTarget,
326                 data = {
327                   containerPoint: containerPoint,
328                   layerPoint: layerPoint,
329                   latlng: latlng,
330                   relatedTarget: relatedTarget
331                 };
332
333             if (hideOnSelect) {
334                 me._hide();
335             }
336
337             if (func) {
338                 func.call(context || map, data);
339             }
340
341             me._map.fire('contextmenu.select', {
342                 contextmenu: me,
343                 el: el
344             });
345         };
346     },
347
348     _insertElementAt: function (tagName, className, container, index) {
349         var refEl,
350             el = document.createElement(tagName);
351
352         el.className = className;
353
354         if (index !== undefined) {
355             refEl = container.children[index];
356         }
357
358         if (refEl) {
359             container.insertBefore(el, refEl);
360         } else {
361             container.appendChild(el);
362         }
363
364         return el;
365     },
366
367     _show: function (e) {
368         this._showAtPoint(e.containerPoint, e);
369     },
370
371     _showAtPoint: function (pt, data) {
372         if (this._items.length) {
373             var map = this._map,
374             event = L.extend(data || {}, {contextmenu: this});
375
376             this._showLocation = {
377                 containerPoint: pt
378             };
379
380             if (data && data.relatedTarget){
381                 this._showLocation.relatedTarget = data.relatedTarget;
382             }
383
384             this._setPosition(pt);
385
386             if (!this._visible) {
387                 this._container.style.display = 'block';
388                 this._visible = true;
389             }
390
391             this._map.fire('contextmenu.show', event);
392         }
393     },
394
395     _hide: function () {
396         if (this._visible) {
397             this._visible = false;
398             this._container.style.display = 'none';
399             this._map.fire('contextmenu.hide', {contextmenu: this});
400         }
401     },
402
403     _getIcon: function (options) {
404         return L.Browser.retina && options.retinaIcon || options.icon;
405     },
406
407     _getIconCls: function (options) {
408         return L.Browser.retina && options.retinaIconCls || options.iconCls;
409     },
410
411     _setPosition: function (pt) {
412         var mapSize = this._map.getSize(),
413             container = this._container,
414             containerSize = this._getElementSize(container),
415             anchor;
416
417         if (this._map.options.contextmenuAnchor) {
418             anchor = L.point(this._map.options.contextmenuAnchor);
419             pt = pt.add(anchor);
420         }
421
422         container._leaflet_pos = pt;
423
424         if (pt.x + containerSize.x > mapSize.x) {
425             container.style.left = 'auto';
426             container.style.right = Math.min(Math.max(mapSize.x - pt.x, 0), mapSize.x - containerSize.x - 1) + 'px';
427         } else {
428             container.style.left = Math.max(pt.x, 0) + 'px';
429             container.style.right = 'auto';
430         }
431
432         if (pt.y + containerSize.y > mapSize.y) {
433             container.style.top = 'auto';
434             container.style.bottom = Math.min(Math.max(mapSize.y - pt.y, 0), mapSize.y - containerSize.y - 1) + 'px';
435         } else {
436             container.style.top = Math.max(pt.y, 0) + 'px';
437             container.style.bottom = 'auto';
438         }
439     },
440
441     _getElementSize: function (el) {
442         var size = this._size,
443             initialDisplay = el.style.display;
444
445         if (!size || this._sizeChanged) {
446             size = {};
447
448             el.style.left = '-999999px';
449             el.style.right = 'auto';
450             el.style.display = 'block';
451
452             size.x = el.offsetWidth;
453             size.y = el.offsetHeight;
454
455             el.style.left = 'auto';
456             el.style.display = initialDisplay;
457
458             this._sizeChanged = false;
459         }
460
461         return size;
462     },
463
464     _onKeyDown: function (e) {
465         var key = e.keyCode;
466
467         // If ESC pressed and context menu is visible hide it
468         if (key === 27) {
469             this._hide();
470         }
471     },
472
473     _onItemMouseOver: function (e) {
474         L.DomUtil.addClass(e.target || e.srcElement, 'over');
475     },
476
477     _onItemMouseOut: function (e) {
478         L.DomUtil.removeClass(e.target || e.srcElement, 'over');
479     }
480 });
481
482 L.Map.addInitHook('addHandler', 'contextmenu', L.Map.ContextMenu);
483 L.Mixin.ContextMenu = {
484     bindContextMenu: function (options) {
485         L.setOptions(this, options);
486         this._initContextMenu();
487
488         return this;
489     },
490
491     unbindContextMenu: function (){
492         this.off('contextmenu', this._showContextMenu, this);
493
494         return this;
495     },
496
497     addContextMenuItem: function (item) {
498             this.options.contextmenuItems.push(item);
499     },
500
501     removeContextMenuItemWithIndex: function (index) {
502         var items = [];
503         for (var i = 0; i < this.options.contextmenuItems.length; i++) {
504             if (this.options.contextmenuItems[i].index == index){
505                 items.push(i);
506             }
507         }
508         var elem = items.pop();
509         while (elem !== undefined) {
510             this.options.contextmenuItems.splice(elem,1);
511             elem = items.pop();
512         }
513     },
514
515     replaceContextMenuItem: function (item) {
516         this.removeContextMenuItemWithIndex(item.index);
517         this.addContextMenuItem(item);
518     },
519
520     _initContextMenu: function () {
521         this._items = [];
522
523         this.on('contextmenu', this._showContextMenu, this);
524     },
525
526     _showContextMenu: function (e) {
527         var itemOptions,
528             data, pt, i, l;
529
530         if (this._map.contextmenu) {
531             data = L.extend({relatedTarget: this}, e);
532
533             pt = this._map.mouseEventToContainerPoint(e.originalEvent);
534
535             if (!this.options.contextmenuInheritItems) {
536                 this._map.contextmenu.hideAllItems();
537             }
538
539             for (i = 0, l = this.options.contextmenuItems.length; i < l; i++) {
540                 itemOptions = this.options.contextmenuItems[i];
541                 this._items.push(this._map.contextmenu.insertItem(itemOptions, itemOptions.index));
542             }
543
544             this._map.once('contextmenu.hide', this._hideContextMenu, this);
545
546             this._map.contextmenu.showAt(pt, data);
547         }
548     },
549
550     _hideContextMenu: function () {
551         var i, l;
552
553         for (i = 0, l = this._items.length; i < l; i++) {
554             this._map.contextmenu.removeItem(this._items[i]);
555         }
556         this._items.length = 0;
557
558         if (!this.options.contextmenuInheritItems) {
559             this._map.contextmenu.showAllItems();
560         }
561     }
562 };
563
564 var classes = [L.Marker, L.Path],
565     defaultOptions = {
566         contextmenu: false,
567         contextmenuItems: [],
568         contextmenuInheritItems: true
569     },
570     cls, i, l;
571
572 for (i = 0, l = classes.length; i < l; i++) {
573     cls = classes[i];
574
575     // L.Class should probably provide an empty options hash, as it does not test
576     // for it here and add if needed
577     if (!cls.prototype.options) {
578         cls.prototype.options = defaultOptions;
579     } else {
580         cls.mergeOptions(defaultOptions);
581     }
582
583     cls.addInitHook(function () {
584         if (this.options.contextmenu) {
585             this._initContextMenu();
586         }
587     });
588
589     cls.include(L.Mixin.ContextMenu);
590 }
591 return L.Map.ContextMenu;
592 });