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