Skip to content
Docs version
@hungpvq/vue-map@1.1.0@hungpvq/vue-map-core@1.1.0@hungpvq/vue-map-dataset@1.1.0@hungpvq/vue-map-draw@1.1.0@hungpvq/map-draw@1.1.0@hungpvq/react-map@1.1.0@hungpvq/react-map-core@1.1.0@hungpvq/react-map-dataset@1.1.0@hungpvq/react-map-draw@1.1.0

Layer Control

Editable layer list: create, group, reorder, delete, run menus.

Props

PropDescriptionTypeRequiredDefault Value
mapIdstringfalse--
dragIdstringfalse--
btnWidthnumberfalse40
controlOrderCSS flex order for the standalone buttonnumberfalse0
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'
controlVisiblebooleanfalsetrue

Map also accepts buttonInMobile ('button' | 'toolbar' | 'menu', default 'button'). On viewports ≤640px:

  • 'button' — leave corner buttons unchanged
  • 'toolbar' — move declared control buttons into a single ToolbarControl host (except controlLayout="button"). Mount one ToolbarControl in the map slot
  • 'menu' — hide per-control corner buttons; fan out intact clusters by position into corner stacks with outside-in More. Mount one ToolbarControl.

See ToolbarControl.

PropTypeDefaultEffect
showbooleanOpen the panel
disabledCreatebooleanfalseHide create-layer
disabledCreateGroupbooleanfalseHide create-group and hide Add to group in ⋮
disabledDeleteAllbooleanfalseHide delete-all
disabledMovebooleanfalseHide Move up/down in ⋮
menuContextobject | (() => 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.

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.

VueReact
Store / switcher:title="trans('map.layer-control.title')"title={trans('map.layer-control.title')}
Styled header#title slottitleNode={<span className="…">…</span>}
Menus in header#after-titleafterTitle

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:

HelperRole
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):

TypeRole
IListViewUIList protocol on a dataset part
LayerListItemFlat row for LayerControl drag list (IListViewUI + tree Item, group narrowed to { id, name })
LayerListTreeNodeTreeItem<LayerListItem>
LayerListGroupTreeGroupTree<LayerListItem>
ListViewGroupRefNormalized { 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

NameVueReact
titleListslot { mapId }ReactNode | ({ mapId }) => ReactNode
endListslot { mapId }same
defaultextra childrenchildren

Vue

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

tsx
<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-id and 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-empty instead of the regular empty-state.