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;