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