Menus
List UI and identify nodes expose actions in five places. Build items with createMenuBuilder(). Click handlers receive { layer, mapId, value, event, meta, context } — not (layer, mapId).
location | Where it renders |
|---|---|
extra | Icon buttons on the layer title row |
title | Panel header after-title / afterTitle slot (immediately after the title text — not trailing extra-btn chrome) |
prebottom | Left of the bottom row (opacity lives here by default) |
bottom | Right of the bottom row |
menu | Context menu (⋮) on the same row |
UI adapters render these via DatasetMenus (@hungpvq/vue-map-dataset / @hungpvq/react-map-dataset): pass menus, data, optional value / mapId, and locations (e.g. ['extra','menu'] for a row, ['title'] for panel chrome). The component partitions with partitionMenuActions and runs handleMenuAction on click.
Per-control placement (byControl)
Hosts inject context.control (MENU_CONTROL_ID.layerControl | layerDetail | identify | attributeTable). Define once, override per host. Full tree example (menu part + list + identify): see with-helper-data.md — Menu / byControl.
import {
MENU_CONTROL_ID,
createMenuBuilder,
createDatasetPartMenuComponentBuilder,
createMenuItemToBoundActionForItem,
createMenuItemShowDetailForItem,
LIST_VIEW_MENU_ID,
} from '@hungpvq/map-dataset/menu';
import { mdiCrosshairsGps } from '@mdi/js';
// 1) On the menu (preferred for custom items)
const flyTo = createMenuBuilder()
.item()
.setId('fly-to')
.setLocation('extra') // LayerControl / Identify / AttributeTable
.setByControl({
[MENU_CONTROL_ID.layerDetail]: { location: 'title' }, // after-title on detail popup
})
.setName('Fly to')
.setIcon(mdiCrosshairsGps)
.setClick(/* ... */)
.build();
const showDetail = createMenuBuilder()
.item()
.setId('show-detail')
.setLocation('menu')
.setByControl({
[MENU_CONTROL_ID.layerDetail]: { hidden: true }, // already on the detail panel
})
.setName('Detail')
.setClick(/* ... */)
.build();
// 2) On the menu-part entry (merged onto menu.byControl)
createDatasetPartMenuComponentBuilder('defaults')
.addItemMenu(createMenuItemToBoundActionForItem(), LIST_VIEW_MENU_ID.item.flyTo)
.addItemMenu(
createMenuItemShowDetailForItem([{ text: 'Name', value: 'name' }]),
LIST_VIEW_MENU_ID.item.showDetail,
)
.addMenu({
for: 'item',
key: 'fly-to-custom',
menu: flyTo,
byControl: { [MENU_CONTROL_ID.layerDetail]: { location: 'title' } },
})
.build();Built-ins: Fly to / Fill bound default extra (title on LayerDetail); Detail / Info hidden on LayerDetail.
Shared defaults (all lists, identify, attribute table) live on a menu dataset part in the same tree (findPartByType). List rows merge for: 'layer'; identify and the table merge for: 'item'.
End-to-end: conditions from app state
Define menus once on the dataset. Evaluate hidden / disabled at render with menuContext (Pinia, React state, …).
1. Menus on the list
import { createRootDataset, createDatasetPartListViewUiComponentBuilder } from '@hungpvq/map-dataset';
import { createMenuBuilder } from '@hungpvq/map-dataset/menu';
import { mdiPen, mdiStar } from '@mdi/js';
function createLayerWithMenus() {
const dataset = createRootDataset('Menu conditions');
const list = createDatasetPartListViewUiComponentBuilder('My layer')
.addMenus([
createMenuBuilder()
.item()
.setId('admin-only')
.setLocation('menu')
.setName('Admin only')
.setIcon(mdiStar)
.setHidden(({ context }) => context?.role !== 'admin')
.setClick(({ layer }) => {
console.info('admin action', layer.getName());
})
.build(),
createMenuBuilder()
.item()
.setId('pen')
.setLocation('extra')
.setName('Pen')
.setIcon(mdiPen)
.setDisabled(({ context }) => !context?.canUsePen)
.setClick(({ layer, mapId }) => {
console.info('pen', layer.id, mapId);
})
.build(),
])
.build();
dataset.add(list);
return dataset;
}setHidden / setDisabled accept boolean or (ctx) => boolean.
type MenuConditionContext = {
layer: IDataset;
mapId?: string;
context?: Record<string, any>; // menuContext + list flags
};2. Vue — pass Pinia / reactive state
<script setup lang="ts">
import { reactive } from 'vue';
import { Map } from '@hungpvq/vue-map-core';
import {
LayerControl,
useMapDataset,
} from '@hungpvq/vue-map-dataset';
import type { MapSimple } from '@hungpvq/map-core';
const menuUi = reactive({
role: 'admin' as 'admin' | 'viewer',
canUsePen: true,
});
function onMapLoaded(map: MapSimple) {
useMapDataset(map.id).addDataset(createLayerWithMenus());
}
</script>
<template>
<Map @map-loaded="onMapLoaded">
<LayerControl position="top-left" show :menu-context="menuUi">
<template #titleList>
<label>
<input
type="checkbox"
:checked="menuUi.role === 'admin'"
@change="
menuUi.role = ($event.target as HTMLInputElement).checked
? 'admin'
: 'viewer'
"
/>
admin
</label>
<label>
<input type="checkbox" v-model="menuUi.canUsePen" />
pen
</label>
</template>
</LayerControl>
</Map>
</template>Getter so nested store fields stay live:
<LayerControl :menu-context="() => ({ role: user.role, canUsePen: user.canUsePen })" />Pinia from any ancestor (no prop):
import { provideMenuConditionContext } from '@hungpvq/vue-map-dataset';
import { useUserStore } from './stores/user';
provideMenuConditionContext(() => {
const user = useUserStore();
return { role: user.role, canUsePen: user.canUsePen };
});3. React — pass hook state
import { useState } from 'react';
import { Map } from '@hungpvq/react-map-core';
import {
LayerControl,
useMapDataset,
} from '@hungpvq/react-map-dataset';
import type { MapSimple } from '@hungpvq/map-core';
function Page() {
const [menuUi, setMenuUi] = useState({
role: 'admin' as 'admin' | 'viewer',
canUsePen: true,
});
function onMapLoaded(map: MapSimple) {
useMapDataset(map.id).addDataset(createLayerWithMenus());
}
return (
<Map onMapLoaded={onMapLoaded}>
<LayerControl
position="top-left"
show
menuContext={menuUi}
titleList={
<>
<label>
<input
type="checkbox"
checked={menuUi.role === 'admin'}
onChange={(e) =>
setMenuUi((s) => ({
...s,
role: e.target.checked ? 'admin' : 'viewer',
}))
}
/>
admin
</label>
<label>
<input
type="checkbox"
checked={menuUi.canUsePen}
onChange={(e) =>
setMenuUi((s) => ({ ...s, canUsePen: e.target.checked }))
}
/>
pen
</label>
</>
}
/>
</Map>
);
}Or wrap (merges with parent):
import { MenuConditionProvider } from '@hungpvq/react-map-dataset';
<MenuConditionProvider value={() => ({ role: user.role })}>
<LayerControl />
</MenuConditionProvider>List UI also injects readonly, disabledMove, disabledCreateGroup. Do not reuse those keys unless you want to override them.
createMenuBuilder()
import { createMenuBuilder } from '@hungpvq/map-dataset/menu';
const item = createMenuBuilder().item() /* .set… */ .build();
const divider = createMenuBuilder().divider().setLocation('menu').build();.item()
| Method | Signature | Role |
|---|---|---|
setId | (id: string) | Needed for updateMenu / removeMenu |
setName | (name: string) | Label |
setIcon | (mdiPath: string) | Icon |
setLocation | 'extra' | 'title' | 'bottom' | 'prebottom' | 'menu' | Placement (title → panel header after-title) |
setClick | fn | string | createMenuClickBuilder() | Action |
setHidden | boolean | (ctx) => boolean | Skip render when true |
setDisabled | boolean | (ctx) => boolean | Visible, not clickable |
setComponentKey | (key: string) | Custom button (extra / bottom / prebottom) |
setComponentMenuKey | (key: string) | Custom context-menu row (location: 'menu') |
setAdditional | (obj) | Extra fields, e.g. { order: 10 } |
build | () | MenuAction |
.divider()
setLocation, setHidden, setDisabled, build.
list.addMenus([
createMenuBuilder()
.item()
.setLocation('menu')
.setName('Export')
.setIcon(mdiDownload)
.setClick(({ layer }) => exportLayer(layer))
.build(),
createMenuBuilder().divider().setLocation('menu').build(),
createMenuBuilder()
.item()
.setLocation('menu')
.setName('Delete extra')
.setHidden(true) // never shown
.setClick(() => {})
.build(),
]);Chain on the list builder before .build():
createDatasetPartListViewUiComponentBuilder('Layer')
.addMenu(item)
.addMenus([a, b])
.build();createMenuClickBuilder()
Compose one or more actions for setClick.
import { createMenuClickBuilder } from '@hungpvq/map-dataset/menu';
createMenuBuilder()
.item()
.setLocation('extra')
.setName('Fly')
.setClick(
createMenuClickBuilder()
.addCommand(({ layer, mapId }) => {
console.info(layer.id, mapId);
})
.addTupleStatic('fitBounds', {
value: [105.8, 21.0, 105.9, 21.1],
})
.build(),
)
.build();| Method | Use |
|---|---|
addCommand(name) | Registry handler: UniversalRegistry.registerMenuHandler(name, fn) |
addCommand(fn) | ({ layer, mapId, value, event, meta, context }) => void |
addCommand({ execute }) | Object handler |
addCommands([...]) | Several of the above |
addTupleStatic(key, props) | Dispatch key with extra props |
addTupleDynamic(key, (props) => partial) | Compute props then dispatch |
build() | One action, or an array if several |
Built-in click-handler keys: LIST_VIEW_MENU_ID.addComponent, .fitBounds, .highlight. Menu ids are nested under LIST_VIEW_MENU_ID.layer (list row) and LIST_VIEW_MENU_ID.item (identify / attribute-table row).
createMenuClickBuilder()
.addTupleDynamic('addComponent', ({ layer }) => ({
value: createMenuClickAddComponentBuilder()
.setComponentKey('style-control')
.setAttr({ item: layer })
.build(),
}))
.build();createMenuClickHighlightBuilder().setDetail(feature).setKey('identify').build()createMenuClickAddComponentBuilder().setComponentKey(key).setAttr({}).setCheck('detail').build()
Built-in item factories
All return a MenuAction. Most accept an optional last argument that overlays fields (order, name, hidden, …) via setAdditional.
import { createMenuItemToggleShow, createMenuItemSetOpacity, createMenuItemStyleEdit, createMenuItemShowDetailInfoSource, createMenuItemToBoundActionForList, createMenuItemIdentifyForList, createMenuItemShowDetailForItem, createMenuItemToBoundActionForItem, createMenuItemMoveUp, createMenuItemMoveDown, createMenuItemAddToGroup } from '@hungpvq/map-dataset/menu';
import { createMenuItemExportGeo } from '@hungpvq/map-dataset/geo-export';
import { createMenuItemAttributeTable } from '@hungpvq/map-dataset/attribute-table';
list.addMenus([
createMenuItemToggleShow(),
createMenuItemIdentifyForList(),
createMenuItemIdentifyForList({ location: 'menu' }),
createMenuItemStyleEdit(),
createMenuItemShowDetailInfoSource(),
createMenuItemToBoundActionForList(),
]);
identify.addMenus([
createMenuItemToBoundActionForItem(),
createMenuItemShowDetailForItem([
{ text: 'Id', value: 'id' },
{ text: 'Name', value: 'name' },
]),
]);| Function | for | Default location | id | Auto-added |
|---|---|---|---|---|
createMenuItemToggleShow | layer | extra | LIST_VIEW_MENU_ID.layer.toggleShow | — (add yourself) |
createMenuItemSetOpacity | layer | prebottom | LIST_VIEW_MENU_ID.layer.setOpacity | List UI unless configDisabledOpacity() |
createMenuItemStyleEdit | layer | extra (unset → default) | LIST_VIEW_MENU_ID.layer.styleEdit | — |
createMenuItemShowDetailInfoSource | layer | unset | LIST_VIEW_MENU_ID.layer.info | — |
createMenuItemToBoundActionForList | layer | extra | LIST_VIEW_MENU_ID.layer.fillBound | createGeoJsonDataset / raster helper |
createMenuItemIdentifyForList | layer | extra (or menu) | LIST_VIEW_MENU_ID.layer.identify / .identifyMenu | createGeoJsonDataset (extra); hidden without identify sibling or without IdentifyControl mounted |
createMenuItemMoveUp / MoveDown | layer | menu | LIST_VIEW_MENU_ID.layer.moveUp / .moveDown | List UI unless configDisabledMove() |
createMenuItemAddToGroup | layer | menu | LIST_VIEW_MENU_ID.layer.addToGroup | List UI unless configDisabledAddToGroup() |
createMenuItemExportGeo | layer | menu | LIST_VIEW_MENU_ID.layer.exportGeo | Opt-in via addMenu; createGeoJsonDataset |
createMenuItemAttributeTable | layer | menu | LIST_VIEW_MENU_ID.layer.attributeTable | Opt-in via addMenu; createGeoJsonDataset |
createMenuItemShowDetailForItem | item | menu | LIST_VIEW_MENU_ID.item.showDetail | Identify builders |
createMenuItemToBoundActionForItem | item | menu | LIST_VIEW_MENU_ID.item.flyTo | Identify builders |
createMenuItemToggleShow
Visibility toggle on the layer title row. Renders the registry component layer-action-toggle-show (LIST_VIEW_MENU_COMPONENT_KEY.toggleShow), not a plain click handler.
| Signature | (menu?: Partial<Omit<MenuItemCustomComponentBottomOrExtra, 'type' | 'click'>>) => MenuAction |
| Location | extra |
| Id | LIST_VIEW_MENU_ID.layer.toggleShow (toggle-show) |
| Needs | List node with WithToggleShow (show / toggleShow) |
list.addMenus([
createMenuItemToggleShow(),
createMenuItemToggleShow({ order: 1, name: 'Visibility' }),
]);Customize the toggle UI
Two registry keys:
| Key | Constant | Role |
|---|---|---|
layer-action-toggle-show | toggleShow | Full logic + default button (or custom via menu componentKey) |
layer-action-toggle-show-button | toggleShowButton | Button UI only (per-layer default and “show all”) |
Map-wide button (every layer using the default toggle, plus show-all): override toggleShowButton with registerComponentForMap.
One layer: pass a custom componentKey on the menu item and register a component that wraps ToggleShow (reuse logic; customize UI via slot / renderButton).
import { LIST_VIEW_MENU_COMPONENT_KEY, createMenuItemToggleShow } from '@hungpvq/map-dataset/menu';
import { UniversalRegistry } from '@hungpvq/vue-map-core'; // or react-map-core
createMenuItemToggleShow({
componentKey: 'demo-layer-toggle-show',
});
function onMapLoaded(map: MapSimple) {
UniversalRegistry.registerComponentForMap(
map.id,
LIST_VIEW_MENU_COMPONENT_KEY.toggleShowButton,
SampleToggleShowButton,
);
UniversalRegistry.registerComponentForMap(
map.id,
'demo-layer-toggle-show',
SampleLayerToggleShow,
);
}Register in onMapLoaded so map-scoped entries survive remount — see UniversalRegistry components.
Demo: Vue / React #/dataset-menu — collapsible Demo guide (lists each layer). Layer byControl · LayerDetail title: Fill bound / ★ Favorite → header on Detail; Detail / List only hidden there.
createMenuItemSetOpacity
Opacity slider on the bottom-left of the list row (prebottom). Registry key: layer-action-set-opacity.
| Signature | (menu?: Partial<Omit<MenuItemBottomOrExtra, 'click'>>) => MenuAction |
| Location | prebottom |
| Id | LIST_VIEW_MENU_ID.layer.setOpacity (set-opacity) |
| Auto | List UI unless .configDisabledOpacity() |
| Needs | List node with opacity helpers |
list.addMenus([createMenuItemSetOpacity({ order: 0 })]);
// or disable the default:
createDatasetPartListViewUiComponentBuilder('Layer')
.configDisabledOpacity()
.build();createMenuItemStyleEdit
Opens the style editor (style-control) via addComponent for the current list layer.
| Signature | (menu?: Partial<Omit<MenuItemBottomOrExtra, 'click'>>) => MenuAction |
| Id | LIST_VIEW_MENU_ID.layer.styleEdit (style-edit) |
| Default name | Edit style |
| Needs | ComponentManagementControl + registry plugin; mapbox layer sibling |
list.addMenus([
createMenuItemStyleEdit(),
createMenuItemStyleEdit({ location: 'menu', name: 'Style…', order: 5 }),
]);createMenuItemShowDetailInfoSource
Opens an info popup (layer-detail) built from getDatasetDetailInfo(layer) (list + source + layer fields). If there are no fields, the click does nothing.
| Signature | (menu?: Partial<Omit<MenuItemBottomOrExtra, 'click'>>) => MenuAction |
| Id | LIST_VIEW_MENU_ID.layer.info (info) |
| Default name | Info |
| Needs | ComponentManagementControl |
list.addMenus([
createMenuItemShowDetailInfoSource(),
createMenuItemShowDetailInfoSource({ location: 'extra', order: 2 }),
]);createMenuItemToBoundActionForList
Fits the map to the layer bbox (fitBounds). Resolves bbox at click time:
props.bbox(if passed — fixed at menu creation)- nearest
boundpart —getData()(Data helper) - nearest
metadatapart —metadata.bbox layer.info.metadata.bbox(if present)
| Signature | (props?: { bbox?: BBox; name?: string }) => MenuAction |
| Location | extra |
| Id | LIST_VIEW_MENU_ID.layer.fillBound (fill-bound) |
| Default name | Fill bound |
| Auto | createGeoJsonDataset / createRasterUrlDataset (with a bound part) |
// Static bbox (won’t change unless you replace the menu)
list.addMenus([
createMenuItemToBoundActionForList({
bbox: [105.83, 21.02, 105.85, 21.04],
name: 'Zoom to layer',
}),
]);
// Dynamic: omit bbox, keep a bound part, update later
import { createDatasetPartBoundComponent } from '@hungpvq/map-dataset';
const bound = createDatasetPartBoundComponent('Cities', [
105.83, 21.02, 105.85, 21.04,
]);
dataset.add(bound);
list.addMenus([createMenuItemToBoundActionForList()]);
// External button / after reload
bound.setData([105.5, 20.5, 106.5, 21.5]);createMenuItemIdentifyForList
Per-layer Identify toggle (same click mode as IdentifyControl, but scoped to this layer’s identify sibling).
| Signature | (options?: IdentifyForListMenuOptions) => MenuAction |
| Default location | extra |
| Id | LIST_VIEW_MENU_ID.layer.identify (identify-layer) / LIST_VIEW_MENU_ID.layer.identifyMenu (identify-layer-menu) |
| Component | layer-action-identify (shared for componentKey and componentMenuKey; UI branches on location) |
| Needs | IdentifyControl mounted; nearest sibling type === 'identify' |
| Auto | createGeoJsonDataset (extra form) |
Options: location?: MenuActionLocation ('extra' \| 'title' \| 'menu' \| 'bottom' \| 'prebottom'), name, icon, hidden, disabled, order.
Hidden when isIdentifyForListMenuHidden(ctx) (no identify sibling, missing mapId, or IdentifyControl not registered on that map). Extra options.hidden is composed after that check.
Active (primary): while this list’s identify scope is on, the button/row uses _active / primary color. Only one layer is active at a time.
list.addMenus([
createMenuItemIdentifyForList(), // title-row icon (`extra`)
createMenuItemIdentifyForList({ location: 'menu' }), // ⋮ row
createMenuItemIdentifyForList({ location: 'bottom' }), // bottom icon
createMenuItemIdentifyForList({ location: 'prebottom' }), // prebottom icon
]);Toggle on → opens IdentifyControl, starts map click, queries only that identify. Toggle off → clears scope and stops click mode. Global Identify toolbar clears the scope (all identify layers again).
createMenuItemToBoundActionForItem
Identify / feature row action: fly to the feature geometry and highlight it.
| Signature | () => MenuAction |
| Location | menu (default — shows under row ⋮ via DatasetMenus) |
| Id | LIST_VIEW_MENU_ID.item.flyTo (fly-to) |
| Default name | Fly to |
| Click | fitBounds on value.geometry + highlight (key: 'identify') |
| Needs | Identify UI + highlight controller / HighlightPointer; value must look like a feature |
identify.addMenus([createMenuItemToBoundActionForItem()]);createMenuItemShowDetailForItem
Identify / feature row: open detail panel and highlight the feature.
| Signature | (fields: FieldFeaturesDef) => MenuAction |
| Location | menu (default — shows under row ⋮ via DatasetMenus) |
| Id | LIST_VIEW_MENU_ID.item.showDetail (show-detail) |
| Default name | Detail |
| Click | addComponent → layer-detail + highlight (key: 'detail') |
| Needs | ComponentManagementControl; fields define labels/keys for value |
The Layer Detail popup renders layer location: 'title' menus then the same item menus as Identify / Attribute Table in #after-title / afterTitle, but omits show-detail (the popup itself).
identify.addMenus([
createMenuItemShowDetailForItem([
{ text: 'Id', value: 'id' },
{ text: 'Name', value: 'name' },
{ trans: 'map.layer-control.field.geometry', value: 'geometry' },
]),
]);FieldFeaturesDef items: { text? | trans?, value: string, inline? }.
createMenuItemMoveUp / createMenuItemMoveDown
Reorder the list row among siblings. Click dispatches LIST_VIEW_MENU_ID.layer.moveUp / LIST_VIEW_MENU_ID.layer.moveDown (handled by LayerControl).
| Move up | Move down | |
|---|---|---|
| Id | LIST_VIEW_MENU_ID.layer.moveUp | LIST_VIEW_MENU_ID.layer.moveDown |
| Order | 20 | 21 |
| Location | menu | menu |
Hidden when (isListViewReorderMenuHidden):
menuContext.readonlyordisabledMovelayer.config.disabled_move(.configDisabledMove())
list.addMenus([
createMenuItemMoveUp(),
createMenuItemMoveDown({ name: 'Down' }),
]);Auto-added by the list builder unless .configDisabledMove().
createMenuItemAddToGroup
Context-menu row with a custom submenu (layer-action-add-to-group): New group + existing groups.
| Signature | (menu?: Partial<Omit<MenuItemBottomOrExtra, 'click'>>) => MenuAction |
| Location | menu |
| Id | LIST_VIEW_MENU_ID.layer.addToGroup (add-to-group) |
| Order | 22 |
| Needs | Registry plugin for the submenu component |
Hidden when: readonly, disabledCreateGroup, or .configDisabledAddToGroup().
Build submenu items yourself with:
import { createAddToGroupSubmenu, LIST_VIEW_MENU_ID } from '@hungpvq/map-dataset/menu';
const items = createAddToGroupSubmenu(
[
{ id: 'g1', name: 'Group A' },
{ id: 'g2', name: 'Group B' },
],
currentGroupId, // excluded from the list
);
// → "New group" + divider + other groupscreateMenuItemExportGeo
Export GeoJSON / KML / CSV / Shapefile from the ⋮ menu. Opens the ExportGeo DraggableModal shell via addComponent (same pattern as Attribute table). Full guide: Export.
| Signature | (menu?: ExportGeoMenuOptions) => MenuAction |
| Location | menu |
| Id | LIST_VIEW_MENU_ID.layer.exportGeo (export-geo) |
| Order | 23 |
| Needs | Registry + ComponentManagementControl; GeoJSON / data-management data |
| Component key | GEO_EXPORT_COMPONENT_KEY.root (layer-action-export-geo; alias LIST_VIEW_MENU_COMPONENT_KEY.exportGeo) |
Options
| Field | Role |
|---|---|
formats | Subset of 'geojson' | 'kml' | 'csv' | 'shapefile' (passed to the shell) |
filename | string or (layer) => string |
getCollection | Custom FeatureCollection (sync/async) |
sourceCrs / targetCrs | EPSG codes for data / initial shell CRS (default 4326) |
hidden / disabled / order / name / icon | Overlay |
Hidden when: menuContext.disabledExport, or the layer has no GeoJSON / data-management export data.
list.addMenu(
createMenuItemExportGeo({
formats: ['geojson', 'kml'],
filename: (layer) => layer.getName(),
}),
);createMenuItemAttributeTable
Opens the attribute table dialog. Full guide: Attribute table.
| Signature | (menu?: AttributeTableMenuOptions) => MenuAction |
| Location | menu |
| Id | LIST_VIEW_MENU_ID.layer.attributeTable (attribute-table) |
| Order | 24 |
| Needs | Registry + ComponentManagementControl; GeoJSON / data-management data |
Options: same overlays as other items, plus columns to limit / rename fields (array of { key, label } / '__geometry', or a Record<key, label>).
Hidden when: menuContext.disabledAttributeTable, or no exportable GeoJSON data.
list.addMenu(
createMenuItemAttributeTable({
columns: [
{ key: 'name', label: 'Name' },
{ key: 'pop', label: 'Population' },
'__geometry',
],
}),
);Constants
Use LIST_VIEW_MENU_ID.layer.* for list-row menus (for: 'layer') and LIST_VIEW_MENU_ID.item.* for identify / attribute-table row menus (for: 'item'). Top-level keys (addComponent, fitBounds, highlight) are click-handler names, not menu ids.
LIST_VIEW_MENU_ID = {
layer: {
toggleShow: 'toggle-show',
info: 'info',
fillBound: 'fill-bound',
styleEdit: 'style-edit',
setOpacity: 'set-opacity',
identify: 'identify-layer',
identifyMenu: 'identify-layer-menu',
moveUp: 'move-up',
moveDown: 'move-down',
addToGroup: 'add-to-group',
addToExistingGroup: 'add-to-existing-group',
exportGeo: 'export-geo',
attributeTable: 'attribute-table',
},
item: {
showDetail: 'show-detail',
flyTo: 'fly-to',
},
addComponent: 'addComponent',
fitBounds: 'fitBounds',
highlight: 'highlight',
};
LIST_VIEW_MENU_COMPONENT_KEY = {
addToGroup: 'layer-action-add-to-group',
exportGeo: 'layer-action-export-geo',
identify: 'layer-action-identify',
toggleShow: 'layer-action-toggle-show',
toggleShowButton: 'layer-action-toggle-show-button',
setOpacity: 'layer-action-set-opacity',
// … layerDetail, styleControl, legends, …
};Move / add-to-group also hide when the list is readonly or LayerControl has disabledMove / disabledCreateGroup.
Condition helpers
Used by LayerControl internally. Call them if you render menus yourself.
resolveMenuContextSource(source)
source is Record<string, any> | (() => Record | undefined) | undefined. Nested functions are unwrapped.
resolveMenuContextSource(() => ({ role: 'admin' })); // { role: 'admin' }
resolveMenuContextSource(undefined); // {}createMenuConditionContext(layer, { mapId, context })
context is an array of sources, merged left → right. ctx.context is a getter (fresh on each read).
import { createMenuConditionContext, isMenuItemHidden, isMenuItemDisabled, resolveMenuCondition } from '@hungpvq/map-dataset/menu';
const ctx = createMenuConditionContext(layer, {
mapId,
context: [
{ readonly: false, disabledMove: false },
() => ({ role: store.role }),
],
});
menus.filter((m) => !isMenuItemHidden(m, ctx));
if (isMenuItemDisabled(menu, ctx)) return;
resolveMenuCondition(menu.hidden, ctx); // booleanVue
| Function | Role |
|---|---|
provideMenuConditionContext(source) | Merges with parent inject |
useMenuConditionSource() | Raw source (object or getter) |
useMenuConditionContext() | Resolved Record<string, any> |
React
| API | Role |
|---|---|
<MenuConditionProvider value={source}> | Merges with parent |
useMenuConditionContext() | Resolved object |
isListViewReorderMenuHidden(menuId, ctx)
isListViewReorderMenuHidden(LIST_VIEW_MENU_ID.layer.moveUp, ctx);
isListViewReorderMenuHidden(LIST_VIEW_MENU_ID.layer.addToGroup, ctx);True when context.readonly, disabledMove / disabledCreateGroup, or layer.config.disabled_move / disabled_add_to_group.
Custom context-menu component
setComponentMenuKey renders your registry component instead of a label. The component owns submenu open/close.
createMenuBuilder()
.item()
.setLocation('menu')
.setName('Sample')
.setIcon(mdiStar)
.setComponentMenuKey('sample-layer-menu')
.build();Register once (same as createDatasetRegistryPlugin() does for add-to-group). Prefer registerComponentForMap + onMapLoaded for page-only overrides — see UniversalRegistry components.
import { UniversalRegistry } from '@hungpvq/vue-map-core';
import SampleCustomMenu from './sample-custom-menu.vue';
UniversalRegistry.registerComponent('sample-layer-menu', SampleCustomMenu);
// or per map:
// UniversalRegistry.registerComponentForMap(mapId, 'sample-layer-menu', SampleCustomMenu);Vue component
<script setup lang="ts">
import type { IListViewUI } from '@hungpvq/map-dataset';
import type { MenuAction } from '@hungpvq/map-dataset/menu';
import { ref } from 'vue';
const props = defineProps<{
item: MenuAction<IListViewUI>;
data?: IListViewUI;
mapId?: string;
}>();
const emit = defineEmits<{ close: [] }>();
const open = ref(false);
function onDone() {
emit('close');
}
</script>
<template>
<li class="layer-context-menu__item" @click.stop="open = !open">
<span>{{ 'name' in item ? item.name : '' }}</span>
<ul v-if="open" class="layer-context-menu layer-context-menu--submenu">
<li class="layer-context-menu__item" @click.stop="onDone">Done</li>
</ul>
</li>
</template>React: same props plus onClose?: () => void instead of emit('close').
| Prop | Vue | React |
|---|---|---|
| Menu definition | item | item |
| Layer | data | data |
| Map id | mapId | mapId |
| Groups | getGroups | getGroups |
| Close popup | emit('close') | onClose() |
Parent row click should toggle a submenu, not close the popup. Call close only after a real action.
isMenuItemCustomComponent(menu) — true when componentMenuKey is set.
Built-in add-to-group: LIST_VIEW_MENU_COMPONENT_KEY.addToGroup ('layer-action-add-to-group').
import { createAddToGroupSubmenu, LIST_VIEW_MENU_ID } from '@hungpvq/map-dataset/menu';
const items = createAddToGroupSubmenu(
[
{ id: 'g1', name: 'Group A' },
{ id: 'g2', name: 'Group B' },
],
currentGroupId, // excluded
);
// → "New group" + divider + existing groupsRuntime API on a dataset that has menus
From createWithMenuHelper() (already mixed into list / identify builders):
list.addMenu(menu);
list.addMenus([a, b]);
list.getMenus();
list.getMenu('admin-only');
list.hasMenu('admin-only');
list.updateMenu('admin-only', (m) => ({ ...m, name: 'Admins' }));
list.removeMenu('admin-only');Duplicate id is ignored on add.
createMenuItem(object) only wraps a raw extra/bottom item. Prefer createMenuBuilder().
handleMenuAction
LayerControl already runs this on click. Use it only if you render a custom list:
import { handleMenuAction } from '@hungpvq/map-dataset/menu';
handleMenuAction(action, {
layer: item,
mapId,
value: item,
event,
});Disabled items should not call this (the default UI already skips them).