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
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="© OpenStreetMap"
|
||||
></leaflet-tile-layer>
|
||||
<leaflet-tile-layer
|
||||
slot="base"
|
||||
name="Light"
|
||||
url="https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png"
|
||||
attribution="© CARTO"
|
||||
></leaflet-tile-layer>
|
||||
<leaflet-tile-layer
|
||||
slot="base"
|
||||
name="Dark"
|
||||
url="https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"
|
||||
attribution="© 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);
|
||||
Loading…
Reference in New Issue