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', {
150 removeAllItems: function () {
153 while (this._container.children.length) {
154 item = this._container.children[0];
155 this._removeItem(L.Util.stamp(item));
159 hideAllItems: function () {
162 for (i = 0, l = this._items.length; i < l; i++) {
163 item = this._items[i];
164 item.el.style.display = 'none';
168 showAllItems: function () {
171 for (i = 0, l = this._items.length; i < l; i++) {
172 item = this._items[i];
173 item.el.style.display = '';
177 setDisabled: function (item, disabled) {
178 var container = this._container,
179 itemCls = L.Map.ContextMenu.BASE_CLS + '-item';
182 item = container.children[item];
185 if (item && L.DomUtil.hasClass(item, itemCls)) {
187 L.DomUtil.addClass(item, itemCls + '-disabled');
188 this._map.fire('contextmenu.disableitem', {
193 L.DomUtil.removeClass(item, itemCls + '-disabled');
194 this._map.fire('contextmenu.enableitem', {
202 isVisible: function () {
203 return this._visible;
206 _createItems: function () {
207 var itemOptions = this._map.options.contextmenuItems,
211 for (i = 0, l = itemOptions.length; i < l; i++) {
212 this._items.push(this._createItem(this._container, itemOptions[i]));
216 _createItem: function (container, options, index) {
217 if (options.separator || options === '-') {
218 return this._createSeparator(container, index);
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),
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>';
235 el.innerHTML = html + options.text;
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);
244 if (L.Browser.touch) {
245 L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation);
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);
254 id: L.Util.stamp(el),
260 _removeItem: function (id) {
265 for (i = 0, l = this._items.length; i < l; i++) {
266 item = this._items[i];
268 if (item.id === id) {
270 callback = item.callback;
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);
279 if (L.Browser.touch) {
280 L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation);
283 if (!L.Browser.pointer) {
284 L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
288 this._container.removeChild(el);
289 this._items.splice(i, 1);
297 _createSeparator: function (container, index) {
298 var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index);
301 id: L.Util.stamp(el),
306 _createEventHandler: function (el, func, context, hideOnSelect) {
309 disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled',
310 hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true;
312 return function (e) {
313 if (L.DomUtil.hasClass(el, disabledCls)) {
322 func.call(context || map, me._showLocation);
325 me._map.fire('contextmenu:select', {
332 _insertElementAt: function (tagName, className, container, index) {
334 el = document.createElement(tagName);
336 el.className = className;
338 if (index !== undefined) {
339 refEl = container.children[index];
343 container.insertBefore(el, refEl);
345 container.appendChild(el);
351 _show: function (e) {
352 this._showAtPoint(e.containerPoint, e);
355 _showAtPoint: function (pt, data) {
356 if (this._items.length) {
358 layerPoint = map.containerPointToLayerPoint(pt),
359 latlng = map.layerPointToLatLng(layerPoint),
360 event = L.extend(data || {}, {contextmenu: this});
362 this._showLocation = {
364 layerPoint: layerPoint,
368 if (data && data.relatedTarget){
369 this._showLocation.relatedTarget = data.relatedTarget;
372 this._setPosition(pt);
374 if (!this._visible) {
375 this._container.style.display = 'block';
376 this._visible = true;
379 this._map.fire('contextmenu.show', event);
385 this._visible = false;
386 this._container.style.display = 'none';
387 this._map.fire('contextmenu.hide', {contextmenu: this});
391 _getIcon: function (options) {
392 return L.Browser.retina && options.retinaIcon || options.icon;
395 _getIconCls: function (options) {
396 return L.Browser.retina && options.retinaIconCls || options.iconCls;
399 _setPosition: function (pt) {
400 var mapSize = this._map.getSize(),
401 container = this._container,
402 containerSize = this._getElementSize(container),
405 if (this._map.options.contextmenuAnchor) {
406 anchor = L.point(this._map.options.contextmenuAnchor);
410 container._leaflet_pos = pt;
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';
416 container.style.left = Math.max(pt.x, 0) + 'px';
417 container.style.right = 'auto';
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';
424 container.style.top = Math.max(pt.y, 0) + 'px';
425 container.style.bottom = 'auto';
429 _getElementSize: function (el) {
430 var size = this._size,
431 initialDisplay = el.style.display;
433 if (!size || this._sizeChanged) {
436 el.style.left = '-999999px';
437 el.style.right = 'auto';
438 el.style.display = 'block';
440 size.x = el.offsetWidth;
441 size.y = el.offsetHeight;
443 el.style.left = 'auto';
444 el.style.display = initialDisplay;
446 this._sizeChanged = false;
452 _onKeyDown: function (e) {
455 // If ESC pressed and context menu is visible hide it
461 _onItemMouseOver: function (e) {
462 L.DomUtil.addClass(e.target || e.srcElement, 'over');
465 _onItemMouseOut: function (e) {
466 L.DomUtil.removeClass(e.target || e.srcElement, 'over');
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();
479 unbindContextMenu: function (){
480 this.off('contextmenu', this._showContextMenu, this);
485 addContextMenuItem: function (item) {
486 this.options.contextmenuItems.push(item);
489 removeContextMenuItemWithIndex: function (index) {
491 for (var i = 0; i < this.options.contextmenuItems.length; i++) {
492 if (this.options.contextmenuItems[i].index == index){
496 var elem = items.pop();
497 while (elem !== undefined) {
498 this.options.contextmenuItems.splice(elem,1);
503 replaceContextMenuItem: function (item) {
504 this.removeContextMenuItemWithIndex(item.index);
505 this.addContextMenuItem(item);
508 _initContextMenu: function () {
511 this.on('contextmenu', this._showContextMenu, this);
514 _showContextMenu: function (e) {
518 if (this._map.contextmenu) {
519 data = L.extend({relatedTarget: this}, e);
521 pt = this._map.mouseEventToContainerPoint(e.originalEvent);
523 if (!this.options.contextmenuInheritItems) {
524 this._map.contextmenu.hideAllItems();
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));
532 this._map.once('contextmenu.hide', this._hideContextMenu, this);
534 this._map.contextmenu.showAt(pt, data);
538 _hideContextMenu: function () {
541 for (i = 0, l = this._items.length; i < l; i++) {
542 this._map.contextmenu.removeItem(this._items[i]);
544 this._items.length = 0;
546 if (!this.options.contextmenuInheritItems) {
547 this._map.contextmenu.showAllItems();
552 var classes = [L.Marker, L.Path],
555 contextmenuItems: [],
556 contextmenuInheritItems: true
560 for (i = 0, l = classes.length; i < l; i++) {
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;
568 cls.mergeOptions(defaultOptions);
571 cls.addInitHook(function () {
572 if (this.options.contextmenu) {
573 this._initContextMenu();
577 cls.include(L.Mixin.ContextMenu);
579 return L.Map.ContextMenu;