# 上下文菜单
 
[ContextMenu](https://vjmap.com/map3d/api/classes/ContextMenu.html)上下文菜单用于右键弹出菜单界面显示。

## 常用方法

|方法|描述|
|:---|:---|
|[add](https://vjmap.com/map3d/api/classes/ContextMenu.html#add)|增加菜单项|
|[show](https://vjmap.com/map3d/api/classes/ContextMenu.html#show)|显示菜单 |
|[hide](https://vjmap.com/map3d/api/classes/ContextMenu.html#hide)|隐藏菜单|
|[toggle](https://vjmap.com/map3d/api/classes/ContextMenu.html#toggle)|切换菜单状态|

## 菜单项类型
|类型|描述|
|:---|:---|
|seperator|分隔线|
|custom|自定义菜单项|
|multi|多个按钮|
|submenu|子菜单项|
|hovermenu|悬浮菜单项|
|normal|缺省正常菜单项|

## 常见坑位

1. **`opts.items` 是必填**：构造器直接 `opts.items.forEach` 无判空，`new ContextMenu({width:...})` 不传 items 必抛 `forEach of undefined`。
2. **构造即显示**：构造器内部立即 `document.body.appendChild(menu)` 并定位——**不需要调 `show()`**；要"先建好稍后显示"只能 `hide()` 收起。
3. **`add(item)` 对外基本不可用**：它期望 `ContextMenuItem` 实例（读 `.element`），而该类**未导出**——传 `{type:'normal',...}` options 对象会崩。菜单项只能在构造 `items` 里一次性给全。
4. **覆盖 `document.onclick`/`onmousedown`**：构造器用赋值（非 addEventListener）挂全局点击关闭，会**清掉宿主先前绑在 document 上的 onclick**。
5. `opts.isSticky` 在构造器里被读，但 `ContextMenuOptions` 接口没声明——粘附定位传 `position:[x,y]` 更可靠。
6. **`opts.position` 只在构造期生效**：`ContextMenuOptions` 虽声明了 `position?: [number, number]`，但 `show()`/`toggle()` 内部调 `PositionMenu` 时**不传 position**——只传 `position` 不传 `event` 时，构造能定位显示，但随后的 `show()` 会因读 `event.clientX` 抛 `Cannot read properties of undefined (reading 'clientX')`。要可重复 show/toggle，必须传 `opts.event`（带 `clientX/clientY` 的对象即可，如 `{clientX, clientY, stopPropagation(){}}`）。

## 示例

下面示例演示了如何给应用、场景中的实体对象、Marker增加菜单 

```js
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let box = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({
    color: '#0ff'
});
let mesh = new THREE.Mesh(box, material);
mesh.position.set(0, 0, 0)
let meshEnt = vjmap3d.Entity.fromObject3d(mesh);
meshEnt.pointerEvents = true; // 实体允许事件，这样在此实体上面弹右键菜单才可以响应
meshEnt.addTo(app);
//
let marker2d = new vjmap3d.Marker2D({
    color: vjmap3d.randHtmlColor()
});
marker2d.setPosition([2, 0, 1]);
marker2d.addTo(app);
//
let isAllowShowMenu = true
app.setMenu((event, object) => {
    if (!isAllowShowMenu) return;
    if (object) {
        // 如果点击在一个实体对象上面时
        return new vjmap3d.ContextMenu({
            event: event.originalEvent,
            theme: "dark", //light
            width: "250px",
            items: [
                {type: 'custom', markup: `<span style="color: #00ffff; padding-left: 30px">实体右键菜单</span>`},
                {
                    label: '编辑',
                    onClick: () => {
                        app.transformObject({
                            target: object.target.node,
                            clickNoSelfEntityExit: true,
                            clickNoEntityExit: true,
                        });
                    }
                },
                {
                    label: '删除',
                    onClick: () => {
                        object.target.remove()
                    }
                },
            ]
        });
    } else {
        // 如果没有点击到东西时,则弹出应用本身的菜单 
        return new vjmap3d.ContextMenu({
            event: event.originalEvent,
            theme: "dark", //light
            width: "250px",
            items: [
                {type: 'custom', markup: `<span style="color: #ffff00; padding-left: 30px">应用右键菜单</span>`},
                {type: 'multi', items: [
                    {label: '放大', onClick: () => { app.cameraControl.dolly(3); }},
                    {label: '缩小', onClick: () => { app.cameraControl.dolly(-3); }},
                ]},
            ]
        });
    }
})
// marker属于div，右键菜单事件需要单独增加响应
marker2d.getElement().addEventListener("contextmenu", event => {
    // @ts-ignore
    if (event.button != 2) return;//不是右键
    // 阻止地图默认右键菜单触发
    event.preventDefault();
    event.stopPropagation();
    new vjmap3d.ContextMenu({
        event: event,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `<span style="color: #ff00ff; padding-left: 30px">标注Marker的右键菜单</span>`},
            {
                label: '允许拖动',
                onClick: () => {
                    marker2d.setDraggable(true)
                }
            },
            {
                label: '不允许拖动',
                onClick: () => {
                    marker2d.setDraggable(false)
                }
            }
        ]
    });
})
app.logInfo("请在相应的位置点击右键弹出右键菜单")
```

<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    stat: { show: true, left: "0" },
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
let box = new THREE.BoxGeometry(2, 2, 2)
const material = new THREE.MeshStandardMaterial({
    color: '#0ff'
});
let mesh = new THREE.Mesh(box, material);
mesh.position.set(0, 0, 0)
let meshEnt = vjmap3d.Entity.fromObject3d(mesh);
meshEnt.pointerEvents = true; // 实体允许事件，这样在此实体上面弹右键菜单才可以响应
meshEnt.addTo(app);
//
let marker2d = new vjmap3d.Marker2D({
    color: vjmap3d.randHtmlColor()
});
marker2d.setPosition([2, 0, 1]);
marker2d.addTo(app);
//
let isAllowShowMenu = true
app.setMenu((event, object) => {
    if (!isAllowShowMenu) return;
    if (object) {
        // 如果点击在一个实体对象上面时
        return new vjmap3d.ContextMenu({
            event: event.originalEvent,
            theme: "dark", //light
            width: "250px",
            items: [
                {type: 'custom', markup: `&lt;span style="color: #00ffff; padding-left: 30px">实体右键菜单&lt;/span>`},
                {
                    label: '编辑',
                    onClick: () => {
                        app.transformObject({
                            target: object.target.node,
                            clickNoSelfEntityExit: true,
                            clickNoEntityExit: true,
                        });
                    }
                },
                {
                    label: '删除',
                    onClick: () => {
                        object.target.remove()
                    }
                },
            ]
        });
    } else {
        // 如果没有点击到东西时， 则弹出应用本身的菜单
        return new vjmap3d.ContextMenu({
            event: event.originalEvent,
            theme: "dark", //light
            width: "250px",
            items: [
                {type: 'custom', markup: `&lt;span style="color: #ffff00; padding-left: 30px">应用右键菜单&lt;/span>`},
                {type: 'multi', items: [
                    {label: '放大', onClick: () => { app.cameraControl.dolly(3); }},
                    {label: '缩小', onClick: () => { app.cameraControl.dolly(-3); }},
                ]},
            ]
        });
    }
})
// marker属于div，右键菜单事件需要单独增加响应
marker2d.getElement().addEventListener("contextmenu", event => {
    // @ts-ignore
    if (event.button != 2) return;//不是右键
    // 阻止地图默认右键菜单触发
    event.preventDefault();
    event.stopPropagation();
    new vjmap3d.ContextMenu({
        event: event,
        theme: "dark", //light
        width: "250px",
        items: [
            {type: 'custom', markup: `&lt;span style="color: #ff00ff; padding-left: 30px">标注Marker的右键菜单&lt;/span>`},
            {
                label: '允许拖动',
                onClick: () => {
                    marker2d.setDraggable(true)
                }
            },
            {
                label: '不允许拖动',
                onClick: () => {
                    marker2d.setDraggable(false)
                }
            }
        ]
    });
})
app.logInfo("请在相应的位置点击右键弹出右键菜单")
</pre>
</mapFrame>

## 按钮弹出菜单 
 ```js
 const resetSettings = '<svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%;" fill="currentColor" viewBox="0 0 24 24">\n' +
    ' <path fill="currentColor" d="M19 8L15 12H18C18 15.31 15.31 18 12 18C11 18 10.03 17.75 9.2 17.3L7.74 18.76C8.97 19.54 10.43 20 12 20C16.42 20 20 16.42 20 12H23M6 12C6 8.69 8.69 6 12 6C13 6 13.97 6.25 14.8 6.7L16.26 5.24C15.03 4.46 13.57 4 12 4C7.58 4 4 7.58 4 12H1L5 16L9 12M14 12C14 13.11 13.11 14 12 14S10 13.11 10 12 10.9 10 12 10 14 10.9 14 12Z" />\n' +
    '</svg>'
const trash = '<svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">\n' +
    ' <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />\n' +
    '</svg>'
let control = new vjmap3d.ButtonGroupControl({
    buttons: [
        {
            id: 'reset-settings',
            icon: resetSettings,
            title: 'Reset All Settings',
        //    round: true,
            onclick: async(e, element) => {
                let rect = element.getBoundingClientRect()
                let position = [rect.left - 6, rect.bottom + 8]
                
                new vjmap3d.ContextMenu({
                    position,
                    theme: "dark", //light
                    width: "150px",
                    items: [
                        {
                            label: 'Menu1',
                            onClick: () => {
                                app.logInfo("Menu1", "success");
                            }
                        },
                        {
                            label: 'Menu2',
                            onClick: () => {
                                app.logInfo("Menu2", "success");
                            }
                        }
                    ]
                });
            },
        },
        {
            id: 'clear-scene',
            icon: trash,
            title: 'Clear Scene',
            // checked: true,
            // toggle: true,
            //round: true,
            onclick: async(e, element) => {
                let rect = element.getBoundingClientRect()
                let position = [rect.left - 6, rect.bottom + 8]
                
                new vjmap3d.ContextMenu({
                    event: e,
                    position,
                    theme: "dark", //light
                    width: "150px",
                    items: [
                        {
                            label: '菜单一',
                            onClick: () => {
                                app.logInfo("菜单一", "success");
                            }
                        },
                        {
                            label: '菜单二',
                            onClick: () => {
                                app.logInfo("菜单二", "success");
                            }
                        }
                    ]
                });
            },
        },
    ],
    direction: "row",
    barSelectMode: true,
    barCurSelectId: "reset-settings"
    //  lightTheme: true
});
app.addControl(control, "top-left")
 ```
            
<mapFrame>
<pre>
let svc = new vjmap3d.Service(env.serviceUrl, env.accessToken);
let app = new vjmap3d.App(svc, {
    container: "map", // 容器id
    scene: {
        gridHelper: { visible: true } // 是否显示坐标网格
    },
    camera: {  // 相机设置
        viewHelper: { enable: true, position: "leftBottom" } // 是否显示视角指示器
    }
})
const resetSettings = '&lt;svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%;" fill="currentColor" viewBox="0 0 24 24">\n' +
    ' &lt;path fill="currentColor" d="M19 8L15 12H18C18 15.31 15.31 18 12 18C11 18 10.03 17.75 9.2 17.3L7.74 18.76C8.97 19.54 10.43 20 12 20C16.42 20 20 16.42 20 12H23M6 12C6 8.69 8.69 6 12 6C13 6 13.97 6.25 14.8 6.7L16.26 5.24C15.03 4.46 13.57 4 12 4C7.58 4 4 7.58 4 12H1L5 16L9 12M14 12C14 13.11 13.11 14 12 14S10 13.11 10 12 10.9 10 12 10 14 10.9 14 12Z" />\n' +
    '&lt;/svg>'
const trash = '&lt;svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%;" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">\n' +
    ' &lt;path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />\n' +
    '&lt;/svg>'
let control = new vjmap3d.ButtonGroupControl({
    buttons: [
        {
            id: 'reset-settings',
            icon: resetSettings,
            title: 'Reset All Settings',
        //    round: true,
            onclick: async(e, element) => {
                let rect = element.getBoundingClientRect()
                let position = [rect.left - 6, rect.bottom + 8]
                
                new vjmap3d.ContextMenu({
                    position,
                    theme: "dark", //light
                    width: "150px",
                    items: [
                        {
                            label: 'Menu1',
                            onClick: () => {
                                app.logInfo("Menu1", "success");
                            }
                        },
                        {
                            label: 'Menu2',
                            onClick: () => {
                                app.logInfo("Menu2", "success");
                            }
                        }
                    ]
                });
            },
        },
        {
            id: 'clear-scene',
            icon: trash,
            title: 'Clear Scene',
            // checked: true,
            // toggle: true,
            //round: true,
            onclick: async(e, element) => {
                let rect = element.getBoundingClientRect()
                let position = [rect.left - 6, rect.bottom + 8]
                
                new vjmap3d.ContextMenu({
                    event: e,
                    position,
                    theme: "dark", //light
                    width: "150px",
                    items: [
                        {
                            label: '菜单一',
                            onClick: () => {
                                app.logInfo("菜单一", "success");
                            }
                        },
                        {
                            label: '菜单二',
                            onClick: () => {
                                app.logInfo("菜单二", "success");
                            }
                        }
                    ]
                });
            },
        },
    ],
    direction: "row",
    barSelectMode: true,
    barCurSelectId: "reset-settings"
    //  lightTheme: true
});
app.addControl(control, "top-left")
</pre>
</mapFrame>