唯杰地图 logo唯杰地图
VJMAP
EN

context-menu

vjmap/references/interaction/context-menu.md 6.1 KB165 行
原文

本文回答:vjmap.ContextMenu 右键菜单用法 —— 地图 / 覆盖物 / Marker / Draw。

一、ContextMenu 基础

new vjmap.ContextMenu({
  event: mouseEvent.originalEvent,  // 鼠标事件对象
  theme: "dark",                     // "dark" | "light"
  width: "250px",
  items: [...]
});

二、菜单项类型

type 说明
normal(省略 type) 普通菜单项
seperator 分割线
custom 自定义 HTML markup
multi 横向多按钮("不倾斜/不旋转/全图")
submenu 子菜单(点击弹出)
hovermenu 悬浮子菜单
Button 按钮型子项(d.ts 联合成员,用法同 normal)

三、地图右键菜单

map.setMenu(event => {
  return new vjmap.ContextMenu({
    event: event.originalEvent,
    theme: "dark",
    width: "250px",
    items: [
      { type: "custom", markup: "<span>菜单右键功能演示</span>" },
      { type: "multi", items: [
        { label: "全图", onClick: () => map.fitMapBounds() },
        { label: "不旋转", onClick: () => map.setBearing(0) },
        { label: "不倾斜", onClick: () => map.setPitch(0) }
      ]},
      { label: "获取此位置坐标", onClick: () => {
        const p = map.fromLngLat(event.lngLat);
        console.log(p.x, p.y);
      }},
      { type: "seperator" },
      { type: "submenu", label: "地图缩放", items: [
        { label: "放大一级", onClick: () => map.setZoom(map.getZoom() + 1) },
        { label: "缩小一级", onClick: () => map.setZoom(map.getZoom() - 1) },
        { label: "飞至此位置", onClick: () =>
          map.flyTo({ center: event.lngLat, pitch: 60, zoom: 5 })
        }
      ]}
    ]
  });
});

四、覆盖物图层右键菜单

// 先给 polygon 默认菜单
map.setMenu(event => { /* 地图菜单 */ });

// 再给覆盖物菜单(key = "overlay")
map.setMenu(event => {
  const features = map.queryRenderedFeatures(event.point, {
    layers: [polygon.layerId]
  });
  if (features.length === 0) return null;   // 返回 null → 继续弹地图菜单
  return new vjmap.ContextMenu({
    event: event.originalEvent,
    items: [
      { label: "查看信息", onClick: () => console.log(features[0].properties) },
      { label: "删除", onClick: () => {
        const d = polygon.getData();
        d.features = d.features.filter(f => f.id != features[0].properties.id);
        polygon.setData(d);
      }}
    ]
  });
}, "overlay");    // key 可以自定义,多个 setMenu 按 key 倒序匹配

setMenu(fn, key):同一 map 可以挂多个;按 key 倒序匹配,空 key 排最后。回调返回 null 时继续下一级。

语义(map.ts):

  • 回调不在 contextmenu DOM 事件上触发,而是在 mousedown + mouseup 且 originalEvent.button === 2(右键单击序列,isClick 判定位移/时长)时依次调用——合成 contextmenu 事件无效;打开前 fire contextMenuPreOpen,成功打开后 fire contextMenuOpened。
  • setMenu() / setMenu(null)(不传 key)会摘掉内部的 mousedown/mouseup 监听,之后右键不再弹任何菜单;但注册表不清空(源码 if (menu) 跳过赋值)——下一次 setMenu(fn, key) 重新挂监听时,之前所有 key 的回调全部复活。只移除某个 key 用 map.setMenu(() => null, key)(覆盖为恒返回 null 的回调)。
  • map.isInteracting() 为 true 时回调不触发,除非设内部开关 _menu_isInteracting_open = true。

五、Marker 右键菜单

Marker 是 DOM,不走地图 setMenu;自己监听:

marker.getElement().addEventListener("mousedown", event => {
  if (event.button !== 2) return;
  event.preventDefault();
  event.stopPropagation();
  new vjmap.ContextMenu({
    event,
    theme: "dark",
    items: [
      { type: "custom", markup: `<span>Marker 菜单</span>` },
      { label: "查看信息", onClick: () => console.log(marker.getLngLat()) }
    ]
  });
});

六、Draw 工具右键菜单

const draw = new vjmap.Draw.Tool();
map.addControl(draw, "top-right");

// simple_select 模式选中实体时
map.on("draw.simpleselect.contextmenu", e => {
  if (e.styleId !== draw.options.styleId) return;
  const feature = e.event.featureTarget;
  if (!feature) return;
  const selectIds = draw.getSelectedIds() || [];
  if (!selectIds.includes(feature.properties.id)) return;
  e.event.originalEvent.preventDefault();
  e.event.originalEvent.stopPropagation();

  new vjmap.ContextMenu({
    event: e.event.originalEvent,
    items: [
      { label: "删除", onClick: () => draw.delete(feature.properties.id) }
    ]
  });
});

类似还有 draw.directselect.contextmenu(夹点编辑模式)——其载荷只有 {mode, state, event}、没有 styleId,过滤实例用 e.state.options.styleId !== draw.options.styleId。

七、菜单项属性

{
  type?: "custom" | "multi" | "Button" | "seperator" | "submenu" | "hovermenu" | "normal";
  markup?: string;       // type=custom 的 HTML
  items?: [...];         // type=multi/submenu/hovermenu 的子项
  label?: string;
  shortcut?: string;     // 快捷键提示
  enabled?: boolean;
  cssIcon?: string;      // css 类名图标
  icon?: string;         // 图标 URL / base64
  onClick?: () => void;  // 调用时不传任何参数;需要的上下文用闭包捕获(如 setMenu 回调里的 event)
}

八、常见坑

  1. 用了 event.originalEvent:map.setMenu 回调里拿到的是 vjmap 封装事件,弹菜单要传 originalEvent;draw.*.contextmenu 回调里拿到的是 e.event.originalEvent。
  2. 菜单溢出屏幕 → 设置 innerWidth / innerHeight 为容器大小:
    const { width, height } = map.getContainer().getBoundingClientRect();
    new vjmap.ContextMenu({ event, innerWidth: width, innerHeight: height, ... });
    
  3. Marker 菜单不弹出 → 忘了 preventDefault(),浏览器原生菜单先弹。
  4. 多个 setMenu 互相覆盖 → 传不同的 key。