feat: add leaflet-control-layers component

New <leaflet-control-layers> 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.
main
Buddy 3 months ago
parent f64b4e70de
commit e45162b0be

@ -0,0 +1,53 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Layers Control Demo</title>
<style>
* { box-sizing: border-box; }
body { margin: 0; padding: 20px; font-family: sans-serif; }
leaflet-map { height: 500px; display: block; border: 1px solid #ccc; }
</style>
</head>
<body>
<h1>Leaflet Layers Control</h1>
<leaflet-map id="map" lat="51.5" lng="-0.09" zoom="12">
<leaflet-control-layers position="topright" collapsed>
<leaflet-tile-layer
slot="base"
name="OpenStreetMap"
checked
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution="&copy; OpenStreetMap"
></leaflet-tile-layer>
<leaflet-tile-layer
slot="base"
name="Light"
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
attribution="&copy; CARTO"
></leaflet-tile-layer>
<leaflet-tile-layer
slot="base"
name="Dark"
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"
attribution="&copy; CARTO"
></leaflet-tile-layer>
</leaflet-control-layers>
<leaflet-marker lat="51.5" lng="-0.09">
<leaflet-popup>Marker inside a layers control overlay</leaflet-popup>
</leaflet-marker>
</leaflet-map>
<script type="importmap">
{
"imports": {
"leaflet": "/node_modules/leaflet/dist/leaflet-src.esm.js"
}
}
</script>
<script type="module" src="/dist/index.js"></script>
</body>
</html>

@ -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<string, Layer> = {};
const overlays: Record<string, Layer> = {};
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);

@ -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';

Loading…
Cancel
Save