|
|
# leaflet-web-components
|
|
|
|
|
|
[Leaflet.js](https://leafletjs.com/) as native Web Components (Custom Elements). Each HTML element maps 1:1 to a Leaflet object with reactive attribute binding.
|
|
|
|
|
|
## Installation
|
|
|
|
|
|
```bash
|
|
|
npm install leaflet-web-components leaflet
|
|
|
```
|
|
|
|
|
|
`leaflet` is a **peer dependency** — you install it, and only one copy must be
|
|
|
present (the nesting protocol relies on `instanceof` against Leaflet's own
|
|
|
classes). The Leaflet type definitions (`@types/leaflet`, `@types/geojson`)
|
|
|
ship as regular dependencies, so TypeScript consumers need nothing extra.
|
|
|
|
|
|
Also published to JSR as [`@buddy/leaflet-components`](https://jsr.io/@buddy/leaflet-components) (`deno add jsr:@buddy/leaflet-components`). There is no CommonJS or UMD build. The examples below use the npm name — substitute `@buddy/leaflet-components` if you pull from JSR.
|
|
|
|
|
|
## Import options
|
|
|
|
|
|
The package is ESM-only — `tsc` emits `dist/` as individual modules, one per component, with no bundling step. Deep imports let you register only what you use.
|
|
|
|
|
|
```js
|
|
|
// Registers all components
|
|
|
import 'leaflet-web-components';
|
|
|
|
|
|
// Register only one component (defines the <leaflet-marker> tag)
|
|
|
import 'leaflet-web-components/components/leaflet-marker.js';
|
|
|
```
|
|
|
|
|
|
Per-component deep imports (`./components/*.js`, `./elements/*.js`) are an **npm-only** convenience — JSR doesn't support subpath patterns, so from JSR the entry points are just the root and `@buddy/leaflet-components/elements` (the class barrel). JSR consumers who want selective registration import the class and call `customElements.define()` themselves.
|
|
|
|
|
|
Each component is two modules with the same basename: `components/leaflet-marker.js` is the side-effecting one that calls `customElements.define()`, and `elements/leaflet-marker.js` is just the class (`default` export, **no** `define`). Import from `elements/` when you want to subclass a component or register it under a different tag name:
|
|
|
|
|
|
```js
|
|
|
// The class only — nothing is registered
|
|
|
import LeafletMarkerElement from 'leaflet-web-components/elements/leaflet-marker.js';
|
|
|
// …or the whole set, by name
|
|
|
import { LeafletMarkerElement, LeafletCircleElement } from 'leaflet-web-components/elements';
|
|
|
```
|
|
|
|
|
|
On npm the package root resolves to a build that also installs the ambient TypeScript augmentations (`HTMLElementTagNameMap` so `document.querySelector('leaflet-map')` is typed, and the custom-event map). The JSR build omits those — JSR's type rules disallow global augmentation — so from JSR you add your own if you want them.
|
|
|
|
|
|
## Usage
|
|
|
|
|
|
Import once to register all custom elements, then use them declaratively in HTML.
|
|
|
|
|
|
```html
|
|
|
<script type="module">
|
|
|
import 'leaflet-web-components';
|
|
|
</script>
|
|
|
|
|
|
<leaflet-map lat="51.505" lng="-0.09" zoom="13" style="height:400px">
|
|
|
<leaflet-tile-layer
|
|
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
|
|
attribution="© OpenStreetMap contributors"
|
|
|
></leaflet-tile-layer>
|
|
|
|
|
|
<leaflet-marker lat="51.505" lng="-0.09">
|
|
|
<leaflet-popup><b>Hello!</b></leaflet-popup>
|
|
|
<leaflet-tooltip>Hover me</leaflet-tooltip>
|
|
|
</leaflet-marker>
|
|
|
|
|
|
<leaflet-circle
|
|
|
lat="51.508"
|
|
|
lng="-0.11"
|
|
|
radius="500"
|
|
|
color="red"
|
|
|
fill-color="#f03"
|
|
|
fill-opacity="0.5"
|
|
|
>
|
|
|
<leaflet-popup>I am a circle</leaflet-popup>
|
|
|
</leaflet-circle>
|
|
|
|
|
|
<leaflet-polygon color="blue">
|
|
|
<leaflet-line lat="51.509" lng="-0.08"></leaflet-line>
|
|
|
<leaflet-line lat="51.503" lng="-0.06"></leaflet-line>
|
|
|
<leaflet-line lat="51.510" lng="-0.047"></leaflet-line>
|
|
|
</leaflet-polygon>
|
|
|
|
|
|
<leaflet-control-scale position="bottomleft"></leaflet-control-scale>
|
|
|
</leaflet-map>
|
|
|
```
|
|
|
|
|
|
## `leaflet-map`
|
|
|
|
|
|
The root component. All other components must be children of `<leaflet-map>`.
|
|
|
|
|
|
```html
|
|
|
<leaflet-map lat="51.505" lng="-0.09" zoom="13" disable-scroll-wheel-zoom></leaflet-map>
|
|
|
```
|
|
|
|
|
|
### View state
|
|
|
|
|
|
These attributes stay in sync with the map as the user interacts with it — panning updates `lat`/`lng`, zooming updates `zoom`.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------- | ------- | --------------------------------------------------------------------------- |
|
|
|
| `lat` | `0` | Center latitude |
|
|
|
| `lng` | `0` | Center longitude |
|
|
|
| `zoom` | `2` | Zoom level |
|
|
|
| `min-zoom` | `0` | Minimum zoom level |
|
|
|
| `max-zoom` | — | Maximum zoom level. When unset, Leaflet uses the tile layer's own max zoom. |
|
|
|
|
|
|
### Interaction
|
|
|
|
|
|
Boolean options that default to `true` are controlled by the presence of a `disable-*` attribute. Handler-based options can be toggled at any time; the rest only apply at construction.
|
|
|
|
|
|
| Attribute | Controls | Live? |
|
|
|
| ------------------------------- | -------------------------------- | ----- |
|
|
|
| `disable-dragging` | Mouse/touch panning | ✓ |
|
|
|
| `disable-scroll-wheel-zoom` | Scroll-wheel zoom | ✓ |
|
|
|
| `disable-double-click-zoom` | Double-click zoom | ✓ |
|
|
|
| `disable-touch-zoom` | Pinch-to-zoom | ✓ |
|
|
|
| `disable-box-zoom` | Shift-drag zoom box | ✓ |
|
|
|
| `disable-keyboard` | Keyboard pan/zoom | ✓ |
|
|
|
| `disable-zoom-control` | Built-in zoom control | — |
|
|
|
| `disable-attribution-control` | Built-in attribution | — |
|
|
|
| `disable-close-popup-on-click` | Close popup on map click | — |
|
|
|
| `disable-track-resize` | Auto-resize on window resize | — |
|
|
|
| `disable-bounce-at-zoom-limits` | Bounce animation at min/max zoom | — |
|
|
|
| `disable-tap-hold` | Long-press context menu (mobile) | — |
|
|
|
|
|
|
Boolean options that default to `false` are enabled by adding the attribute:
|
|
|
|
|
|
| Attribute | Description |
|
|
|
| ----------------- | ------------------------------------------------------------- |
|
|
|
| `prefer-canvas` | Render vector layers on Canvas instead of SVG |
|
|
|
| `world-copy-jump` | Pan to the original world copy when crossing the antimeridian |
|
|
|
|
|
|
### Animation
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------------------- | ------- | -------------------------------- |
|
|
|
| `disable-zoom-animation` | — | Disable CSS zoom animation |
|
|
|
| `disable-fade-animation` | — | Disable tile fade-in |
|
|
|
| `disable-marker-zoom-animation` | — | Disable marker zoom animation |
|
|
|
| `zoom-animation-threshold` | `4` | Max zoom delta for animated zoom |
|
|
|
|
|
|
### Inertia & panning
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------------------- | ---------- | ---------------------------- |
|
|
|
| `disable-inertia` | — | Disable inertial panning |
|
|
|
| `inertia-deceleration` | `3000` | Deceleration rate (px/s²) |
|
|
|
| `inertia-max-speed` | `Infinity` | Maximum inertia speed (px/s) |
|
|
|
| `ease-linearity` | `0.2` | Pan easing linearity |
|
|
|
|
|
|
### Zoom behaviour
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------------------- | ------- | ------------------------------------------------------ |
|
|
|
| `zoom-snap` | `1` | Zoom snapping interval; `0` for continuous zoom |
|
|
|
| `zoom-delta` | `1` | Zoom step per keyboard/button press |
|
|
|
| `max-bounds-viscosity` | `0` | How much the bounds resist panning past them (`0`–`1`) |
|
|
|
|
|
|
### Scroll wheel
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------------- | ------- | ------------------------------------ |
|
|
|
| `wheel-debounce-time` | `40` | Debounce delay for wheel events (ms) |
|
|
|
| `wheel-px-per-zoom-level` | `60` | Pixels of scroll per zoom level |
|
|
|
|
|
|
### Keyboard & touch
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| -------------------- | ------- | ---------------------------------------- |
|
|
|
| `keyboard-pan-delta` | `80` | Pan distance per key press (px) |
|
|
|
| `tap-tolerance` | `15` | Max touch movement to trigger a tap (px) |
|
|
|
|
|
|
### Rendering
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| -------------------- | --------- | ---------------------------------------------------------- |
|
|
|
| `transform-3d-limit` | `8388608` | Max CSS `translate3d` component value before a layer reset |
|
|
|
|
|
|
### Accessing the underlying map
|
|
|
|
|
|
```js
|
|
|
const el = document.querySelector('leaflet-map');
|
|
|
el.leafletObject; // L.Map instance (undefined before connected)
|
|
|
el.zoom; // current zoom (reads live from map, falls back to attribute)
|
|
|
el.scrollWheelZoom = false; // disable at runtime
|
|
|
```
|
|
|
|
|
|
All `leaflet-map` properties are two-way: reading returns the live map value; writing updates both the attribute and the map.
|
|
|
|
|
|
## Escape hatch
|
|
|
|
|
|
Every component exposes a `leafletObject` getter that returns the underlying Leaflet instance. This is useful when you need to call Leaflet API methods directly:
|
|
|
|
|
|
```js
|
|
|
const marker = document.querySelector('leaflet-marker');
|
|
|
marker.leafletObject?.setLatLng([51.5, -0.09]);
|
|
|
|
|
|
const map = document.querySelector('leaflet-map');
|
|
|
map.leafletObject?.flyTo([48.86, 2.35], 13);
|
|
|
```
|
|
|
|
|
|
Returns `undefined` while the element isn't connected to the DOM.
|
|
|
|
|
|
### CSS
|
|
|
|
|
|
Leaflet's stylesheet is loaded from a CDN via a `<link>` in the shadow DOM. Marker icon paths are derived from the CSS URL automatically.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ----------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
|
| `css-url` | `https://unpkg.com/leaflet@1.9.4/dist/leaflet.css` | URL for Leaflet CSS |
|
|
|
| `css-integrity` | `sha256-...` | SRI hash (required with default URL; when `css-url` is custom, no default integrity is used unless explicitly set) |
|
|
|
| `css-crossorigin` | _(auto)_ | `"anonymous"` when integrity is active, otherwise absent. Set explicitly to override. |
|
|
|
|
|
|
### Resize
|
|
|
|
|
|
A `ResizeObserver` on the host element automatically calls `map.invalidateSize()` whenever the element's dimensions change — from CSS classes, inline styles, attribute changes, or parent layout.
|
|
|
|
|
|
## Events
|
|
|
|
|
|
Every event the underlying Leaflet object fires is re-emitted on the element as a `leaflet:<type>` DOM event — `zoomend` becomes `leaflet:zoomend`, `popupopen` becomes `leaflet:popupopen`, and so on. This is generic and automatic for every component; nothing needs to be configured per event.
|
|
|
|
|
|
```js
|
|
|
const map = document.querySelector('leaflet-map');
|
|
|
map.addEventListener('leaflet:moveend', (e) => {
|
|
|
console.log(e.detail.target.getCenter());
|
|
|
});
|
|
|
|
|
|
const marker = document.querySelector('leaflet-marker');
|
|
|
marker.addEventListener('leaflet:dragend', (e) => {
|
|
|
console.log('dragged', e.detail.distance, 'px');
|
|
|
});
|
|
|
```
|
|
|
|
|
|
`event.detail` is the same object a native Leaflet `.on()` listener receives — it always carries `type`, `target`, and `sourceTarget` in addition to whatever fields that particular event adds (`latlng` for mouse events, `popup` for `popupopen`/`popupclose`, `distance` for `dragend`, etc.). These events don't bubble: Leaflet already propagates layer events up to the map internally, so a bubbling DOM event would make `<leaflet-map>` see each one twice.
|
|
|
|
|
|
Which events a component can fire depends on the Leaflet class it wraps — see [Leaflet's own event reference](https://leafletjs.com/reference.html) for the full list per class (Map, Layer, Marker, Path, Popup, TileLayer, etc.).
|
|
|
|
|
|
## TypeScript
|
|
|
|
|
|
`leaflet-*` elements are typed. `document.createElement`/`querySelector` infer the right component class for every tag:
|
|
|
|
|
|
```ts
|
|
|
const map = document.querySelector('leaflet-map'); // LeafletMap | null
|
|
|
```
|
|
|
|
|
|
`addEventListener` is typed per component against the real Leaflet event payload for events that component actually fires — ordinary DOM events (`click`, etc.) still work normally, and an event name the component doesn't fire is a compile error:
|
|
|
|
|
|
```ts
|
|
|
marker.addEventListener('leaflet:dragend', (e) => {
|
|
|
e.detail.distance; // number — DragEndEvent
|
|
|
});
|
|
|
|
|
|
// Type error: baselayerchange is only fired by <leaflet-map>, not <leaflet-marker>.
|
|
|
marker.addEventListener('leaflet:baselayerchange', () => {});
|
|
|
```
|
|
|
|
|
|
There's no generic fallback overload for `leaflet:*` names — that's deliberate, so a typo or a wrong event name doesn't silently type-check. A genuinely dynamic (non-literal) event name string needs a cast.
|
|
|
|
|
|
---
|
|
|
|
|
|
## Components
|
|
|
|
|
|
### `<leaflet-tile-layer>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
|
| `url` | `''` | Tile URL template (`https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`) |
|
|
|
| `attribution` | `''` | Attribution text |
|
|
|
| `min-zoom` | `0` | Minimum zoom level |
|
|
|
| `max-zoom` | `18` | Maximum zoom level |
|
|
|
| `opacity` | `1.0` | Tile layer opacity |
|
|
|
| `z-index` | `1` | Z-index |
|
|
|
| `subdomains` | `'abc'` | Subdomains for `{s}` in the URL template |
|
|
|
| `tms` | — | Use TMS tile coordinate scheme |
|
|
|
| `zoom-offset` | `0` | Offset added to the zoom level when requesting tiles |
|
|
|
| `zoom-reverse` | — | Reverse the zoom level when requesting tiles |
|
|
|
| `detect-retina` | — | Request higher-resolution tiles on retina displays |
|
|
|
| `cross-origin` | `''` | CORS setting for tile images |
|
|
|
| `referrer-policy` | — | `referrerPolicy` for tile image requests |
|
|
|
| `error-tile-url` | `''` | Fallback tile image on load error |
|
|
|
| `tile-size` | `256` | Tile size in pixels |
|
|
|
| `no-wrap` | — | Don't wrap tiles horizontally across the antimeridian |
|
|
|
| `bounds` | `''` | Restrict tile loading to this bounding box, as JSON: `[[south,west],[north,east]]` |
|
|
|
| `class-name` | `''` | CSS class for tile images |
|
|
|
| `min-native-zoom` | — | Lowest zoom level the tile source natively supports |
|
|
|
| `max-native-zoom` | — | Highest zoom level the tile source natively supports |
|
|
|
| `keep-buffer` | `2` | Extra rows/columns of tiles to keep loaded outside the viewport |
|
|
|
| `update-when-idle` | — | Update tiles only when the map stops moving |
|
|
|
| `update-when-zooming` | `true` (toggle with `update-when-zooming="false"`) | Update tiles continuously while zooming |
|
|
|
| `update-interval` | `200` | Throttle between tile updates while panning (ms) |
|
|
|
| `pane` | `'tilePane'` | Map pane name |
|
|
|
|
|
|
Most of these apply only at construction — Leaflet exposes no setter for them, so changing the attribute afterward has no effect. `min-zoom`, `max-zoom`, `opacity`, and `z-index` are live.
|
|
|
|
|
|
### `<leaflet-tile-layer-wms>`
|
|
|
|
|
|
All `<leaflet-tile-layer>` attributes above apply (a WMS layer is a tile layer), plus:
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------- | -------------- | -------------------------------------------------------------------------------------------- |
|
|
|
| `url` | `''` | WMS service URL |
|
|
|
| `layers` | `''` | Comma-separated layer names |
|
|
|
| `styles` | `''` | Comma-separated style names |
|
|
|
| `format` | `'image/jpeg'` | Image format |
|
|
|
| `transparent` | — | Request transparent tiles |
|
|
|
| `version` | `'1.1.1'` | WMS version |
|
|
|
| `uppercase` | — | Use uppercase WMS parameter names |
|
|
|
| `crs` | — | Coordinate reference system by name: `'EPSG3857'`, `'EPSG4326'`, `'EPSG3395'`, or `'Simple'` |
|
|
|
|
|
|
For a CRS Leaflet doesn't ship (a custom projection from a plugin, say), nest a CRS-providing element instead of using the `crs` attribute. It's just a custom element that fires a `leaflet-crs-changed` event on itself, bubbling, carrying the actual `L.CRS` value — no base class required:
|
|
|
|
|
|
```js
|
|
|
class MyCRSProvider extends HTMLElement {
|
|
|
connectedCallback() {
|
|
|
this.dispatchEvent(
|
|
|
new CustomEvent('leaflet-crs-changed', { bubbles: true, detail: { crs: myPluginCRS } }),
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
customElements.define('my-crs-provider', MyCRSProvider);
|
|
|
```
|
|
|
|
|
|
```html
|
|
|
<leaflet-tile-layer-wms url="..." layers="...">
|
|
|
<my-crs-provider></my-crs-provider>
|
|
|
</leaflet-tile-layer-wms>
|
|
|
```
|
|
|
|
|
|
A nested provider takes priority over the `crs` attribute when both are present. `crs` is construction-only (Leaflet has no live setter for it), so picking one up — from either source — rebuilds the whole layer; place the provider so it connects before you need the correct projection to take effect, since there's nothing to revert to partway through a request.
|
|
|
|
|
|
### `<leaflet-marker>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------------- | ------- | ----------------------------- |
|
|
|
| `lat` | `0` | Latitude |
|
|
|
| `lng` | `0` | Longitude |
|
|
|
| `title` | `''` | Tooltip text on hover |
|
|
|
| `alt` | `''` | Alt text for the marker image |
|
|
|
| `draggable` | — | Allow dragging the marker |
|
|
|
| `opacity` | `1.0` | Marker opacity |
|
|
|
| `z-index-offset` | `0` | Z-index offset |
|
|
|
|
|
|
A `<leaflet-icon>` or `<leaflet-div-icon>` child sets the marker's icon (see below); without one, Leaflet's default icon is used.
|
|
|
|
|
|
### `<leaflet-icon>`
|
|
|
|
|
|
Sets the marker icon to an image. Not rendered itself — swapped into a parent `<leaflet-marker>` via `setIcon()` whenever an attribute changes. Produces no icon (Leaflet's default is used instead) until `icon-url` is set.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------- | ------- | --------------------------------------------------------------------- |
|
|
|
| `icon-url` | `''` | Icon image URL (required) |
|
|
|
| `icon-retina-url` | `''` | Higher-resolution icon image URL |
|
|
|
| `icon-size` | — | Icon size as JSON: `[width, height]` |
|
|
|
| `icon-anchor` | — | Point of the icon aligned to the marker's location, as JSON: `[x, y]` |
|
|
|
| `popup-anchor` | — | Popup anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
|
|
|
| `tooltip-anchor` | — | Tooltip anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
|
|
|
| `shadow-url` | `''` | Shadow image URL |
|
|
|
| `shadow-retina-url` | `''` | Higher-resolution shadow image URL |
|
|
|
| `shadow-size` | — | Shadow size as JSON: `[width, height]` |
|
|
|
| `shadow-anchor` | — | Shadow anchor point, as JSON: `[x, y]` |
|
|
|
| `class-name` | `''` | CSS class for the icon image |
|
|
|
|
|
|
### `<leaflet-div-icon>`
|
|
|
|
|
|
Sets the marker icon to an HTML element instead of an image. Content comes from `innerHTML` (or the `html` attribute if no markup is given) — either way it's rebuilt on every change, the same as `<leaflet-icon>`.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------------- | -------------------- | --------------------------------------------------------------------- |
|
|
|
| `icon-size` | — | Icon size as JSON: `[width, height]` |
|
|
|
| `icon-anchor` | — | Point of the icon aligned to the marker's location, as JSON: `[x, y]` |
|
|
|
| `popup-anchor` | — | Popup anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
|
|
|
| `tooltip-anchor` | — | Tooltip anchor point relative to `icon-anchor`, as JSON: `[x, y]` |
|
|
|
| `class-name` | `'leaflet-div-icon'` | CSS class for the icon element |
|
|
|
| `html` | `''` | HTML content, if not using `innerHTML` |
|
|
|
| `bg-pos` | — | Background position offset, as JSON: `[x, y]` |
|
|
|
|
|
|
### `<leaflet-circle>`
|
|
|
|
|
|
All [path style](#path-style) attributes apply.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------- | ------- | ---------------- |
|
|
|
| `lat` | `0` | Center latitude |
|
|
|
| `lng` | `0` | Center longitude |
|
|
|
| `radius` | `1000` | Radius in meters |
|
|
|
|
|
|
### `<leaflet-circle-marker>`
|
|
|
|
|
|
All [path style](#path-style) attributes apply.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------- | ------- | ---------------- |
|
|
|
| `lat` | `0` | Center latitude |
|
|
|
| `lng` | `0` | Center longitude |
|
|
|
| `radius` | `10` | Radius in pixels |
|
|
|
|
|
|
### `<leaflet-polyline>`
|
|
|
|
|
|
Coordinates are taken from child `<leaflet-line>` elements, not from attributes. All [path style](#path-style) attributes apply, except `fill` defaults to unfilled (a polyline isn't a closed shape, but it can still be filled explicitly).
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------------- | ------- | ------------------------------------------------------------------------------- |
|
|
|
| `smooth-factor` | `1.0` | Simplification factor applied while panning/zooming — higher is more simplified |
|
|
|
| `no-clip` | — | Disable polyline clipping |
|
|
|
|
|
|
### `<leaflet-polygon>`
|
|
|
|
|
|
Coordinates are taken from child `<leaflet-line>` elements, not from attributes. All [path style](#path-style) attributes apply.
|
|
|
|
|
|
### `<leaflet-rectangle>`
|
|
|
|
|
|
All [path style](#path-style) attributes apply.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------- | ------- | --------------------------------------------------- |
|
|
|
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
|
|
|
|
|
|
### `<leaflet-line>`
|
|
|
|
|
|
Vertex helper — not rendered directly. Used as a child of `<leaflet-polyline>` or `<leaflet-polygon>`.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------- | ------- | ----------- |
|
|
|
| `lat` | `0` | Latitude |
|
|
|
| `lng` | `0` | Longitude |
|
|
|
|
|
|
### `<leaflet-image-overlay>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------- | ------- | --------------------------------------------------- |
|
|
|
| `url` | `''` | Image URL |
|
|
|
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
|
|
|
| `opacity` | `1.0` | Overlay opacity |
|
|
|
| `alt` | `''` | Alt text |
|
|
|
| `interactive` | — | Receive mouse/touch events |
|
|
|
| `cross-origin` | `''` | CORS setting for the image |
|
|
|
| `error-overlay-url` | `''` | Fallback image on load error |
|
|
|
| `z-index` | `0` | Z-index |
|
|
|
| `class-name` | `''` | CSS class for the image element |
|
|
|
|
|
|
### `<leaflet-video-overlay>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------- | ------------------------------------------------ | --------------------------------------------------- |
|
|
|
| `url` | `''` | Video URL |
|
|
|
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
|
|
|
| `opacity` | `1.0` | Overlay opacity |
|
|
|
| `alt` | `''` | Alt text |
|
|
|
| `interactive` | — | Receive mouse/touch events |
|
|
|
| `cross-origin` | `''` | CORS setting for the video |
|
|
|
| `loop` | — | Loop playback |
|
|
|
| `autoplay` | — | Start playing automatically |
|
|
|
| `muted` | — | Mute audio |
|
|
|
| `playsinline` | — | Play inline (mobile) |
|
|
|
| `z-index` | `0` | Z-index |
|
|
|
| `class-name` | `''` | CSS class for the video element |
|
|
|
| `keep-aspect-ratio` | `true` (toggle with `keep-aspect-ratio="false"`) | Preserve the video's aspect ratio within its bounds |
|
|
|
| `error-overlay-url` | `''` | Fallback image on load error |
|
|
|
|
|
|
### `<leaflet-svg-overlay>`
|
|
|
|
|
|
Content comes from an inline `<svg>` child element. No `url` attribute.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| -------------- | ------- | --------------------------------------------------- |
|
|
|
| `bounds` | `''` | Bounding box as JSON: `[[south,west],[north,east]]` |
|
|
|
| `opacity` | `1.0` | Overlay opacity |
|
|
|
| `interactive` | — | Receive mouse/touch events |
|
|
|
| `cross-origin` | `''` | CORS setting |
|
|
|
| `z-index` | `0` | Z-index |
|
|
|
| `class-name` | `''` | CSS class for the SVG element |
|
|
|
|
|
|
### `<leaflet-layer-group>`
|
|
|
|
|
|
Passthrough container. No attributes. Accepts layers, popups, and tooltips as children.
|
|
|
|
|
|
### `<leaflet-feature-group>`
|
|
|
|
|
|
Passthrough container with Leaflet's `FeatureGroup` (supports `getBounds()`, style propagation, etc.). No attributes. Accepts layers, popups, and tooltips as children.
|
|
|
|
|
|
### `<leaflet-geojson>`
|
|
|
|
|
|
All [path style](#path-style) attributes apply — each feature GeoJSON builds gets them as its style.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| --------- | ------- | -------------- |
|
|
|
| `data` | `''` | GeoJSON string |
|
|
|
|
|
|
### `<leaflet-popup>`
|
|
|
|
|
|
Content comes from `innerHTML`, not an attribute. Mutations to innerHTML sync automatically via a `MutationObserver`.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| -------------- | ------- | ------------------------------------------------------------- |
|
|
|
| `lat` | `0` | Latitude (omit to auto-attach to the parent layer's position) |
|
|
|
| `lng` | `0` | Longitude |
|
|
|
| `max-width` | `300` | Maximum width (px) |
|
|
|
| `min-width` | `50` | Minimum width (px) |
|
|
|
| `max-height` | `0` | Maximum height (px; `0` = unlimited) |
|
|
|
| `auto-pan` | — | Automatically pan the map to keep the popup visible |
|
|
|
| `close-button` | — | Show a close button |
|
|
|
| `auto-close` | — | Close the popup when another popup is opened |
|
|
|
|
|
|
### `<leaflet-tooltip>`
|
|
|
|
|
|
Content comes from `innerHTML`, not an attribute.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ----------- | -------- | ------------------------------------------------------------------------- |
|
|
|
| `lat` | `0` | Latitude (omit to auto-attach to the parent layer's position) |
|
|
|
| `lng` | `0` | Longitude |
|
|
|
| `pane` | — | Map pane name |
|
|
|
| `offset` | — | Pixel offset as JSON: `[x, y]` |
|
|
|
| `direction` | `'auto'` | Direction: `'right'`, `'left'`, `'top'`, `'bottom'`, `'center'`, `'auto'` |
|
|
|
| `permanent` | — | Always visible (no hover required) |
|
|
|
| `sticky` | — | Follow the mouse |
|
|
|
| `opacity` | `1.0` | Tooltip opacity |
|
|
|
|
|
|
### `<leaflet-control-zoom>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------------- | ------------ | ----------------------- |
|
|
|
| `position` | `'topleft'` | Corner position |
|
|
|
| `zoom-in-text` | `'+'` | Zoom-in button label |
|
|
|
| `zoom-in-title` | `'Zoom in'` | Zoom-in button tooltip |
|
|
|
| `zoom-out-text` | `'-'` | Zoom-out button label |
|
|
|
| `zoom-out-title` | `'Zoom out'` | Zoom-out button tooltip |
|
|
|
|
|
|
### `<leaflet-control-attribution>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ---------- | --------------- | --------------------------- |
|
|
|
| `position` | `'bottomright'` | Corner position |
|
|
|
| `prefix` | `''` | Text before the attribution |
|
|
|
|
|
|
### `<leaflet-control-scale>`
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------ | -------------- | ------------------------------------- |
|
|
|
| `position` | `'bottomleft'` | Corner position |
|
|
|
| `max-width` | `100` | Maximum width of the scale (px) |
|
|
|
| `metric` | — | Show metric scale (m/km) |
|
|
|
| `imperial` | — | Show imperial scale (mi/ft) |
|
|
|
| `update-when-idle` | — | Update only when the map stops moving |
|
|
|
|
|
|
### `<leaflet-control-layers>`
|
|
|
|
|
|
The built-in Leaflet layer switcher. Children with `type="base"` appear as radio buttons; children with `type="overlay"` (or no `type`) appear as checkboxes.
|
|
|
|
|
|
```html
|
|
|
<leaflet-map>
|
|
|
<leaflet-control-layers position="topright" collapsed>
|
|
|
<leaflet-tile-layer type="base" name="Streets" active url="..."></leaflet-tile-layer>
|
|
|
<leaflet-tile-layer type="base" name="Satellite" url="..."></leaflet-tile-layer>
|
|
|
<leaflet-marker type="overlay" name="Cities" active lat="51.5" lng="-0.09"></leaflet-marker>
|
|
|
</leaflet-control-layers>
|
|
|
</leaflet-map>
|
|
|
```
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ------------------ | ---------------------------------------- | ------------------------------------------------------------ |
|
|
|
| `position` | `'topright'` | Corner position |
|
|
|
| `collapsed` | `true` (toggle with `collapsed="false"`) | Collapse into an icon until hovered |
|
|
|
| `auto-z-index` | `true` | Assign increasing z-indexes to layers |
|
|
|
| `hide-single-base` | — | Hide the base layers section when only one base layer exists |
|
|
|
| `sort-layers` | — | Sort layers alphabetically |
|
|
|
|
|
|
Each child layer inside `<leaflet-control-layers>` may carry:
|
|
|
|
|
|
| Attribute | Description |
|
|
|
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `type="base"` | Show as a radio button (base layer). Omit or set `type="overlay"` for a checkbox. |
|
|
|
| `name` | Human-readable label displayed in the control. |
|
|
|
| `active` | Add the layer to the map immediately so it starts visible. Internally, the control dispatches a `leaflet-add-layer` event that the map handles. Layers without `active` are removed from the map before the control renders, so they correctly appear unchecked. |
|
|
|
|
|
|
The control intercepts child `leaflet-register` events and stops propagation — layers inside `<leaflet-control-layers>` are managed by the layers control, not added directly to the map.
|
|
|
|
|
|
## Path style
|
|
|
|
|
|
The following attributes apply to `<leaflet-circle>`, `<leaflet-circle-marker>`, `<leaflet-polyline>`, `<leaflet-polygon>`, `<leaflet-rectangle>`, and `<leaflet-geojson>`.
|
|
|
|
|
|
| Attribute | Default | Description |
|
|
|
| ----------------------- | ---------------------------------------------------- | ------------------------------------------------- |
|
|
|
| `stroke` | `true` (toggle with `stroke="false"`) | Draw the stroke |
|
|
|
| `color` | `'#3388ff'` | Stroke color |
|
|
|
| `weight` | `3` | Stroke width (px) |
|
|
|
| `opacity` | `1.0` | Stroke opacity |
|
|
|
| `line-cap` | `'round'` | Line cap: `'butt'`, `'round'`, `'square'` |
|
|
|
| `line-join` | `'round'` | Line join: `'miter'`, `'round'`, `'bevel'` |
|
|
|
| `dash-array` | `''` | Dash pattern, e.g. `'5, 10'` |
|
|
|
| `dash-offset` | `''` | Dash offset |
|
|
|
| `fill` | `true` (toggle with `fill="false"`) | Enable fill |
|
|
|
| `fill-color` | `'#3388ff'` | Fill color |
|
|
|
| `fill-opacity` | `0.2` | Fill opacity |
|
|
|
| `fill-rule` | `'evenodd'` | Fill rule: `'nonzero'`, `'evenodd'` |
|
|
|
| `interactive` | `true` (toggle with `interactive="false"`) | Receive mouse/touch events |
|
|
|
| `bubbling-mouse-events` | `true` (toggle with `bubbling-mouse-events="false"`) | Let mouse events on this layer also reach the map |
|
|
|
| `class-name` | `''` | CSS class for the path element |
|
|
|
| `pane` | `'overlay'` | Map pane name |
|
|
|
|
|
|
`stroke`, `color`, `weight`, `opacity`, `line-cap`, `line-join`, `dash-array`, `dash-offset`, `fill`, `fill-color`, `fill-opacity`, and `fill-rule` are live — Leaflet has a setter for them (`setStyle`). `interactive`, `bubbling-mouse-events`, `class-name`, and `pane` only apply at construction.
|
|
|
|
|
|
## Nesting rules
|
|
|
|
|
|
- **Popup / tooltip as child of a layer** → bound via `bindPopup` / `bindTooltip`.
|
|
|
- **Layer as child of a group** → added via `addLayer`.
|
|
|
- **Any layer as child of `leaflet-map`** → added to the map directly.
|
|
|
- **`leaflet-polygon` / `leaflet-polyline`** take their coordinates from `<leaflet-line>` children, not from attributes.
|
|
|
- **`<leaflet-icon>` / `<leaflet-div-icon>` as child of `<leaflet-marker>`** → swapped in via `setIcon()`.
|
|
|
- **Layer as child of `<leaflet-control-layers>`** → intercepted by the control and registered as a base or overlay entry. The control stops propagation so the layer doesn't reach the map directly. Use `active` to start the layer visible.
|
|
|
|
|
|
## Building your own components
|
|
|
|
|
|
This library's own components are built from a small toolkit — a mixin, some prop-codec builders, and a few reusable fragments — and that whole toolkit is exported from the package root. A component wrapping a Leaflet plugin (a custom layer, control, or icon) is built exactly the same way `<leaflet-marker>` or `<leaflet-circle>` are, whether or not it ever ends up in this package.
|
|
|
|
|
|
### The pattern
|
|
|
|
|
|
1. Describe your attributes as a `const PROPS` table, build a base class with `const Base = WithProps(PROPS, options?)`, then `extends Base`.
|
|
|
2. Implement `createLeafletObject(options)`, returning whatever Leaflet object your plugin provides.
|
|
|
3. `declare readonly leafletObject?: TheType;` — `WithProps`'s own inference of the object type from the `PROPS` table alone isn't reliable enough to skip this (every component in this package does it).
|
|
|
4. Call `customElements.define('my-plugin-layer', MyPluginLayer)`. (If you want to let _your_ consumers subclass or re-register, mirror this package's split: put the class in its own module as a `default` export and keep the `define()` call in a separate side-effecting module.)
|
|
|
|
|
|
```ts
|
|
|
import { MyClusterGroup, type MyClusterGroupOptions } from 'some-leaflet-plugin';
|
|
|
import {
|
|
|
WithProps,
|
|
|
bool,
|
|
|
num,
|
|
|
type PropDef,
|
|
|
type LeafletElementConstructor,
|
|
|
} from 'leaflet-web-components';
|
|
|
|
|
|
const PROPS: {
|
|
|
radius: PropDef<number>;
|
|
|
disableClusteringAtZoom: PropDef<number>;
|
|
|
spiderfy: PropDef<boolean>;
|
|
|
} = {
|
|
|
radius: num(80),
|
|
|
disableClusteringAtZoom: num(18),
|
|
|
spiderfy: bool(true),
|
|
|
};
|
|
|
|
|
|
const Base: LeafletElementConstructor<MyClusterGroup, typeof PROPS> = WithProps(PROPS);
|
|
|
|
|
|
class MyClusterLayer extends Base {
|
|
|
declare readonly leafletObject?: MyClusterGroup;
|
|
|
|
|
|
createLeafletObject(options: MyClusterGroupOptions): MyClusterGroup {
|
|
|
return new MyClusterGroup(options);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
customElements.define('my-cluster-layer', MyClusterLayer);
|
|
|
```
|
|
|
|
|
|
The two `const`s with explicit type annotations — rather than
|
|
|
`class X extends WithProps({ ... })` inline — are what lets this package
|
|
|
publish to JSR, whose type checker rejects a call expression as a superclass
|
|
|
and any public type it can't resolve without full inference. If you're only
|
|
|
consuming the package in an app (not publishing your component to JSR), the
|
|
|
inline `extends WithProps({ ... })` form works too.
|
|
|
|
|
|
That's it — no other integration point is needed. `WithProps` handles `observedAttributes`, attribute↔property sync, building the options object from attributes on connect, dispatching attribute changes to the matching Leaflet setter (or a `set` you provide per prop, for anything a setter can't handle), and re-emitting every event your object fires as `leaflet:<type>`.
|
|
|
|
|
|
### Attribute builders (`PROPS` table entries)
|
|
|
|
|
|
| Builder | Attribute holds | Notes |
|
|
|
| ------------------------------------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
|
| `str(default?, opts?)` | a string | |
|
|
|
| `num(default?, opts?)` | a number | |
|
|
|
| `bool(default?, opts?)` | a boolean | present/absent by default; use `bool(true, ...)` for an option Leaflet defaults to `true`, so `attr="false"` can opt out |
|
|
|
| `choice<'a' \| 'b'>(default, opts?)` | a string literal union | not runtime-validated, just typed |
|
|
|
| `json<T>(default, opts?)` | JSON | decodes/encodes via `JSON.parse`/`JSON.stringify` — for points, bounds, arbitrary objects |
|
|
|
| `disabled(opts?)` | a boolean | the inverse of `bool(true)`: attribute is auto-named `disable-<kebab-name>`, presence means `false` |
|
|
|
| `positional(def)` | — | wraps any of the above; marks a prop your Leaflet constructor takes as an argument rather than an option, so it's excluded from the options object `createLeafletObject` receives |
|
|
|
|
|
|
Each builder's `opts` can include `set(obj, value, el)` (called on attribute change instead of the default `setXyz()` lookup — needed when there's no matching setter, or the update needs more than one value, like a lat/lng pair), `get(obj)` (backs the live property getter, and — paired with `event` — keeps the attribute in sync whenever your object fires that event), and `attribute` (override the default kebab-cased name).
|
|
|
|
|
|
### Reusing the built-in fragments
|
|
|
|
|
|
Common attribute groups are already factored out and exported: `pathProps` (Leaflet `Path` styling — color, weight, dashes, fill, etc.), `latLngProps` (a synced lat/lng pair, for anything positioned on the map), `tileLayerProps` (the base `GridLayer`/`TileLayer` options), and `urlProp`. Each is exported with an explicit object type, so a `PROPS` annotation can pull it in with `typeof`. A custom vector layer plugin, for instance, can just spread `pathProps` in instead of redeclaring styling attributes:
|
|
|
|
|
|
```ts
|
|
|
import {
|
|
|
WithProps,
|
|
|
pathProps,
|
|
|
positional,
|
|
|
json,
|
|
|
type Positional,
|
|
|
type LeafletElementConstructor,
|
|
|
} from 'leaflet-web-components';
|
|
|
|
|
|
const PROPS: typeof pathProps & {
|
|
|
data: Positional<MyData | null>;
|
|
|
} = {
|
|
|
...pathProps,
|
|
|
data: positional(json<MyData | null>(null)),
|
|
|
};
|
|
|
|
|
|
const Base: LeafletElementConstructor<MyShape, typeof PROPS> = WithProps(PROPS);
|
|
|
|
|
|
class MyShapeLayer extends Base {
|
|
|
/* ... */
|
|
|
}
|
|
|
```
|
|
|
|
|
|
`Positional<T, Obj>` is the exported alias for a `positional()` prop's type
|
|
|
(`PropDef<T, Obj> & { option: false }`).
|
|
|
|
|
|
### Nesting into the tree
|
|
|
|
|
|
By default (`attach: 'children'`, the default when you omit `options`), your component registers itself with its nearest ancestor and accepts registering descendants as child layers/popups/tooltips. Use `attach: 'self'` for something that has a parent but manages no children of its own (a popup, a tooltip, a control); use `attach: 'none'` for something that's neither (an icon).
|
|
|
|
|
|
The registration check on the _receiving_ side keys off `instanceof Layer` / `instanceof Popup` / `instanceof Tooltip` — Leaflet's own base classes, not this package's. Since real Leaflet plugins are conventionally built by extending those same classes, a plugin layer or control nests correctly under `<leaflet-map>`, `<leaflet-layer-group>`, `<leaflet-feature-group>`, or `<leaflet-control-layers>` with no special-casing on either side.
|
|
|
|
|
|
### Typing events
|
|
|
|
|
|
`LeafletAddEventListener<TEvents>`/`LeafletRemoveEventListener<TEvents>` narrow `addEventListener`/`removeEventListener` for `leaflet:<name>` events, the same way every component in this package does:
|
|
|
|
|
|
```ts
|
|
|
import {
|
|
|
WithProps,
|
|
|
type LeafletAddEventListener,
|
|
|
type LeafletRemoveEventListener,
|
|
|
type PathEvents,
|
|
|
} from 'leaflet-web-components';
|
|
|
|
|
|
// Base = WithProps(PROPS), as in the previous example
|
|
|
class MyShapeLayer extends Base {
|
|
|
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
|
|
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
|
|
/* ... */
|
|
|
}
|
|
|
```
|
|
|
|
|
|
`event-types.ts`'s fragments (`MouseEvents`, `PopupBindEvents`, `DragEvents`, `TileEvents`, ...) and their compositions (`PathEvents`, `MarkerEvents`, `TileLayerEvents`, `GroupEvents`, `MapEvents`, ...) are all exported for reuse; compose your own if your plugin fires events none of them cover. If you also want `document.createElement('my-cluster-layer')` to infer your class, augment `HTMLElementTagNameMap`:
|
|
|
|
|
|
```ts
|
|
|
declare global {
|
|
|
interface HTMLElementTagNameMap {
|
|
|
'my-cluster-layer': MyClusterLayer;
|
|
|
}
|
|
|
}
|
|
|
```
|
|
|
|
|
|
This package keeps its own version of that augmentation in a dedicated module (`src/core/globals.ts`) that its npm entry imports and its JSR entry doesn't — JSR disallows `declare global`. If you publish your plugin to JSR, do the same: keep the augmentation out of your JSR entry's module graph.
|
|
|
|
|
|
### Custom marker icons
|
|
|
|
|
|
`<leaflet-marker>` listens for a generic `icon-changed` event and calls `setIcon()` with whatever `detail.icon` holds — it doesn't check which component fired it. A component wrapping a plugin that provides its own icon (e.g. a themed marker icon set) just needs to call `emitIconChanged`:
|
|
|
|
|
|
```ts
|
|
|
import { emitIconChanged } from 'leaflet-web-components';
|
|
|
|
|
|
// after building or updating your icon:
|
|
|
emitIconChanged(this, myPluginIcon);
|
|
|
// and on disconnect:
|
|
|
emitIconChanged(this, null);
|
|
|
```
|
|
|
|
|
|
and it plugs into any `<leaflet-marker>` immediately, the same as this package's own `<leaflet-icon>`/`<leaflet-div-icon>`.
|
|
|
|
|
|
### One ordering pitfall, if your plugin has multiple custom elements
|
|
|
|
|
|
`customElements.define()` upgrades every matching element already parsed into the page immediately and synchronously. If one of your custom elements listens for a bubbling announcement fired by another of your own custom elements on connect (the way `<leaflet-polygon>` listens for `<leaflet-line>`), the listening one must be registered (`customElements.define`d) first — otherwise, on a real static-HTML page, every instance of the "announcer" tag upgrades and fires its one-shot announcement before the "listener" tag even exists, and it's lost for good. This only matters for relationships _within_ your own plugin; it doesn't affect nesting into this package's components, which is handled by the bubbling registration protocol above, not a one-shot announcement.
|
|
|
|
|
|
## Development
|
|
|
|
|
|
```bash
|
|
|
npm run build # tsc emits individual ESM modules to dist/ (no bundling)
|
|
|
npm run typecheck # tsc --noEmit, then tsc -p tsconfig.test.json for test/
|
|
|
npm run lint # oxlint over src/ and test/
|
|
|
npm run format # oxfmt
|
|
|
npm run test # vitest run
|
|
|
npm run test:watch # vitest, watch mode
|
|
|
# serve the project root with any static-file server and open index.html
|
|
|
```
|
|
|
|
|
|
Design docs — how the library is built and why — are in [`docs/`](./docs/README.md).
|