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;