diff --git a/package.json b/package.json index 6b1e0522..836a6260 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "georsstogeojson": "^0.1.0", "jsdom": "^24.0.0", "leaflet": "1.9.4", - "leaflet-contextmenu": "^1.4.0", "leaflet-editable": "^1.3.0", "leaflet-editinosm": "0.2.3", "leaflet-formbuilder": "0.2.10", diff --git a/scripts/vendorsjs.sh b/scripts/vendorsjs.sh index a4835707..46d5993c 100755 --- a/scripts/vendorsjs.sh +++ b/scripts/vendorsjs.sh @@ -14,7 +14,6 @@ mkdir -p umap/static/umap/vendors/minimap/ && cp -r node_modules/leaflet-minimap mkdir -p umap/static/umap/vendors/loading/ && cp -r node_modules/leaflet-loading/src/Control.Loading.* umap/static/umap/vendors/loading/ mkdir -p umap/static/umap/vendors/markercluster/ && cp -r node_modules/leaflet.markercluster/dist/leaflet.markercluster.* umap/static/umap/vendors/markercluster/ mkdir -p umap/static/umap/vendors/markercluster/ && cp -r node_modules/leaflet.markercluster/dist/MarkerCluster.* umap/static/umap/vendors/markercluster/ -mkdir -p umap/static/umap/vendors/contextmenu/ && cp -r node_modules/leaflet-contextmenu/dist/leaflet.contextmenu.min.* umap/static/umap/vendors/contextmenu/ mkdir -p umap/static/umap/vendors/heat/ && cp -r node_modules/leaflet.heat/dist/leaflet-heat.js umap/static/umap/vendors/heat/ mkdir -p umap/static/umap/vendors/fullscreen/ && cp -r node_modules/leaflet-fullscreen/dist/** umap/static/umap/vendors/fullscreen/ mkdir -p umap/static/umap/vendors/toolbar/ && cp -r node_modules/leaflet-toolbar/dist/leaflet.toolbar.* umap/static/umap/vendors/toolbar/ diff --git a/umap/static/umap/css/contextmenu.css b/umap/static/umap/css/contextmenu.css index da55f1c8..e7ed2508 100644 --- a/umap/static/umap/css/contextmenu.css +++ b/umap/static/umap/css/contextmenu.css @@ -9,3 +9,8 @@ .umap-contextmenu li + li { margin-top: var(--text-margin); } + +.umap-contextmenu hr { + margin-top: var(--text-margin); + margin-bottom: var(--text-margin); +} diff --git a/umap/static/umap/js/modules/data/features.js b/umap/static/umap/js/modules/data/features.js index 0b334c81..2db9d99d 100644 --- a/umap/static/umap/js/modules/data/features.js +++ b/umap/static/umap/js/modules/data/features.js @@ -591,6 +591,55 @@ class Feature { } } } + + getContextMenuItems(event) { + const permalink = this.getPermalink() + let items = [] + if (permalink) { + items.push({ + label: translate('Permalink'), + action: () => { + window.open(permalink) + }, + }) + } + items.push({ + label: translate('Copy as GeoJSON'), + action: () => { + L.Util.copyToClipboard(JSON.stringify(this.toGeoJSON())) + this.map.tooltip.open({ content: L._('✅ Copied!') }) + }, + }) + if (this.map.editEnabled && !this.isReadOnly()) { + items = items.concat(this.getContextMenuEditItems(event)) + } + return items + } + + getContextMenuEditItems() { + let items = ['-'] + if (this.map.editedFeature !== this) { + items.push({ + label: `${translate('Edit this feature')} (⇧+Click)`, + action: () => this.edit(), + }) + } + items = items.concat( + { + label: this.map.help.displayLabel('EDIT_FEATURE_LAYER'), + action: () => this.datalayer.edit(), + }, + { + label: translate('Delete this feature'), + action: () => this.confirmDelete(), + }, + { + label: translate('Clone this feature'), + action: () => this.clone(), + } + ) + return items + } } export class Point extends Feature { @@ -762,6 +811,62 @@ class Path extends Feature { } if (callback) callback.call(this) } + + getContextMenuItems(event) { + const items = super.getContextMenuItems(event) + items.push({ + label: translate('Display measure'), + action: () => Alert.info(this.ui.getMeasure()), + }) + if (this.map.editEnabled && !this.isReadOnly() && this.isMulti()) { + items.push(...this.getContextMenuMultiItems(event)) + } + return items + } + + getContextMenuMultiItems(event) { + const items = [ + '-', + { + label: translate('Remove shape from the multi'), + action: () => { + this.ui.enableEdit().deleteShapeAt(event.latlng) + }, + }, + ] + const shape = this.ui.shapeAt(event.latlng) + if (this.ui._latlngs.indexOf(shape) > 0) { + items.push({ + label: translate('Make main shape'), + action: () => { + this.ui.enableEdit().deleteShape(shape) + this.ui.editor.prependShape(shape) + }, + }) + } + return items + } + + getContextMenuEditItems(event) { + const items = super.getContextMenuEditItems(event) + if (this.map?.editedFeature !== this && this.isSameClass(this.map.editedFeature)) { + items.push({ + label: translate('Transfer shape to edited feature'), + action: () => { + this.transferShape(event.latlng, this.map.editedFeature) + }, + }) + } + if (this.isMulti()) { + items.push({ + label: translate('Extract shape to separate feature'), + action: () => { + this.ui.isolateShape(event.latlng) + }, + }) + } + return items + } } export class LineString extends Path { @@ -875,6 +980,41 @@ export class LineString extends Path { isMulti() { return !LineUtil.isFlat(this.coordinates) && this.coordinates.length > 1 } + + getContextMenuEditItems(event) { + const items = super.getContextMenuEditItems(event) + const vertexClicked = event.vertex + if (!this.isMulti()) { + items.push({ + label: translate('Transform to polygon'), + action: () => this.toPolygon(), + }) + } + if (vertexClicked) { + const index = event.vertex.getIndex() + if (index !== 0 && index !== event.vertex.getLastIndex()) { + items.push({ + label: translate('Split line'), + action: () => event.vertex.split(), + }) + } else if (index === 0 || index === event.vertex.getLastIndex()) { + items.push({ + label: this.map.help.displayLabel('CONTINUE_LINE'), + action: () => event.vertex.continue(), + }) + } + } + return items + } + + getContextMenuMultiItems(event) { + const items = super.getContextMenuMultiItems(event) + items.push({ + label: translate('Merge lines'), + action: () => this.mergeShapes(), + }) + return items + } } export class Polygon extends Path { @@ -985,4 +1125,21 @@ export class Polygon extends Path { items.push(U.CreateHoleAction) return items } + + getContextMenuEditItems(event) { + const items = super.getContextMenuEditItems(event) + const shape = this.ui.shapeAt(event.latlng) + // No multi and no holes. + if (shape && !this.isMulti() && (LineUtil.isFlat(shape) || shape.length === 1)) { + items.push({ + label: translate('Transform to lines'), + action: () => this.toLineString(), + }) + } + items.push({ + label: translate('Start a hole here'), + action: () => this.ui.startHole(event), + }) + return items + } } diff --git a/umap/static/umap/js/modules/global.js b/umap/static/umap/js/modules/global.js index 61a92802..52b23fa1 100644 --- a/umap/static/umap/js/modules/global.js +++ b/umap/static/umap/js/modules/global.js @@ -9,6 +9,7 @@ import { } from './autocomplete.js' import Browser from './browser.js' import Caption from './caption.js' +import ContextMenu from './ui/contextmenu.js' import Facets from './facets.js' import { Formatter } from './formatter.js' import Help from './help.js' @@ -43,6 +44,7 @@ window.U = { AutocompleteDatalist, Browser, Caption, + ContextMenu, DataLayer, DataLayerPermissions, Dialog, diff --git a/umap/static/umap/js/modules/rendering/ui.js b/umap/static/umap/js/modules/rendering/ui.js index 72a3354a..64f037d2 100644 --- a/umap/static/umap/js/modules/rendering/ui.js +++ b/umap/static/umap/js/modules/rendering/ui.js @@ -9,6 +9,7 @@ import { latLng, LatLng, LatLngBounds, + DomEvent, } from '../../../vendors/leaflet/leaflet-src.esm.js' import { translate } from '../i18n.js' import { uMapAlert as Alert } from '../../components/alerts/alert.js' @@ -36,7 +37,7 @@ const FeatureMixin = { }, addInteractions: function () { - this.on('contextmenu editable:vertex:contextmenu', this._showContextMenu) + this.on('contextmenu editable:vertex:contextmenu', this.onContextMenu) this.on('click', this.onClick) }, @@ -61,7 +62,7 @@ const FeatureMixin = { }).addTo(this._map, this.feature, event.latlng) } } - L.DomEvent.stop(event) + DomEvent.stop(event) }, resetTooltip: function () { @@ -83,67 +84,14 @@ const FeatureMixin = { } }, - _showContextMenu: function (event) { - L.DomEvent.stop(event) - const pt = this._map.mouseEventToContainerPoint(event.originalEvent) - event.relatedTarget = this - this._map.contextmenu.showAt(pt, event) - }, - - getContextMenuItems: function (event) { - const permalink = this.feature.getPermalink() - let items = [] - if (permalink) { - items.push({ - text: translate('Permalink'), - callback: () => { - window.open(permalink) - }, - }) - } - items.push({ - text: translate('Copy as GeoJSON'), - callback: () => { - L.Util.copyToClipboard(JSON.stringify(this.feature.toGeoJSON())) - this._map.tooltip.open({ content: L._('✅ Copied!') }) - }, - }) - if (this._map.editEnabled && !this.feature.isReadOnly()) { - items = items.concat(this.getContextMenuEditItems(event)) - } - return items - }, - - getContextMenuEditItems: function () { - let items = ['-'] - if (this._map.editedFeature !== this) { - items.push({ - text: `${translate('Edit this feature')} (⇧+Click)`, - callback: this.feature.edit, - context: this.feature, - iconCls: 'umap-edit', - }) - } - items = items.concat( - { - text: this._map.help.displayLabel('EDIT_FEATURE_LAYER'), - callback: this.feature.datalayer.edit, - context: this.feature.datalayer, - iconCls: 'umap-edit', - }, - { - text: translate('Delete this feature'), - callback: this.feature.confirmDelete, - context: this.feature, - iconCls: 'umap-delete', - }, - { - text: translate('Clone this feature'), - callback: this.feature.clone, - context: this.feature, - } + onContextMenu: function (event) { + DomEvent.stop(event) + const items = this._map.getContextMenuItems(event) + items.push('-', ...this.feature.getContextMenuItems(event)) + this._map.contextmenu.open( + [event.originalEvent.clientX, event.originalEvent.clientY], + items ) - return items }, onCommit: function () { @@ -360,65 +308,6 @@ const PathMixin = { }).addTo(this._map, this, event.latlng, event.vertex) }, - getContextMenuItems: function (event) { - let items = FeatureMixin.getContextMenuItems.call(this, event) - items.push({ - text: translate('Display measure'), - callback: () => Alert.info(this.getMeasure()), - }) - if (this._map.editEnabled && !this.feature.isReadOnly() && this.feature.isMulti()) { - items = items.concat(this.getContextMenuMultiItems(event)) - } - return items - }, - - getContextMenuMultiItems: function (event) { - const items = [ - '-', - { - text: translate('Remove shape from the multi'), - callback: () => { - this.enableEdit().deleteShapeAt(event.latlng) - }, - }, - ] - const shape = this.shapeAt(event.latlng) - if (this._latlngs.indexOf(shape) > 0) { - items.push({ - text: translate('Make main shape'), - callback: () => { - this.enableEdit().deleteShape(shape) - this.editor.prependShape(shape) - }, - }) - } - return items - }, - - getContextMenuEditItems: function (event) { - const items = FeatureMixin.getContextMenuEditItems.call(this, event) - if ( - this._map?.editedFeature !== this.feature && - this.feature.isSameClass(this._map.editedFeature) - ) { - items.push({ - text: translate('Transfer shape to edited feature'), - callback: () => { - this.feature.transferShape(event.latlng, this._map.editedFeature) - }, - }) - } - if (this.feature.isMulti()) { - items.push({ - text: translate('Extract shape to separate feature'), - callback: () => { - this.isolateShape(event.latlng) - }, - }) - } - return items - }, - isolateShape: function (atLatLng) { if (!this.feature.isMulti()) return const shape = this.enableEdit().deleteShapeAt(atLatLng) @@ -463,46 +352,6 @@ export const LeafletPolyline = Polyline.extend({ return actions }, - getContextMenuEditItems: function (event) { - const items = PathMixin.getContextMenuEditItems.call(this, event) - const vertexClicked = event.vertex - let index - if (!this.feature.isMulti()) { - items.push({ - text: translate('Transform to polygon'), - callback: this.feature.toPolygon, - context: this.feature, - }) - } - if (vertexClicked) { - index = event.vertex.getIndex() - if (index !== 0 && index !== event.vertex.getLastIndex()) { - items.push({ - text: translate('Split line'), - callback: event.vertex.split, - context: event.vertex, - }) - } else if (index === 0 || index === event.vertex.getLastIndex()) { - items.push({ - text: this._map.help.displayLabel('CONTINUE_LINE'), - callback: event.vertex.continue, - context: event.vertex.continue, - }) - } - } - return items - }, - - getContextMenuMultiItems: function (event) { - const items = PathMixin.getContextMenuMultiItems.call(this, event) - items.push({ - text: translate('Merge lines'), - callback: this.feature.mergeShapes, - context: this.feature, - }) - return items - }, - getMeasure: function (shape) { // FIXME: compute from data in feature (with TurfJS) const length = L.GeoUtil.lineLength(this._map, shape || this._defaultShape()) @@ -516,29 +365,6 @@ export const LeafletPolygon = Polygon.extend({ getClass: () => LeafletPolygon, - getContextMenuEditItems: function (event) { - const items = PathMixin.getContextMenuEditItems.call(this, event) - const shape = this.shapeAt(event.latlng) - // No multi and no holes. - if ( - shape && - !this.feature.isMulti() && - (LineUtil.isFlat(shape) || shape.length === 1) - ) { - items.push({ - text: translate('Transform to lines'), - callback: this.feature.toLineString, - context: this.feature, - }) - } - items.push({ - text: translate('Start a hole here'), - callback: this.startHole, - context: this, - }) - return items - }, - startHole: function (event) { this.enableEdit().newHole(event.latlng) }, diff --git a/umap/static/umap/js/modules/ui/base.js b/umap/static/umap/js/modules/ui/base.js index 50b55487..725e3c9b 100644 --- a/umap/static/umap/js/modules/ui/base.js +++ b/umap/static/umap/js/modules/ui/base.js @@ -72,9 +72,9 @@ export class Positioned { left = x - this.container.offsetWidth } if (y < window.innerHeight / 2) { - top = y + top = Math.min(y, window.innerHeight - this.container.offsetHeight) } else { - top = y - this.container.offsetHeight + top = Math.max(0, y - this.container.offsetHeight) } this.setPosition({ left, top }) } diff --git a/umap/static/umap/js/modules/ui/contextmenu.js b/umap/static/umap/js/modules/ui/contextmenu.js index 1ff33a5d..9231129a 100644 --- a/umap/static/umap/js/modules/ui/contextmenu.js +++ b/umap/static/umap/js/modules/ui/contextmenu.js @@ -18,14 +18,18 @@ export default class ContextMenu extends Positioned { open([x, y], items) { this.container.innerHTML = '' for (const item of items) { - const li = loadTemplate( - `
` - ) - li.addEventListener('click', () => { - this.close() - item.action() - }) - this.container.appendChild(li) + if (item === '-') { + this.container.appendChild(document.createElement('hr')) + } else { + const li = loadTemplate( + `` + ) + li.addEventListener('click', () => { + this.close() + item.action() + }) + this.container.appendChild(li) + } } document.body.appendChild(this.container) this.computePosition([x, y]) diff --git a/umap/static/umap/js/umap.controls.js b/umap/static/umap/js/umap.controls.js index 8fc6a5df..ed969d94 100644 --- a/umap/static/umap/js/umap.controls.js +++ b/umap/static/umap/js/umap.controls.js @@ -1140,23 +1140,6 @@ L.Control.Loading.include({ }, }) -/* - * Make it dynamic - */ -U.ContextMenu = L.Map.ContextMenu.extend({ - _createItems: function (e) { - this._map.setContextMenuItems(e) - L.Map.ContextMenu.prototype._createItems.call(this) - }, - - _showAtPoint: function (pt, e) { - this._items = [] - this._container.innerHTML = '' - this._createItems(e) - L.Map.ContextMenu.prototype._showAtPoint.call(this, pt, e) - }, -}) - U.Editable = L.Editable.extend({ initialize: function (map, options) { L.Editable.prototype.initialize.call(this, map, options) diff --git a/umap/static/umap/js/umap.js b/umap/static/umap/js/umap.js index 66194aab..6c5aa3d9 100644 --- a/umap/static/umap/js/umap.js +++ b/umap/static/umap/js/umap.js @@ -58,6 +58,7 @@ U.Map = L.Map.extend({ this.panel = new U.Panel(this) this.dialog = new U.Dialog({ className: 'dark' }) this.tooltip = new U.Tooltip(this._controlContainer) + this.contextmenu = new U.ContextMenu() if (this.hasEditMode()) { this.editPanel = new U.EditPanel(this) this.fullPanel = new U.FullPanel(this) @@ -197,8 +198,8 @@ U.Map = L.Map.extend({ window.onbeforeunload = () => (this.editEnabled && this.isDirty) || null this.backup() - this.initContextMenu() this.on('click', this.closeInplaceToolbar) + this.on('contextmenu', this.onContextMenu) }, initSyncEngine: async function () { @@ -1679,118 +1680,107 @@ U.Map = L.Map.extend({ this.loader.onAdd(this) }, - initContextMenu: function () { - this.contextmenu = new U.ContextMenu(this) - this.contextmenu.enable() - }, - - setContextMenuItems: function (e) { - let items = [] + getContextMenuItems: function (event) { + const items = [] if (this._zoom !== this.getMaxZoom()) { items.push({ - text: L._('Zoom in'), - callback: function () { - this.zoomIn() - }, + label: L._('Zoom in'), + action: () => this.zoomIn(), }) } if (this._zoom !== this.getMinZoom()) { items.push({ - text: L._('Zoom out'), - callback: function () { - this.zoomOut() - }, + label: L._('Zoom out'), + action: () => this.zoomOut(), }) } - if (e?.relatedTarget) { - if (e.relatedTarget.getContextMenuItems) { - items = items.concat(e.relatedTarget.getContextMenuItems(e)) - } - } if (this.hasEditMode()) { items.push('-') if (this.editEnabled) { if (!this.isDirty) { items.push({ - text: this.help.displayLabel('STOP_EDIT'), - callback: this.disableEdit, + label: this.help.displayLabel('STOP_EDIT'), + action: () => this.disableEdit(), }) } if (this.options.enableMarkerDraw) { items.push({ - text: this.help.displayLabel('DRAW_MARKER'), - callback: this.startMarker, - context: this, + label: this.help.displayLabel('DRAW_MARKER'), + action: () => this.startMarker(event), }) } if (this.options.enablePolylineDraw) { items.push({ - text: this.help.displayLabel('DRAW_POLYGON'), - callback: this.startPolygon, - context: this, + label: this.help.displayLabel('DRAW_POLYGON'), + action: () => this.startPolygon(event), }) } if (this.options.enablePolygonDraw) { items.push({ - text: this.help.displayLabel('DRAW_LINE'), - callback: this.startPolyline, - context: this, + label: this.help.displayLabel('DRAW_LINE'), + action: () => this.startPolyline(event), }) } items.push('-') items.push({ - text: L._('Help'), - callback: function () { - this.help.show('edit') - }, + label: L._('Help'), + action: () => this.help.show('edit'), }) } else { items.push({ - text: this.help.displayLabel('TOGGLE_EDIT'), - callback: this.enableEdit, + label: this.help.displayLabel('TOGGLE_EDIT'), + action: () => this.enableEdit(), }) } } items.push( '-', { - text: L._('Open browser'), - callback: () => this.openBrowser('layers'), + label: L._('Open browser'), + action: () => this.openBrowser('layers'), }, { - text: L._('Browse data'), - callback: () => this.openBrowser('data'), + label: L._('Browse data'), + action: () => this.openBrowser('data'), } ) if (this.options.facetKey) { items.push({ - text: L._('Filter data'), - callback: () => this.openBrowser('filters'), + label: L._('Filter data'), + action: () => this.openBrowser('filters'), }) } items.push( { - text: L._('Open caption'), - callback: this.openCaption, + label: L._('Open caption'), + action: () => this.openCaption(), }, { - text: this.help.displayLabel('SEARCH'), - callback: this.search, + label: this.help.displayLabel('SEARCH'), + action: () => this.search(event), } ) if (this.options.urls.routing) { items.push('-', { - text: L._('Directions from here'), - callback: this.openExternalRouting, + label: L._('Directions from here'), + action: () => this.openExternalRouting(event), }) } if (this.options.urls.edit_in_osm) { items.push('-', { - text: L._('Edit in OpenStreetMap'), - callback: this.editInOSM, + label: L._('Edit in OpenStreetMap'), + action: () => this.editInOSM(event), }) } - this.options.contextmenuItems = items + return items + }, + + onContextMenu: function (event) { + const items = this.getContextMenuItems(event) + this.contextmenu.open( + [event.originalEvent.clientX, event.originalEvent.clientY], + items + ) }, editInOSM: function (e) { diff --git a/umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css b/umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css deleted file mode 100644 index ef6c6a0e..00000000 --- a/umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.css +++ /dev/null @@ -1 +0,0 @@ -.leaflet-contextmenu{display:none;box-shadow:0 1px 7px rgba(0,0,0,0.4);-webkit-border-radius:4px;border-radius:4px;padding:4px 0;background-color:#fff;cursor:default;-webkit-user-select:none;-moz-user-select:none;user-select:none}.leaflet-contextmenu a.leaflet-contextmenu-item{display:block;color:#222;font-size:12px;line-height:20px;text-decoration:none;padding:0 12px;border-top:1px solid transparent;border-bottom:1px solid transparent;cursor:default;outline:0}.leaflet-contextmenu a.leaflet-contextmenu-item-disabled{opacity:.5}.leaflet-contextmenu a.leaflet-contextmenu-item.over{background-color:#f4f4f4;border-top:1px solid #f0f0f0;border-bottom:1px solid #f0f0f0}.leaflet-contextmenu a.leaflet-contextmenu-item-disabled.over{background-color:inherit;border-top:1px solid transparent;border-bottom:1px solid transparent}.leaflet-contextmenu-icon{margin:2px 8px 0 0;width:16px;height:16px;float:left;border:0}.leaflet-contextmenu-separator{border-bottom:1px solid #ccc;margin:5px 0} diff --git a/umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js b/umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js deleted file mode 100644 index 6aca0f6c..00000000 --- a/umap/static/umap/vendors/contextmenu/leaflet.contextmenu.min.js +++ /dev/null @@ -1,7 +0,0 @@ -/* - Leaflet.contextmenu, a context menu for Leaflet. - (c) 2015, Adam Ratcliffe, GeoSmart Maps Limited - - @preserve -*/ -(function(t){var e;if(typeof define==="function"&&define.amd){define(["leaflet"],t)}else if(typeof module==="object"&&typeof module.exports==="object"){e=require("leaflet");module.exports=t(e)}else{if(typeof window.L==="undefined"){throw new Error("Leaflet must be loaded first")}t(window.L)}})(function(t){t.Map.mergeOptions({contextmenuItems:[]});t.Map.ContextMenu=t.Handler.extend({_touchstart:t.Browser.msPointer?"MSPointerDown":t.Browser.pointer?"pointerdown":"touchstart",statics:{BASE_CLS:"leaflet-contextmenu"},initialize:function(e){t.Handler.prototype.initialize.call(this,e);this._items=[];this._visible=false;var n=this._container=t.DomUtil.create("div",t.Map.ContextMenu.BASE_CLS,e._container);n.style.zIndex=1e4;n.style.position="absolute";if(e.options.contextmenuWidth){n.style.width=e.options.contextmenuWidth+"px"}this._createItems();t.DomEvent.on(n,"click",t.DomEvent.stop).on(n,"mousedown",t.DomEvent.stop).on(n,"dblclick",t.DomEvent.stop).on(n,"contextmenu",t.DomEvent.stop)},addHooks:function(){var e=this._map.getContainer();t.DomEvent.on(e,"mouseleave",this._hide,this).on(document,"keydown",this._onKeyDown,this);if(t.Browser.touch){t.DomEvent.on(document,this._touchstart,this._hide,this)}this._map.on({contextmenu:this._show,mousedown:this._hide,movestart:this._hide,zoomstart:this._hide},this)},removeHooks:function(){var e=this._map.getContainer();t.DomEvent.off(e,"mouseleave",this._hide,this).off(document,"keydown",this._onKeyDown,this);if(t.Browser.touch){t.DomEvent.off(document,this._touchstart,this._hide,this)}this._map.off({contextmenu:this._show,mousedown:this._hide,movestart:this._hide,zoomstart:this._hide},this)},showAt:function(e,n){if(e instanceof t.LatLng){e=this._map.latLngToContainerPoint(e)}this._showAtPoint(e,n)},hide:function(){this._hide()},addItem:function(t){return this.insertItem(t)},insertItem:function(t,e){e=e!==undefined?e:this._items.length;var n=this._createItem(this._container,t,e);this._items.push(n);this._sizeChanged=true;this._map.fire("contextmenu.additem",{contextmenu:this,el:n.el,index:e});return n.el},removeItem:function(e){var n=this._container;if(!isNaN(e)){e=n.children[e]}if(e){this._removeItem(t.Util.stamp(e));this._sizeChanged=true;this._map.fire("contextmenu.removeitem",{contextmenu:this,el:e});return e}return null},removeAllItems:function(){var e=this._container.children,n;while(e.length){n=e[0];this._removeItem(t.Util.stamp(n))}return e},hideAllItems:function(){var t,e,n;for(e=0,n=this._items.length;e