2 Leaflet.contextmenu, a context menu for Leaflet.
3 (c) 2015, Adam Ratcliffe, GeoSmart Maps Limited
9 // Packaging/modules magic dance
11 if (typeof define === 'function' && define.amd) {
13 define(['leaflet'], factory);
14 } else if (typeof module === 'object' && typeof module.exports === 'object') {
16 L = require('leaflet');
17 module.exports = factory(L);
20 if (typeof window.L === 'undefined') {
21 throw new Error('Leaflet must be loaded first');
30 L.Map.ContextMenu = L.Handler.extend({
31 _touchstart: L.Browser.msPointer ? 'MSPointerDown' : L.Browser.pointer ? 'pointerdown' : 'touchstart',
34 BASE_CLS: 'leaflet-contextmenu'
37 initialize: function (map) {
38 L.Handler.prototype.initialize.call(this, map);
41 this._visible = false;
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';
47 if (map.options.contextmenuWidth) {
48 container.style.width = map.options.contextmenuWidth + 'px';
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);
60 addHooks: function () {
61 var container = this._map.getContainer();
64 .on(container, 'mouseleave', this._hide, this)
65 .on(document, 'keydown', this._onKeyDown, this);
67 if (L.Browser.touch) {
68 L.DomEvent.on(document, this._touchstart, this._hide, this);
72 contextmenu: this._show,
73 mousedown: this._hide,
74 movestart: this._hide,
79 removeHooks: function () {
80 var container = this._map.getContainer();
83 .off(container, 'mouseleave', this._hide, this)
84 .off(document, 'keydown', this._onKeyDown, this);
86 if (L.Browser.touch) {
87 L.DomEvent.off(document, this._touchstart, this._hide, this);
91 contextmenu: this._show,
92 mousedown: this._hide,
93 movestart: this._hide,
98 showAt: function (point, data) {
99 if (point instanceof L.LatLng) {
100 point = this._map.latLngToContainerPoint(point);
102 this._showAtPoint(point, data);
109 addItem: function (options) {
110 return this.insertItem(options);
113 insertItem: function (options, index) {
114 index = index !== undefined ? index: this._items.length;
116 var item = this._createItem(this._container, options, index);
118 this._items.push(item);
120 this._sizeChanged = true;
122 this._map.fire('contextmenu.additem', {
131 removeItem: function (item) {
132 var container = this._container;
135 item = container.children[item];
139 this._removeItem(L.Util.stamp(item));
141 this._sizeChanged = true;
143 this._map.fire('contextmenu.removeitem', {
154 removeAllItems: function () {
155 var items = this._container.children,
158 while (items.length) {
160 this._removeItem(L.Util.stamp(item));
165 hideAllItems: function () {
168 for (i = 0, l = this._items.length; i < l; i++) {
169 item = this._items[i];
170 item.el.style.display = 'none';
174 showAllItems: function () {
177 for (i = 0, l = this._items.length; i < l; i++) {
178 item = this._items[i];
179 item.el.style.display = '';
183 setDisabled: function (item, disabled) {
184 var container = this._container,
185 itemCls = L.Map.ContextMenu.BASE_CLS + '-item';
188 item = container.children[item];
191 if (item && L.DomUtil.hasClass(item, itemCls)) {
193 L.DomUtil.addClass(item, itemCls + '-disabled');
194 this._map.fire('contextmenu.disableitem', {
199 L.DomUtil.removeClass(item, itemCls + '-disabled');
200 this._map.fire('contextmenu.enableitem', {
208 isVisible: function () {
209 return this._visible;
212 _createItems: function () {
213 var itemOptions = this._map.options.contextmenuItems,
217 for (i = 0, l = itemOptions.length; i < l; i++) {
218 this._items.push(this._createItem(this._container, itemOptions[i]));
222 _createItem: function (container, options, index) {
223 if (options.separator || options === '-') {
224 return this._createSeparator(container, index);
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),
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>';
241 el.innerHTML = html + options.text;
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);
250 if (L.Browser.touch) {
251 L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation);
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);
260 id: L.Util.stamp(el),
266 _removeItem: function (id) {
271 for (i = 0, l = this._items.length; i < l; i++) {
272 item = this._items[i];
274 if (item.id === id) {
276 callback = item.callback;
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);
285 if (L.Browser.touch) {
286 L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation);
289 if (!L.Browser.pointer) {
290 L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
294 this._container.removeChild(el);
295 this._items.splice(i, 1);
303 _createSeparator: function (container, index) {
304 var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index);
307 id: L.Util.stamp(el),
312 _createEventHandler: function (el, func, context, hideOnSelect) {
315 disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled',
316 hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true;
318 return function (e) {
319 if (L.DomUtil.hasClass(el, disabledCls)) {
328 func.call(context || map, me._showLocation);
331 me._map.fire('contextmenu.select', {
338 _insertElementAt: function (tagName, className, container, index) {
340 el = document.createElement(tagName);
342 el.className = className;
344 if (index !== undefined) {
345 refEl = container.children[index];
349 container.insertBefore(el, refEl);
351 container.appendChild(el);
357 _show: function (e) {
358 this._showAtPoint(e.containerPoint, e);
361 _showAtPoint: function (pt, data) {
362 if (this._items.length) {
364 layerPoint = map.containerPointToLayerPoint(pt),
365 latlng = map.layerPointToLatLng(layerPoint),
366 event = L.extend(data || {}, {contextmenu: this});
368 this._showLocation = {
370 layerPoint: layerPoint,
374 if (data && data.relatedTarget){
375 this._showLocation.relatedTarget = data.relatedTarget;
378 this._setPosition(pt);
380 if (!this._visible) {
381 this._container.style.display = 'block';
382 this._visible = true;
385 this._map.fire('contextmenu.show', event);
391 this._visible = false;
392 this._container.style.display = 'none';
393 this._map.fire('contextmenu.hide', {contextmenu: this});
397 _getIcon: function (options) {
398 return L.Browser.retina && options.retinaIcon || options.icon;
401 _getIconCls: function (options) {
402 return L.Browser.retina && options.retinaIconCls || options.iconCls;
405 _setPosition: function (pt) {
406 var mapSize = this._map.getSize(),
407 container = this._container,
408 containerSize = this._getElementSize(container),
411 if (this._map.options.contextmenuAnchor) {
412 anchor = L.point(this._map.options.contextmenuAnchor);
416 container._leaflet_pos = pt;
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';
422 container.style.left = Math.max(pt.x, 0) + 'px';
423 container.style.right = 'auto';
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';
430 container.style.top = Math.max(pt.y, 0) + 'px';
431 container.style.bottom = 'auto';
435 _getElementSize: function (el) {
436 var size = this._size,
437 initialDisplay = el.style.display;
439 if (!size || this._sizeChanged) {
442 el.style.left = '-999999px';
443 el.style.right = 'auto';
444 el.style.display = 'block';
446 size.x = el.offsetWidth;
447 size.y = el.offsetHeight;
449 el.style.left = 'auto';
450 el.style.display = initialDisplay;
452 this._sizeChanged = false;
458 _onKeyDown: function (e) {
461 // If ESC pressed and context menu is visible hide it
467 _onItemMouseOver: function (e) {
468 L.DomUtil.addClass(e.target || e.srcElement, 'over');
471 _onItemMouseOut: function (e) {
472 L.DomUtil.removeClass(e.target || e.srcElement, 'over');
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();
485 unbindContextMenu: function (){
486 this.off('contextmenu', this._showContextMenu, this);
491 addContextMenuItem: function (item) {
492 this.options.contextmenuItems.push(item);
495 removeContextMenuItemWithIndex: function (index) {
497 for (var i = 0; i < this.options.contextmenuItems.length; i++) {
498 if (this.options.contextmenuItems[i].index == index){
502 var elem = items.pop();
503 while (elem !== undefined) {
504 this.options.contextmenuItems.splice(elem,1);
509 replaceContextMenuItem: function (item) {
510 this.removeContextMenuItemWithIndex(item.index);
511 this.addContextMenuItem(item);
514 _initContextMenu: function () {
517 this.on('contextmenu', this._showContextMenu, this);
520 _showContextMenu: function (e) {
524 if (this._map.contextmenu) {
525 data = L.extend({relatedTarget: this}, e);
527 pt = this._map.mouseEventToContainerPoint(e.originalEvent);
529 if (!this.options.contextmenuInheritItems) {
530 this._map.contextmenu.hideAllItems();
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));
538 this._map.once('contextmenu.hide', this._hideContextMenu, this);
540 this._map.contextmenu.showAt(pt, data);
544 _hideContextMenu: function () {
547 for (i = 0, l = this._items.length; i < l; i++) {
548 this._map.contextmenu.removeItem(this._items[i]);
550 this._items.length = 0;
552 if (!this.options.contextmenuInheritItems) {
553 this._map.contextmenu.showAllItems();
558 var classes = [L.Marker, L.Path],
561 contextmenuItems: [],
562 contextmenuInheritItems: true
566 for (i = 0, l = classes.length; i < l; i++) {
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;
574 cls.mergeOptions(defaultOptions);
577 cls.addInitHook(function () {
578 if (this.options.contextmenu) {
579 this._initContextMenu();
583 cls.include(L.Mixin.ContextMenu);
585 return L.Map.ContextMenu;