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,
78 removeHooks: function () {
79 var container = this._map.getContainer();
82 .off(container, 'mouseleave', this._hide, this)
83 .off(document, 'keydown', this._onKeyDown, this);
85 if (L.Browser.touch) {
86 L.DomEvent.off(document, this._touchstart, this._hide, this);
90 contextmenu: this._show,
91 mousedown: this._hide,
96 showAt: function (point, data) {
97 if (point instanceof L.LatLng) {
98 point = this._map.latLngToContainerPoint(point);
100 this._showAtPoint(point, data);
107 addItem: function (options) {
108 return this.insertItem(options);
111 insertItem: function (options, index) {
112 index = index !== undefined ? index: this._items.length;
114 var item = this._createItem(this._container, options, index);
116 this._items.push(item);
118 this._sizeChanged = true;
120 this._map.fire('contextmenu.additem', {
129 removeItem: function (item) {
130 var container = this._container;
133 item = container.children[item];
137 this._removeItem(L.Util.stamp(item));
139 this._sizeChanged = true;
141 this._map.fire('contextmenu.removeitem', {
152 removeAllItems: function () {
153 var items = this._container.children,
156 while (items.length) {
158 this._removeItem(L.Util.stamp(item));
163 hideAllItems: function () {
166 for (i = 0, l = this._items.length; i < l; i++) {
167 item = this._items[i];
168 item.el.style.display = 'none';
172 showAllItems: function () {
175 for (i = 0, l = this._items.length; i < l; i++) {
176 item = this._items[i];
177 item.el.style.display = '';
181 setDisabled: function (item, disabled) {
182 var container = this._container,
183 itemCls = L.Map.ContextMenu.BASE_CLS + '-item';
186 item = container.children[item];
189 if (item && L.DomUtil.hasClass(item, itemCls)) {
191 L.DomUtil.addClass(item, itemCls + '-disabled');
192 this._map.fire('contextmenu.disableitem', {
197 L.DomUtil.removeClass(item, itemCls + '-disabled');
198 this._map.fire('contextmenu.enableitem', {
206 isVisible: function () {
207 return this._visible;
210 _createItems: function () {
211 var itemOptions = this._map.options.contextmenuItems,
215 for (i = 0, l = itemOptions.length; i < l; i++) {
216 this._items.push(this._createItem(this._container, itemOptions[i]));
220 _createItem: function (container, options, index) {
221 if (options.separator || options === '-') {
222 return this._createSeparator(container, index);
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),
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>';
239 el.innerHTML = html + options.text;
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);
248 if (L.Browser.touch) {
249 L.DomEvent.on(el, this._touchstart, L.DomEvent.stopPropagation);
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);
258 id: L.Util.stamp(el),
264 _removeItem: function (id) {
269 for (i = 0, l = this._items.length; i < l; i++) {
270 item = this._items[i];
272 if (item.id === id) {
274 callback = item.callback;
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);
283 if (L.Browser.touch) {
284 L.DomEvent.off(el, this._touchstart, L.DomEvent.stopPropagation);
287 if (!L.Browser.pointer) {
288 L.DomEvent.on(el, 'click', this._onItemMouseOut, this);
292 this._container.removeChild(el);
293 this._items.splice(i, 1);
301 _createSeparator: function (container, index) {
302 var el = this._insertElementAt('div', L.Map.ContextMenu.BASE_CLS + '-separator', container, index);
305 id: L.Util.stamp(el),
310 _createEventHandler: function (el, func, context, hideOnSelect) {
313 disabledCls = L.Map.ContextMenu.BASE_CLS + '-item-disabled',
314 hideOnSelect = (hideOnSelect !== undefined) ? hideOnSelect : true;
316 return function (e) {
317 if (L.DomUtil.hasClass(el, disabledCls)) {
322 containerPoint = me._showLocation.containerPoint,
323 layerPoint = map.containerPointToLayerPoint(containerPoint),
324 latlng = map.layerPointToLatLng(layerPoint),
325 relatedTarget = me._showLocation.relatedTarget,
327 containerPoint: containerPoint,
328 layerPoint: layerPoint,
330 relatedTarget: relatedTarget
338 func.call(context || map, data);
341 me._map.fire('contextmenu.select', {
348 _insertElementAt: function (tagName, className, container, index) {
350 el = document.createElement(tagName);
352 el.className = className;
354 if (index !== undefined) {
355 refEl = container.children[index];
359 container.insertBefore(el, refEl);
361 container.appendChild(el);
367 _show: function (e) {
368 this._showAtPoint(e.containerPoint, e);
371 _showAtPoint: function (pt, data) {
372 if (this._items.length) {
374 event = L.extend(data || {}, {contextmenu: this});
376 this._showLocation = {
380 if (data && data.relatedTarget){
381 this._showLocation.relatedTarget = data.relatedTarget;
384 this._setPosition(pt);
386 if (!this._visible) {
387 this._container.style.display = 'block';
388 this._visible = true;
391 this._map.fire('contextmenu.show', event);
397 this._visible = false;
398 this._container.style.display = 'none';
399 this._map.fire('contextmenu.hide', {contextmenu: this});
403 _getIcon: function (options) {
404 return L.Browser.retina && options.retinaIcon || options.icon;
407 _getIconCls: function (options) {
408 return L.Browser.retina && options.retinaIconCls || options.iconCls;
411 _setPosition: function (pt) {
412 var mapSize = this._map.getSize(),
413 container = this._container,
414 containerSize = this._getElementSize(container),
417 if (this._map.options.contextmenuAnchor) {
418 anchor = L.point(this._map.options.contextmenuAnchor);
422 container._leaflet_pos = pt;
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';
428 container.style.left = Math.max(pt.x, 0) + 'px';
429 container.style.right = 'auto';
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';
436 container.style.top = Math.max(pt.y, 0) + 'px';
437 container.style.bottom = 'auto';
441 _getElementSize: function (el) {
442 var size = this._size,
443 initialDisplay = el.style.display;
445 if (!size || this._sizeChanged) {
448 el.style.left = '-999999px';
449 el.style.right = 'auto';
450 el.style.display = 'block';
452 size.x = el.offsetWidth;
453 size.y = el.offsetHeight;
455 el.style.left = 'auto';
456 el.style.display = initialDisplay;
458 this._sizeChanged = false;
464 _onKeyDown: function (e) {
467 // If ESC pressed and context menu is visible hide it
473 _onItemMouseOver: function (e) {
474 L.DomUtil.addClass(e.target || e.srcElement, 'over');
477 _onItemMouseOut: function (e) {
478 L.DomUtil.removeClass(e.target || e.srcElement, 'over');
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();
491 unbindContextMenu: function (){
492 this.off('contextmenu', this._showContextMenu, this);
497 addContextMenuItem: function (item) {
498 this.options.contextmenuItems.push(item);
501 removeContextMenuItemWithIndex: function (index) {
503 for (var i = 0; i < this.options.contextmenuItems.length; i++) {
504 if (this.options.contextmenuItems[i].index == index){
508 var elem = items.pop();
509 while (elem !== undefined) {
510 this.options.contextmenuItems.splice(elem,1);
515 replaceContextMenuItem: function (item) {
516 this.removeContextMenuItemWithIndex(item.index);
517 this.addContextMenuItem(item);
520 _initContextMenu: function () {
523 this.on('contextmenu', this._showContextMenu, this);
526 _showContextMenu: function (e) {
530 if (this._map.contextmenu) {
531 data = L.extend({relatedTarget: this}, e);
533 pt = this._map.mouseEventToContainerPoint(e.originalEvent);
535 if (!this.options.contextmenuInheritItems) {
536 this._map.contextmenu.hideAllItems();
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));
544 this._map.once('contextmenu.hide', this._hideContextMenu, this);
546 this._map.contextmenu.showAt(pt, data);
550 _hideContextMenu: function () {
553 for (i = 0, l = this._items.length; i < l; i++) {
554 this._map.contextmenu.removeItem(this._items[i]);
556 this._items.length = 0;
558 if (!this.options.contextmenuInheritItems) {
559 this._map.contextmenu.showAllItems();
564 var classes = [L.Marker, L.Path],
567 contextmenuItems: [],
568 contextmenuInheritItems: true
572 for (i = 0, l = classes.length; i < l; i++) {
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;
580 cls.mergeOptions(defaultOptions);
583 cls.addInitHook(function () {
584 if (this.options.contextmenu) {
585 this._initContextMenu();
589 cls.include(L.Mixin.ContextMenu);
591 return L.Map.ContextMenu;