Context Menu
ContextMenu is a context menu that displays a right-click menu interface.
Common Methods
| Method | Description |
|---|---|
| add | Add menu item |
| show | Show menu |
| hide | Hide menu |
| toggle | Toggle menu visibility |
Menu Item Types
| Type | Description |
|---|---|
| seperator | Separator line |
| custom | Custom menu item |
| multi | Multiple buttons |
| submenu | Submenu item |
| hovermenu | Hover menu item |
| normal | Default normal menu item |
Examples
Map Context Menu
// Selected icon, or specify src path directly
const checkIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANpJREFUOE9jZKAQMFKon2FgDKjZHVHf4rqiEeR6kl1QuzOyn+E/4/lmj2WLSDagekdkASMjg1aL+/I0WNgR7YLaHRG2/xgZYlrdV6QjBzzRBtTsijzzl4nVvd1l0VucBtTsjlz5/z/jt1a3ZYnIimp2RZ7//+9fQ6vHyo3o0Q53Qc32CAcGpv88DIxMxQyMDB9aXJcHghTX7IxcyMjAeLTZfdksbGkGqxdqdkUuZ2BgkPjPyDCFgeG/W6srqr+JCoPqXZG9jAwM9iyszy0aHA/8wZViiQ7EYWwAAPShQBF/1IlVAAAAAElFTkSuQmCC";
map.setMenu(event => {
return new vjmap.ContextMenu({
event: event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">Context menu demo</span>`},
{type: 'multi', items: [
{label: 'Fit to map', onClick: () => { map.fitMapBounds(); }},
{label: 'Reset rotation', onClick: () => { map.setBearing(0); }},
{label: 'Reset pitch', onClick: () => { map.setPitch(0); }},
]},
{label: 'Get coordinates at this position', onClick: () => {
let point = map.fromLngLat(event.lngLat);
map.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
}},
{type: 'seperator'},
{type: 'submenu', label: 'Map zoom', items: [
{label: 'Zoom in one level', onClick: () => { map.setZoom(map.getZoom() + 1)}},
{label: 'Zoom out one level', onClick: () => { map.setZoom(map.getZoom() - 1)}, enabled: map.getZoom() - 1 > 0},
{label: 'Zoom to minimum level', onClick: () => { map.setZoom(0)}},
{label: 'Fly to this position', onClick: () => {
map.flyTo({
center: event.lngLat,
pitch: 60,
zoom: 5
})
}}
]},
{type: 'hovermenu', label: 'Map settings', items: [
{
label: 'Double-click to zoom',
icon: map.doubleClickZoom.isEnabled() ? checkIcon : "",
onClick: () => {
if (map.doubleClickZoom.isEnabled()) {
map.doubleClickZoom.disable()
} else {
map.doubleClickZoom.enable()
}
}
},
{
label: 'Allow map rotation',
icon: map.dragRotate.isEnabled() ? checkIcon : "",
onClick: () => {
if (map.dragRotate.isEnabled()) {
map.dragRotate.disable();
} else {
map.dragRotate.enable();
}
}
},
{
label: 'Allow map pitch',
icon: map.getMaxPitch() != 0 ? checkIcon : "",
onClick: () => {
if (map.getMaxPitch() == 0) {
map.setMaxPitch(85)
} else {
map.setMaxPitch(0)
}
}
}
]}
]
});
})
Runs in a sandbox against the VJMAP public demo service
// --Map context menu--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
mapid: env.exampleMapId, // Map ID
mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometric data rendering
style: vjmap.openMapDarkStyle() // Use dark background style when div has dark background
})
if (res.error) {
// If open fails
message.error(res.error)
}
// Create geometric projection coordinate system based on map extent
let prj = new vjmap.GeoProjection(res.bounds);
// Map object
let map = new vjmap.Map({
container: 'map', // DIV container ID
style: svc.rasterStyle(), // Style, raster style here
center: prj.toLngLat(prj.getMapExtent().center()), // Set map center
zoom: 2, // Set zoom level
renderWorldCopies: false
});
// Associate service and projection objects
map.attach(svc, prj);
let mapBounds = map.getGeoBounds(); // Get map geographic extent
// Selected icon, or specify src path directly
const checkIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAANpJREFUOE9jZKAQMFKon2FgDKjZHVHf4rqiEeR6kl1QuzOyn+E/4/lmj2WLSDagekdkASMjg1aL+/I0WNgR7YLaHRG2/xgZYlrdV6QjBzzRBtTsijzzl4nVvd1l0VucBtTsjlz5/z/jt1a3ZYnIimp2RZ7//+9fQ6vHyo3o0Q53Qc32CAcGpv88DIxMxQyMDB9aXJcHghTX7IxcyMjAeLTZfdksbGkGqxdqdkUuZ2BgkPjPyDCFgeG/W6srqr+JCoPqXZG9jAwM9iyszy0aHA/8wZViiQ7EYWwAAPShQBF/1IlVAAAAAElFTkSuQmCC";
map.setMenu(event => {
return new vjmap.ContextMenu({
event: event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">Context menu demo</span>`},
{type: 'multi', items: [
{label: 'Fit to map', onClick: () => { map.fitMapBounds(); }},
{label: 'Reset rotation', onClick: () => { map.setBearing(0); }},
{label: 'Reset pitch', onClick: () => { map.setPitch(0); }},
]},
{label: 'Get coordinates at this position', onClick: () => {
let point = map.fromLngLat(event.lngLat);
map.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
}},
{type: 'seperator'},
{type: 'submenu', label: 'Map zoom', items: [
{label: 'Zoom in one level', onClick: () => { map.setZoom(map.getZoom() + 1)}},
{label: 'Zoom out one level', onClick: () => { map.setZoom(map.getZoom() - 1)}, enabled: map.getZoom() - 1 > 0},
{label: 'Zoom to minimum level', onClick: () => { map.setZoom(0)}},
{label: 'Fly to this position', onClick: () => {
map.flyTo({
center: event.lngLat,
pitch: 60,
zoom: 5
})
}}
]},
{type: 'hovermenu', label: 'Map settings', items: [
{
label: 'Double-click to zoom',
icon: map.doubleClickZoom.isEnabled() ? checkIcon : "",
onClick: () => {
if (map.doubleClickZoom.isEnabled()) {
map.doubleClickZoom.disable()
} else {
map.doubleClickZoom.enable()
}
}
},
{
label: 'Allow map rotation',
icon: map.dragRotate.isEnabled() ? checkIcon : "",
onClick: () => {
if (map.dragRotate.isEnabled()) {
map.dragRotate.disable();
} else {
map.dragRotate.enable();
}
}
},
{
label: 'Allow map pitch',
icon: map.getMaxPitch() != 0 ? checkIcon : "",
onClick: () => {
if (map.getMaxPitch() == 0) {
map.setMaxPitch(85)
} else {
map.setMaxPitch(0)
}
}
}
]}
]
});
})
map.logInfo('Right-click to open the menu');Overlay Layer Context Menu
// Show menu when clicking an overlay. The key can be customized. If the function returns null, the map context menu above is triggered. If the function returns non-null, this menu is shown
map.setMenu(event => {
let features = map.queryRenderedFeatures(event.point, {
layers: [polygon.layerId]
});
if (features.length == 0) {
// If the click position has no features from the polygon layer above, return null to trigger the default map menu
return null;
}
// If the click position has features from the polygon layer, show this layer's custom context menu
return new vjmap.ContextMenu({
event: event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the polygon layer context menu</span>`},
{
label: 'View selected polygon info',
onClick: () => {
map.logInfo(`Current polygon ID: ${features[0].properties.id}, Current polygon color: ${features[0].properties.color}`);
}
},
{
label: 'Delete',
onClick: () => {
let data = polygon.getData();
data.features = data.features.filter(f => f.id != features[0].properties.id);
polygon.setData(data);
}
}
]
});
}, "overlay")
Runs in a sandbox against the VJMAP public demo service
// --Overlay layer context menu--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
mapid: env.exampleMapId, // Map ID
mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometric data rendering
style: vjmap.openMapDarkStyle() // Use dark background style when div has dark background
})
if (res.error) {
// If open fails
message.error(res.error)
}
// Create geometric projection coordinate system based on map extent
let prj = new vjmap.GeoProjection(res.bounds);
// Map object
let map = new vjmap.Map({
container: 'map', // DIV container ID
style: svc.rasterStyle(), // Style, raster style here
center: prj.toLngLat(prj.getMapExtent().center()), // Set map center
zoom: 2, // Set zoom level
renderWorldCopies: false
});
// Associate service and projection objects
map.attach(svc, prj);
await map.onLoad();
// Add many polygons for testing. Right-click on a polygon to show polygon context menu, otherwise show map context menu
let mapBounds = map.getGeoBounds(0.6);
let len = mapBounds.width() / 50;
let polygons = []
for(let i = 0; i < 200; i++) {
const p1 = mapBounds.randomPoint();
const p2 = vjmap.geoPoint([p1.x, p1.y + len]);
const p3 = vjmap.geoPoint([p1.x + len, p1.y]);
polygons.push({
points: map.toLngLat([p1, p2, p3]),
properties: {
name: "polygon" + (i + 1),
color: vjmap.randomColor()
}
})
}
let polygon = new vjmap.Polygon({
data: polygons,
// When hover, use red; otherwise use 'color' from properties
fillColor: ['case', ['to-boolean', ['feature-state', 'hover']], 'red', ['get', 'color']],
fillOpacity: 0.8,
fillOutlineColor: "#f00",
isHoverPointer: true,
isHoverFeatureState: true
});
polygon.addTo(map);
polygon.clickLayer(e => map.logInfo(`You clicked polygon ${e.features[0].id}, name: ${e.features[0].properties.name}, color: ${e.features[0].properties.color}`))
// By default the map menu is shown because the key here is empty. When setMenu is called multiple times, order is by key descending; empty key is last
map.setMenu(event => {
return new vjmap.ContextMenu({
event: event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the map context menu</span>`},
{
label: 'Fit to map extent',
onClick: () => {
map.setBearing(0);
map.setPitch(0);
map.fitMapBounds();
}
},
{
label: 'Get coordinates at this position',
onClick: () => {
let point = map.fromLngLat(event.lngLat);
map.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
}
}
]
});
})
// Show menu when clicking an overlay. The key can be customized. If the function returns null, the map context menu above is triggered. If the function returns non-null, this menu is shown
map.setMenu(event => {
let features = map.queryRenderedFeatures(event.point, {
layers: [polygon.layerId]
});
if (features.length == 0) {
// If the click position has no features from the polygon layer above, return null to trigger the default map menu
return null;
}
// If the click position has features from the polygon layer, show this layer's custom context menu
return new vjmap.ContextMenu({
event: event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the polygon layer context menu</span>`},
{
label: 'View selected polygon info',
onClick: () => {
map.logInfo(`Current polygon ID: ${features[0].properties.id}, Current polygon color: ${features[0].properties.color}`);
}
},
{
label: 'Delete',
onClick: () => {
let data = polygon.getData();
data.features = data.features.filter(f => f.id != features[0].properties.id);
polygon.setData(data);
}
}
]
});
}, "overlay")
// Draw control context menu
const draw = new vjmap.Draw.Tool();
map.addControl(draw, 'top-right');
map.on("draw.simpleselect.contextmenu", function (e) {
// If not an event from this draw object, ignore
if (e.styleId != draw.options.styleId) return;
let feature = e.event.featureTarget
if (!feature) return
//if (draw.getMode() != "simple_select") return
let selectFeatureId = feature.properties.id
// Must be selected to right-click
let selectIds = draw.getSelectedIds() || []
if (selectIds.indexOf(selectFeatureId)== -1) return
let event = e.event.originalEvent
// Prevent default map context menu
event.preventDefault();
event.stopPropagation();
return new vjmap.ContextMenu({
event: event,
theme: "dark", //light
width: "250px",
items: [
{
label: "Delete",
onClick: () => {
draw.delete(selectFeatureId)
}
}
]
});
});Marker Context Menu
// Unlike polygon and other overlay layers, marker is a div while overlay layers are canvas-rendered and share the same div as the map
// So marker right-click must be triggered by its own div's context menu
marker.getElement().addEventListener("mousedown", event => {
if (event.button != 2) return;// Not right-click
// Prevent default map context menu
event.preventDefault();
event.stopPropagation();
return new vjmap.ContextMenu({
event: event,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the Marker context menu</span>`},
{
label: 'Get this Marker info',
onClick: () => {
map.logInfo(`Current Marker ID: ${marker.id}`);
}
}
]
});
})
Runs in a sandbox against the VJMAP public demo service
// --Marker context menu--
// Map service object
let svc = new vjmap.Service(env.serviceUrl, env.accessToken)
// Open map
let res = await svc.openMap({
mapid: env.exampleMapId, // Map ID
mapopenway: vjmap.MapOpenWay.GeomRender, // Open with geometric data rendering
style: vjmap.openMapDarkStyle() // Use dark background style when div has dark background
})
if (res.error) {
// If open fails
message.error(res.error)
}
// Create geometric projection coordinate system based on map extent
let prj = new vjmap.GeoProjection(res.bounds);
// Map object
let map = new vjmap.Map({
container: 'map', // DIV container ID
style: svc.rasterStyle(), // Style, raster style here
center: prj.toLngLat(prj.getMapExtent().center()), // Set map center
zoom: 2, // Set zoom level
renderWorldCopies: false
});
// Associate service and projection objects
map.attach(svc, prj);
await map.onLoad();
map.setMenu(event => {
return new vjmap.ContextMenu({
event: event.originalEvent,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the map context menu</span>`},
{
label: 'Fit to map extent',
onClick: () => {
map.setBearing(0);
map.setPitch(0);
map.fitMapBounds();
}
},
{
label: 'Get coordinates at this position',
onClick: () => {
let point = map.fromLngLat(event.lngLat);
app.logInfo(`Current coordinates: x: ${point.x}, y: ${point.y}`);
}
}
]
});
})
let mapBounds = map.getGeoBounds(); // Get map geographic extent
for(let i = 0; i < 50; i++) {
let point = mapBounds.randomPoint(); // Random point within geographic extent
// marker
let marker = new vjmap.Marker({
color: vjmap.randomColor()
});
marker.setLngLat(map.toLngLat(point))
marker.addTo(map);
marker.id = i + 1;
// Unlike polygon and other overlay layers, marker is a div while overlay layers are canvas-rendered and share the same div as the map
// So marker right-click must be triggered by its own div's context menu
marker.getElement().addEventListener("mousedown", event => {
if (event.button != 2) return;// Not right-click
// Prevent default map context menu
event.preventDefault();
event.stopPropagation();
return new vjmap.ContextMenu({
event: event,
theme: "dark", //light
width: "250px",
items: [
{type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">I am the Marker context menu</span>`},
{
label: 'Get this Marker info',
onClick: () => {
map.logInfo(`Current Marker ID: ${marker.id}`);
}
}
]
});
})
}Type Definitions
/**
* Context menu.
*
**/
export class ContextMenu {
private options;
private menuControl;
private position;
/**
* Creates a new ContextMenu menu
* @param {object} opts options which build the menu e.g. position and items
* @param {number} opts.width sets the width of the menu including children
* @param {object} opts.event Event object
* @param {theme} opts.theme Custom theme, supports dark and light, default dark
* @param {boolean} opts.isSticky sets how the menu appears, follow the mouse or sticky
* @param {Array<ContextMenuItem>} opts.items sets the default items in the menu
*/
constructor(opts: ContextMenuOptions);
/**
* Adds item to this ContextMenu menu instance
* @param {ContextMenuItem} item item to add to the ContextMenu menu
*/
add(item: any): void;
/**
* Makes this ContextMenu menu visible
*/
show(): void;
/**
* Hides this ContextMenu menu
*/
hide(): void;
/**
* Toggle visibility of menu
*/
toggle(): void;
}
/**
* Context menu options.
*
**/
export interface ContextMenuOptions {
/** Event object. */
event: Event;
/** Menu width (including submenus), pixels, default 150px. */
width?: string;
/** Menu theme. (dark and light, default dark)*/
theme?: string;
/** Inner width of the menu outer container, used to auto-adjust position when menu exceeds bounds; default is window width. Can be set via map.getContainer().getBoundingClientRect().width */
innerWidth?: number;
/** Inner height of the menu outer container, used to auto-adjust position when menu exceeds bounds; default is window height. Can be set via map.getContainer().getBoundingClientRect().height */
innerHeight?: number;
/** Menu items */
items: ContextMenuSubItemOptions[];
}
/**
* Context menu sub-item options.
*
**/
export interface ContextMenuSubItemOptions {
/** Submenu type. */
type?: "custom" | "multi" | "Button" | "seperator" | "submenu" | "hovermenu" | "normal";
/** Custom HTML content when type is 'custom' */
markup?: string;
/** Submenu items when type is 'multi'*/
items?: ContextMenuSubItemOptions[];
/** Click handler*/
onClick?: Function;
/** Menu label*/
label?: string;
/** Shortcut key*/
shortcut?: string;
/** Whether enabled*/
enabled?: boolean;
/** CSS icon*/
cssIcon?: string;
/** Icon*/
icon?: string;
}
Was this page helpful?Thanks for the feedback!