Layer Control
Editable layer list: create, group, reorder, delete, run menus.
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 |
menuContext is merged with readonly, disabledMove, disabledCreateGroup and any parent provideMenuConditionContext / MenuConditionProvider. See Menus.
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) |
Custom LayerControl forks should call registerAddGeojsonHereForMap on mount (and its cleanup on unmount) when the map also mounts MapContextMenuControl.
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 />Read-only list: LayerInfoControl.
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.