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

408 lines
14 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
```
Available on JSR as `@buddy/leaflet-components`.
## 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.map; // 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.
### 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 |
## 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.
## Development
```bash
npm run build # type-check + emit .d.ts via tsc, then bundle to dist/index.js via esbuild
npm run lint # ESLint
npm run format # Prettier
# serve the project root with any static-file server and open index.html
```