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;