From e45162b0be81e87acc22bd9bfaf1dda34bfc37e8 Mon Sep 17 00:00:00 2001 From: Buddy Date: Tue, 9 Jun 2026 23:52:54 -0700 Subject: [PATCH] feat: add leaflet-control-layers component New wraps Control.Layers. Children with slot="base" or slot="overlay" and a name attribute are automatically registered as base layers or overlays. The control intercepts child leaflet-register events and stops propagation so layers are managed by the layers control rather than added directly to the map. Unchecked layers that connected before the control are removed from the map before the control initialises, so Control.Layers._addItem correctly reflects the unchecked state on first render. --- demos/layers-control.html | 53 ++++++++++++++ src/components/leaflet-control-layers.ts | 92 ++++++++++++++++++++++++ src/index.ts | 1 + 3 files changed, 146 insertions(+) create mode 100644 demos/layers-control.html create mode 100644 src/components/leaflet-control-layers.ts diff --git a/demos/layers-control.html b/demos/layers-control.html new file mode 100644 index 0000000..a1f37ed --- /dev/null +++ b/demos/layers-control.html @@ -0,0 +1,53 @@ + + + + + + Layers Control Demo + + + +

Leaflet Layers Control

+ + + + + + + + + + Marker inside a layers control overlay + + + + + + + diff --git a/src/components/leaflet-control-layers.ts b/src/components/leaflet-control-layers.ts new file mode 100644 index 0000000..2b4da58 --- /dev/null +++ b/src/components/leaflet-control-layers.ts @@ -0,0 +1,92 @@ +import { Control, ControlPosition, Layer } from 'leaflet'; +import { defineProps, str } from '../core/props.ts'; +import { WithProps, registerWithParent } from '../core/utils.ts'; +import { LeafletRegisterEvent } from '../core/register.ts'; + +const PROPS = defineProps({ + position: str('topright'), +}); + +export class LeafletControlLayers extends WithProps(HTMLElement, PROPS) { + #obj?: Control.Layers; + + connectedCallback() { + const baseLayers: Record = {}; + const overlays: Record = {}; + const uncheckedLayers: Layer[] = []; + + for (const child of this.querySelectorAll(':scope > *')) { + const layer = (child as unknown as { leafletObject?: Layer }).leafletObject; + const name = child.getAttribute('name'); + if (!name || !layer) continue; + const base = child.getAttribute('slot') === 'base'; + const checked = child.hasAttribute('checked'); + (base ? baseLayers : overlays)[name] = layer; + if (!checked) uncheckedLayers.push(layer); + } + + const map = this.#map; + for (const layer of uncheckedLayers) map?.removeLayer(layer); + + this.#obj = new Control.Layers(baseLayers, overlays, { + position: this.getAttribute('position') as ControlPosition | undefined, + collapsed: !this.hasAttribute('collapsed') || this.getAttribute('collapsed') !== 'false', + autoZIndex: + !this.hasAttribute('auto-z-index') || this.getAttribute('auto-z-index') !== 'false', + hideSingleBase: this.hasAttribute('hide-single-base'), + sortLayers: this.hasAttribute('sort-layers'), + }); + + registerWithParent(this, this.#obj); + this.addEventListener('leaflet-register', this.#onChildRegister); + } + + disconnectedCallback() { + this.removeEventListener('leaflet-register', this.#onChildRegister); + this.#obj?.remove(); + this.#obj = undefined; + } + + attributeChangedCallback(name: string) { + if (name === 'position' && this.#obj) { + this.#obj.setPosition(this.getAttribute('position') as ControlPosition); + } + } + + get leafletObject() { + return this.#obj; + } + + get #map(): + | { + removeLayer(l: Layer): unknown; + addLayer(l: Layer): unknown; + } + | undefined { + const mapEl = this.closest('leaflet-map'); + if (!mapEl) return undefined; + return ( + mapEl as unknown as { + leafletObject?: { + removeLayer(l: Layer): unknown; + addLayer(l: Layer): unknown; + }; + } + ).leafletObject; + } + + #onChildRegister = (e: LeafletRegisterEvent) => { + e.stopPropagation(); + const el = e.detail.element; + const layer = e.detail.leafletObject; + const name = el.getAttribute('name'); + if (!name || !layer) return; + const base = el.getAttribute('slot') === 'base'; + const checked = el.hasAttribute('checked'); + if (base) this.#obj!.addBaseLayer(layer, name); + else this.#obj!.addOverlay(layer, name); + if (checked) this.#map?.addLayer(layer); + }; +} + +customElements.define('leaflet-control-layers', LeafletControlLayers); diff --git a/src/index.ts b/src/index.ts index 4dab2d5..ba8b88b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ export { LeafletSVGOverlay } from './components/leaflet-svg-overlay.ts'; export { LeafletLayerGroup } from './components/leaflet-layer-group.ts'; export { LeafletFeatureGroup } from './components/leaflet-feature-group.ts'; export { LeafletGeoJSON } from './components/leaflet-geojson.ts'; +export { LeafletControlLayers } from './components/leaflet-control-layers.ts'; export { LeafletControlZoom } from './components/leaflet-control-zoom.ts'; export { LeafletControlAttribution } from './components/leaflet-control-attribution.ts'; export { LeafletControlScale } from './components/leaflet-control-scale.ts';