Layer Control
Editable layer list: create, group, reorder, delete, run menus. Click a group name (or the pencil) to rename it.
Props
| Prop | Description | Type | Required | Default Value |
|---|---|---|---|---|
mapId | string | false | -- | |
dragId | string | false | -- | |
btnWidth | number | false | 40 | |
controlOrder | CSS flex order for the standalone button | number | false | 0 |
position | 'top-left', 'top-right', 'bottom-left', 'bottom-right' | false | 'bottom-right' | |
controlLayout | 'standalone' (corner), 'toolbar' (always in toolbar), 'button' (always corner; not auto-promoted on mobile) | 'standalone' | 'toolbar' | 'button' | false | 'standalone' |
controlVisible | boolean | false | true |
Map also accepts buttonInMobile ('button' | 'toolbar' | 'menu', default 'button'). On viewports ≤640px:
'button'— leave corner buttons unchanged'toolbar'— move declared control buttons into a singleToolbarControlhost (exceptcontrolLayout="button"). Mount oneToolbarControlin the map slot'menu'— hide per-control corner buttons; fan out intact clusters bypositioninto corner stacks with outside-in More. Mount oneToolbarControl.
See ToolbarControl.
| Prop | Type | Default | Effect |
|---|---|---|---|
show | boolean | — | Open the panel |
disabledCreate | boolean | false | Hide create-layer |
disabledCreateGroup | boolean | false | Hide create-group and hide Add to group in ⋮ |
disabledDeleteAll | boolean | false | Hide delete-all |
disabledMove | boolean | false | Hide Move up/down in ⋮ |
menuContext | object | (() => object) | — | Bag for setHidden / setDisabled |
globalVisibilityMode | 'override' | 'sync' | 'sync' | Header eye: mask map only vs sync every layer show |
createLayerTypes | LayerType[] | all types | Allowlist for nested CreateControl type select |
menuContext is merged with readonly, disabledMove, disabledCreateGroup and any parent provideMenuConditionContext / MenuConditionProvider. See Menus.
globalVisibilityMode:
sync(default) — header eye sets every list item’sshowto the all-button value and applies on the map.override— header eye hides/restores layers on the map only; each row’s intendedshowis unchanged.
createLayerTypes is forwarded to the nested CreateControl (with controlVisible={false} so CreateControl’s own chrome button stays hidden). Omit for all LAYER_TYPES.
Nested CreateControl always uses controlVisible={false}; open create via LayerControl’s +. Mount CreateControl alone when you want its map chrome button. Events: none. Visibility / opacity fire on the list node (toggleShow, changeOpacity) — see Events. Dialogs from ⋮ menus need ComponentManagementControl. Create-layer uses CreateControl internally.
Keyboard: / focuses the layer search input for that map ([data-map-layer-search][data-map-id="<mapId>"], with rAF retry until mounted). Esc closes the top open control and blurs search only if that map’s search was focused.
Sidebar header (Vue ↔ React)
LayerControl mounts a sidebar panel. Pass a plain title string for the panel switcher menu; use Vue #title / React titleNode only for styled header text. Header layout: [ title | after-title ] …… [ extra-btn ] — menus with location: 'title' render in after-title / afterTitle, not trailing chrome.
| Vue | React | |
|---|---|---|
| Store / switcher | :title="trans('map.layer-control.title')" | title={trans('map.layer-control.title')} |
| Styled header | #title slot | titleNode={<span className="…">…</span>} |
| Menus in header | #after-title | afterTitle |
Do not pass a ReactNode as React title — the switcher store coerces non-strings to '' and blank menu labels appear.
Thin host / core helpers
LayerControl stays a UI host. Shared orchestration lives in @hungpvq/map-dataset:
| Helper | Role |
|---|---|
getLayerControlTitleMenuState(roots) | Aggregate location: 'title' menus across dataset list views for #after-title / afterTitle |
registerAddGeojsonHereForMap(mapId, addDataset) | Register context-menu “Add GeoJSON here” + default items; returns unregister for unmount |
syncListViewLayerOrder(map, views) | Apply drag-reorder / index to MapLibre layers (used by LayerList) |
applyAllLayerVisibility(items, map, show, mode) | Header eye: override (map mask) or sync (intended show) |
syncAllLayerIntendedShow(items, map, show) | Set every list item show and apply on map |
Custom LayerControl forks should call registerAddGeojsonHereForMap on mount (and its cleanup on unmount) when the map also mounts MapContextMenuControl.
Layer list rows refresh when datasets are added/removed via useMapDataset(mapId).datasetVersion (pass the mapId ref/computed, not mapId.value — plain store bag is not framework-reactive) — see useMapDataset.
Layer list types (custom UIs)
Flat rows and drag trees use shared types from @hungpvq/map-dataset (not adapter-local aliases):
| Type | Role |
|---|---|
IListViewUI | List protocol on a dataset part |
LayerListItem | Flat row for LayerControl drag list (IListViewUI + tree Item, group narrowed to { id, name }) |
LayerListTreeNode | TreeItem<LayerListItem> |
LayerListGroupTree | GroupTree<LayerListItem> |
ListViewGroupRef | Normalized { id, name } group on a flat row |
Use convertListToTree / convertTreeToList / mergeEmptyGroups with LayerListItem[]. See List UI.
When MapContextMenuControl is on the same map, Quick analysis includes Buffer 500 m here, Buffer 1 km here, and Buffer 5 km here. Clicking one adds a GeoJSON circle layer (with a Fill bound extra button).
Slots / render props
| Name | Vue | React |
|---|---|---|
titleList | slot { mapId } | ReactNode | ({ mapId }) => ReactNode |
endList | slot { mapId } | same |
default | extra children | children |
Vue
<script setup lang="ts">
import { reactive } from 'vue';
import { Map, BaseMapCard } from '@hungpvq/vue-map-core';
import { LayerControl, ComponentManagementControl } from '@hungpvq/vue-map-dataset';
import '@hungpvq/map-core/style.css';
import '@hungpvq/map-dataset/style.css';
import '@hungpvq/vue-map-core/style.css';
import '@hungpvq/vue-map-dataset/style.css';
import '@hungpvq/vue-draggable/style.css';
const menuUi = reactive({ role: 'admin', canUsePen: true });
</script>
<template>
<Map>
<LayerControl
position="top-left"
show
:disabled-create="false"
:disabled-create-group="false"
:disabled-delete-all="false"
:disabled-move="false"
:menu-context="menuUi"
>
<template #titleList="{ mapId }">
<button type="button">Extra header ({{ mapId }})</button>
</template>
<template #endList="{ mapId }">
<BaseMapCard :mapId="mapId" />
</template>
</LayerControl>
<ComponentManagementControl />
</Map>
</template>React
<LayerControl
position="top-left"
show
disabledCreateGroup={false}
disabledMove={false}
menuContext={{ role: 'admin', canUsePen: true }}
titleList={<button type="button">Extra header</button>}
endList={({ mapId }) => <BaseMapCard mapId={mapId} />}
/>
<ComponentManagementControl />Search behavior
- Search input uses
data-map-layer-search+data-map-idand is focused by/shortcut (bindMapKeyboardShortcuts) for that map only. - Filtering is debounced on the client (~150ms) and matching text is highlighted in row titles.
- When the query has no result, the panel shows
search-emptyinstead of the regular empty-state.