You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
leaflet-components/README.md

477 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# leaflet-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-components
```
## Import options
The package ships multiple bundle formats and supports deep imports for tree-shaking.
```js
// Default ESM bundle (recommended) — registers all components
import 'leaflet-components';
// Minified ESM bundle
import 'leaflet-components/dist/index.min.js';
// CommonJS
const lc = require('leaflet-components');
// Deep import — register only one component
import 'leaflet-components/dist/components/leaflet-marker.js';
// UMD (for script tags, expects Leaflet as window.L)
// <script src="node_modules/leaflet-components/dist/index.umd.js"></script>
```
`leaflet` is always an external dependency — you must install it yourself.
## Usage
Import once to register all custom elements, then use them declaratively in HTML.
```html
<script type="module">
import 'leaflet-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>
```
### 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.
---
## 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` | `0` | Z-index |
### `<leaflet-tile-layer-wms>`
| 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 |
### `<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 |
### `<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 (excluding `fill*` — polylines don't fill).
| Attribute | Default | Description |
|---|---|---|
| `color` | `'#3388ff'` | Stroke color |
| `weight` | `3` | Stroke width (px) |
| `opacity` | `1.0` | Stroke opacity |
### `<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) |
### `<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.
| Attribute | Default | Description |
|---|---|---|
| `data` | `''` | GeoJSON string |
| `stroke` | `''` | Stroke color (overrides `color`) |
| `line-cap` | `'round'` | Line cap style |
| `line-join` | `'round'` | Line join style |
| `dash-array` | `''` | Dash pattern |
| `dash-offset` | `''` | Dash offset |
| `fill-rule` | `'evenodd'` | Fill rule |
### `<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 |
|---|---|---|
| `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` | — | Enable fill |
| `fill-color` | `'#3388ff'` | Fill color |
| `fill-opacity` | `0.2` | Fill opacity |
| `fill-rule` | `'evenodd'` | Fill rule: `'nonzero'`, `'evenodd'` |
## 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.
- **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.
## Development
```bash
npm run build # tsc emits individual ESM modules; Rollup bundles ESM, CJS + UMD (minified/unminified)
npm run typecheck # tsc --noEmit
npm run lint # ESLint
npm run format # Prettier
# serve the project root with any static-file server and open index.html
```