Compare commits

...

3 Commits

Author SHA1 Message Date
Buddy 664064bf1c 0.1.1 2 weeks ago
Buddy 5ded37a970 docs: JSDoc every public symbol + module docs for JSR score
JSR's score docked "module docs in all entrypoints" and "docs for most
symbols" (0% documented). This adds:

- `@module` doc comments to `src/index.ts` and `src/elements/index.ts`
- JSDoc on every top-level exported symbol reachable from those entrypoints:
  all `props.ts` types + codec factories, the `WithProps` mixin and its
  types, the `register.ts` events and `emit*` helpers, the `shared-props.ts`
  fragments and structural interfaces, every `event-types.ts` family, and all
  25 `Leaflet*Element` classes
- field-level docs on the `PropDef` and `LeafletElement` interfaces

Existing `//` explanations were converted in place; no behaviour change.
`jsr publish --dry-run` still clean; typecheck / lint / test / build pass.
2 weeks ago
Buddy 829f78fd8d chore: update stale package-name reference in test comment 2 weeks ago

@ -1,6 +1,6 @@
{ {
"name": "@buddy/leaflet-components", "name": "@buddy/leaflet-components",
"version": "0.1.0", "version": "0.1.1",
"license": "MIT", "license": "MIT",
"exports": { "exports": {
".": "./src/index.ts", ".": "./src/index.ts",

4
package-lock.json generated

@ -1,12 +1,12 @@
{ {
"name": "leaflet-web-components", "name": "leaflet-web-components",
"version": "0.1.0", "version": "0.1.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "leaflet-web-components", "name": "leaflet-web-components",
"version": "0.1.0", "version": "0.1.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",

@ -1,6 +1,6 @@
{ {
"name": "leaflet-web-components", "name": "leaflet-web-components",
"version": "0.1.0", "version": "0.1.1",
"description": "LeafletJS as Web Components", "description": "LeafletJS as Web Components",
"main": "dist/index.npm.js", "main": "dist/index.npm.js",
"module": "dist/index.npm.js", "module": "dist/index.npm.js",

@ -27,17 +27,20 @@ import type {
ZoomAnimEvent, ZoomAnimEvent,
} from 'leaflet'; } from 'leaflet';
/** `movestart` / `move` / `moveend`. */
export interface MoveEvents { export interface MoveEvents {
movestart: LeafletEvent; movestart: LeafletEvent;
move: LeafletEvent; move: LeafletEvent;
moveend: LeafletEvent; moveend: LeafletEvent;
} }
/** A layer's own `add` / `remove` (added to / removed from a map). */
export interface LayerAddRemoveEvents { export interface LayerAddRemoveEvents {
add: LeafletEvent; add: LeafletEvent;
remove: LeafletEvent; remove: LeafletEvent;
} }
/** `click` / `dblclick` / `mousedown` / `mouseup` / `mouseover` / `mouseout` / `contextmenu`. */
export interface MouseEvents { export interface MouseEvents {
click: LeafletMouseEvent; click: LeafletMouseEvent;
dblclick: LeafletMouseEvent; dblclick: LeafletMouseEvent;
@ -48,22 +51,26 @@ export interface MouseEvents {
contextmenu: LeafletMouseEvent; contextmenu: LeafletMouseEvent;
} }
/** `popupopen` / `popupclose`, fired on the layer a popup is bound to. */
export interface PopupBindEvents { export interface PopupBindEvents {
popupopen: PopupEvent; popupopen: PopupEvent;
popupclose: PopupEvent; popupclose: PopupEvent;
} }
/** `tooltipopen` / `tooltipclose`, fired on the layer a tooltip is bound to. */
export interface TooltipBindEvents { export interface TooltipBindEvents {
tooltipopen: TooltipEvent; tooltipopen: TooltipEvent;
tooltipclose: TooltipEvent; tooltipclose: TooltipEvent;
} }
/** `dragstart` / `drag` / `dragend` (marker dragging). */
export interface DragEvents { export interface DragEvents {
dragstart: LeafletEvent; dragstart: LeafletEvent;
drag: LeafletEvent; drag: LeafletEvent;
dragend: DragEndEvent; dragend: DragEndEvent;
} }
/** Tile-loading lifecycle: `loading` / `load` / `tileloadstart` / `tileload` / `tileunload` / `tileerror`. */
export interface TileEvents { export interface TileEvents {
loading: LeafletEvent; loading: LeafletEvent;
load: LeafletEvent; load: LeafletEvent;
@ -73,34 +80,40 @@ export interface TileEvents {
tileerror: TileErrorEvent; tileerror: TileErrorEvent;
} }
/** `contentupdate`, fired by a popup/tooltip when its content changes. */
export interface DivOverlayEvents { export interface DivOverlayEvents {
contentupdate: LeafletEvent; contentupdate: LeafletEvent;
} }
/** `layeradd` / `layerremove`, fired by a group about its children. */
export interface LayerGroupEvents { export interface LayerGroupEvents {
layeradd: LayerEvent; layeradd: LayerEvent;
layerremove: LayerEvent; layerremove: LayerEvent;
} }
// Every non-group layer (marker, path, overlay, tile layer...) can have a /** Events every non-group layer has: its own `add`/`remove` plus popup/tooltip bind events. */
// popup/tooltip bound to it regardless of its more specific family.
export type BaseLayerEvents = LayerAddRemoveEvents & PopupBindEvents & TooltipBindEvents; export type BaseLayerEvents = LayerAddRemoveEvents & PopupBindEvents & TooltipBindEvents;
/** Event map for `<leaflet-circle|polygon|polyline|rectangle>`. */
export type PathEvents = BaseLayerEvents & MouseEvents; export type PathEvents = BaseLayerEvents & MouseEvents;
/** Event map for `<leaflet-marker>`. */
export type MarkerEvents = BaseLayerEvents & MouseEvents & MoveEvents & DragEvents; export type MarkerEvents = BaseLayerEvents & MouseEvents & MoveEvents & DragEvents;
/** Event map for `<leaflet-tile-layer>` / `<leaflet-tile-layer-wms>`. */
export type TileLayerEvents = BaseLayerEvents & TileEvents; export type TileLayerEvents = BaseLayerEvents & TileEvents;
// Popup/Tooltip themselves don't fire popupopen/tooltipopen about /**
// themselves -- that fires on whatever they're bound to -- so this is * Event map for `<leaflet-popup>` / `<leaflet-tooltip>`. Uses
// LayerAddRemoveEvents rather than the fuller BaseLayerEvents. * {@link LayerAddRemoveEvents} rather than {@link BaseLayerEvents} a
* popup/tooltip doesn't fire `popupopen` about *itself*.
*/
export type DivOverlayLayerEvents = LayerAddRemoveEvents & MouseEvents & DivOverlayEvents; export type DivOverlayLayerEvents = LayerAddRemoveEvents & MouseEvents & DivOverlayEvents;
// LayerGroup, FeatureGroup and GeoJSON (itself a FeatureGroup) all get both /** Event map for `<leaflet-layer-group>` / `<leaflet-feature-group>` / `<leaflet-geojson>` — own add/remove plus children's `layeradd`/`layerremove`. */
// their own add/remove and their children's layeradd/layerremove.
export type GroupEvents = LayerAddRemoveEvents & LayerGroupEvents; export type GroupEvents = LayerAddRemoveEvents & LayerGroupEvents;
/** Event map for `<leaflet-map>` — move, mouse, zoom, resize, location, keyboard, layer-control and popup/tooltip events. */
export interface MapEvents export interface MapEvents
extends MoveEvents, MouseEvents, PopupBindEvents, TooltipBindEvents, LayerGroupEvents { extends MoveEvents, MouseEvents, PopupBindEvents, TooltipBindEvents, LayerGroupEvents {
zoomstart: LeafletEvent; zoomstart: LeafletEvent;

@ -1,74 +1,88 @@
// Every element property is described by a PropDef: how its value encodes to /**
// and decodes from an HTML attribute, and how it is pushed into (and read back * Describes one element property: how its value encodes to and decodes from an
// out of) the Leaflet object. `WithProps` in with-props.ts is the only consumer * HTML attribute, and how it is pushed into (and read back out of) the Leaflet
// -- components just declare a table of these and never touch the plumbing. * object. {@link WithProps} is the only consumer a component just declares a
* table of these and never touches the plumbing.
*
* @typeParam T - the decoded property value type
* @typeParam TObj - the Leaflet object type `set`/`get` operate on
*/
export interface PropDef<T = unknown, TObj = unknown> { export interface PropDef<T = unknown, TObj = unknown> {
// Attribute name. Defaults to the kebab-cased property name. A function /** Attribute name; defaults to the kebab-cased property name. A function receives the property name and returns the attribute (see {@link disabled}). */
// receives the property name and returns the attribute (see `disabled`).
attribute?: string | ((name: string) => string); attribute?: string | ((name: string) => string);
// The property value when the attribute is absent. Keep this equal to /** The value when the attribute is absent. Keep it equal to Leaflet's own default — an absent attribute is omitted from the options object entirely. */
// Leaflet's own default: an absent attribute is left out of the options
// object entirely, so it is Leaflet's default that actually takes effect.
default: T; default: T;
// Set through `positional()` for values the Leaflet constructor takes as an /** Set (to `false`) by {@link positional} for values the Leaflet constructor takes as an argument rather than an option. */
// argument (coordinates, urls, bounds) rather than as an option.
option?: false; option?: false;
/** Parse the raw attribute string into the value. */
decode(raw: string): T; decode(raw: string): T;
// Returning null removes the attribute, which restores Leaflet's default. /** Serialise the value back to an attribute string; return `null` to remove the attribute (restoring Leaflet's default). */
encode(value: T): string | null; encode(value: T): string | null;
// Pushes a new value into the Leaflet object. Defaults to calling the /** Push a new value into the live Leaflet object. Defaults to the matching setter (`opacity` → `setOpacity`) when the object has one. */
// matching setter when the object has one (`opacity` -> `setOpacity`).
set?(obj: TObj, value: T, el: HTMLElement): void; set?(obj: TObj, value: T, el: HTMLElement): void;
// Reads the live value back out of the Leaflet object. Used by the property /** Read the live value back out of the object — backs the property getter and the `event` write-back. */
// getter, and by `event` below to write the value back to the attribute.
get?(obj: TObj): T | undefined; get?(obj: TObj): T | undefined;
// Leaflet event after which `get` is re-read and synced to the attribute, /** Leaflet event after which `get` is re-read and synced to the attribute (`move` keeps lat/lng current during a drag). */
// e.g. `move` keeps lat/lng current while a marker is dragged.
event?: string; event?: string;
} }
/** A record of {@link PropDef}s keyed by property name — an element's `PROPS` table. */
export type PropTable<T> = Record<string, PropDef<unknown, T>>; export type PropTable<T> = Record<string, PropDef<unknown, T>>;
// Everything a codec factory doesn't fill in for you. `option` is not here: /**
// it has to come from `positional()` to be visible in PropOptionValues. * The optional half of a {@link PropDef} everything a codec factory doesn't
* fill in for you. `option` is excluded: it must come from {@link positional}
* to stay visible to {@link PropOptionValues}.
*/
export type PropOptions<TObj, T> = Partial< export type PropOptions<TObj, T> = Partial<
Omit<PropDef<T, TObj>, 'default' | 'decode' | 'encode' | 'option'> Omit<PropDef<T, TObj>, 'default' | 'decode' | 'encode' | 'option'>
>; >;
// The value type of a single prop, and of a whole table. /** The decoded value type of a single prop, read off its `default`. */
export type PropValue<P> = P extends { default: infer T } ? T : never; export type PropValue<P> = P extends { default: infer T } ? T : never;
/** {@link PropValue} mapped over a whole {@link PropTable} — the element's property shape. */
export type PropValues<T> = { [K in keyof T]: PropValue<T[K]> }; export type PropValues<T> = { [K in keyof T]: PropValue<T[K]> };
// The options object handed to `createLeafletObject`. Partial because a prop /**
// only appears when its attribute is present; `option: false` props never do. * The options object handed to `createLeafletObject`. `Partial` because a prop
* only appears when its attribute is present, and `positional()` props
* (`option: false`) are dropped entirely.
*/
export type PropOptionValues<T> = Partial<{ export type PropOptionValues<T> = Partial<{
[K in keyof T as T[K] extends { option: false } ? never : K]: PropValue<T[K]>; [K in keyof T as T[K] extends { option: false } ? never : K]: PropValue<T[K]>;
}>; }>;
// The type `positional()` produces: a PropDef flagged so #buildOptions skips /**
// it. Spelled out as an alias so element PROPS tables can be given the explicit * The type {@link positional} produces: a {@link PropDef} flagged so
// type annotations JSR's "no slow types" check requires without repeating the * `#buildOptions` skips it. Spelled out as an alias so element `PROPS` tables
// intersection everywhere. * can carry the explicit annotations JSR's "no slow types" check requires
* without repeating the intersection everywhere.
*/
export type Positional<T, TObj = unknown> = PropDef<T, TObj> & { option: false }; export type Positional<T, TObj = unknown> = PropDef<T, TObj> & { option: false };
// Marks a prop the Leaflet constructor takes as an argument, so it is left out /**
// of the options object handed to createLeafletObject(). * Marks a prop the Leaflet constructor takes as a positional argument
* (coordinates, url, bounds), so it is left out of the options object handed to
* `createLeafletObject`.
*/
export function positional<T extends PropDef>(def: T): T & { option: false } { export function positional<T extends PropDef>(def: T): T & { option: false } {
return { ...def, option: false }; return { ...def, option: false };
} }
/** `fooBar` → `foo-bar`. The default attribute name for a property. */
export function kebab(name: string): string { export function kebab(name: string): string {
return name.replaceAll(/[A-Z]/gu, (c) => `-${c.toLowerCase()}`); return name.replaceAll(/[A-Z]/gu, (c) => `-${c.toLowerCase()}`);
} }
/** A numeric attribute: `Number` on the way in, `String` on the way out. */
export function num<TObj = unknown>( export function num<TObj = unknown>(
def = 0, def = 0,
opts?: PropOptions<TObj, number>, opts?: PropOptions<TObj, number>,
@ -76,6 +90,7 @@ export function num<TObj = unknown>(
return { default: def, decode: Number, encode: String, ...opts }; return { default: def, decode: Number, encode: String, ...opts };
} }
/** A string attribute — identity codec both ways. */
export function str<TObj = unknown>( export function str<TObj = unknown>(
def = '', def = '',
opts?: PropOptions<TObj, string>, opts?: PropOptions<TObj, string>,
@ -83,9 +98,11 @@ export function str<TObj = unknown>(
return { default: def, decode: (raw) => raw, encode: (value) => value, ...opts }; return { default: def, decode: (raw) => raw, encode: (value) => value, ...opts };
} }
// A string attribute whose values Leaflet types as a union -- ControlPosition, /**
// CrossOrigin, tooltip Direction. Nothing is validated at runtime; this is how * A string attribute whose values Leaflet types as a union (`ControlPosition`,
// the options object comes out with the type Leaflet's constructor expects. * `CrossOrigin`, tooltip `Direction`). Not validated at runtime this just
* makes the options object come out with the type Leaflet's constructor wants.
*/
export function choice<T extends string, TObj = unknown>( export function choice<T extends string, TObj = unknown>(
def: T, def: T,
opts?: PropOptions<TObj, T>, opts?: PropOptions<TObj, T>,
@ -93,9 +110,11 @@ export function choice<T extends string, TObj = unknown>(
return { default: def, decode: (raw) => raw as T, encode: (value) => value, ...opts }; return { default: def, decode: (raw) => raw as T, encode: (value) => value, ...opts };
} }
// A boolean attribute: present is true, `="false"` is false, absent is `def`. /**
// Use `bool(true)` for options Leaflet already defaults to true, so that * A boolean attribute: present is `true`, `="false"` is `false`, absent is
// `<leaflet-popup auto-pan="false">` can turn them off. * `def`. Use `bool(true)` for options Leaflet already defaults on, so
* `<leaflet-popup auto-pan="false">` can turn them off.
*/
export function bool<TObj = unknown>( export function bool<TObj = unknown>(
def = false, def = false,
opts?: PropOptions<TObj, boolean>, opts?: PropOptions<TObj, boolean>,
@ -108,8 +127,11 @@ export function bool<TObj = unknown>(
}; };
} }
// The inverse of `bool(true)`: `<leaflet-map disable-dragging>` reads as /**
// `dragging === false`. Named `disable-<kebab>` unless `attribute` says else. * The inverse of `bool(true)`: `<leaflet-map disable-dragging>` reads as
* `dragging === false`. Attribute is named `disable-<kebab>` unless `attribute`
* overrides it.
*/
export function disabled<TObj = unknown>( export function disabled<TObj = unknown>(
opts?: PropOptions<TObj, boolean>, opts?: PropOptions<TObj, boolean>,
): PropDef<boolean, TObj> { ): PropDef<boolean, TObj> {
@ -122,7 +144,7 @@ export function disabled<TObj = unknown>(
}; };
} }
// For attributes holding JSON: bounds, icon sizes and anchors, GeoJSON data. /** A JSON attribute (`JSON.parse` / `JSON.stringify`): bounds, icon sizes and anchors, GeoJSON data. */
export function json<T, TObj = unknown>(def: T, opts?: PropOptions<TObj, T>): PropDef<T, TObj> { export function json<T, TObj = unknown>(def: T, opts?: PropOptions<TObj, T>): PropDef<T, TObj> {
return { return {
default: def, default: def,

@ -1,39 +1,46 @@
import { DivIcon, Icon, Layer, type CRS } from 'leaflet'; import { DivIcon, Icon, Layer, type CRS } from 'leaflet';
// Custom event type for the bubbling registration protocol. Carries the /**
// Leaflet object and the originating element so the nearest parent can * The bubbling `leaflet-register` event. Carries the Leaflet object and the
// add it as a child layer, popup, or tooltip. * originating element so the nearest ancestor component can add it as a child
* layer, bind it as a popup, or bind it as a tooltip.
*/
export type LeafletRegisterEvent = CustomEvent<{ export type LeafletRegisterEvent = CustomEvent<{
leafletObject: Layer; leafletObject: Layer;
element: HTMLElement; element: HTMLElement;
}>; }>;
/** `leaflet-add-layer` / `leaflet-remove-layer` — dispatched by `<leaflet-control-layers>` to toggle a layer on the map. */
export type LeafletLayerEvent = CustomEvent<{ layer: Layer }>; export type LeafletLayerEvent = CustomEvent<{ layer: Layer }>;
/** `icon-changed` — a `<leaflet-icon>` / `<leaflet-div-icon>` child announcing its icon (or `null` on disconnect) to its parent marker. */
export type LeafletIconChangedEvent = CustomEvent<{ icon: Icon | null }>; export type LeafletIconChangedEvent = CustomEvent<{ icon: Icon | null }>;
// Fired by <leaflet-line> on itself -- on connect and on every lat/lng /**
// change -- carrying its own current position. polygon/polyline listen for * `leaflet-line-sync` fired by `<leaflet-line>` on itself, on connect and on
// this (bubbling) to track vertices without ever reading a child's state * every lat/lng change, carrying its own position. `<leaflet-polygon>` /
// directly. * `<leaflet-polyline>` listen for it (bubbling) to track vertices without ever
* reading a child's state.
*/
export type LeafletLineSyncEvent = CustomEvent<{ element: HTMLElement; latlng: [number, number] }>; export type LeafletLineSyncEvent = CustomEvent<{ element: HTMLElement; latlng: [number, number] }>;
// Fired by <leaflet-line> on disconnect, so a listening parent can drop it. /** `leaflet-line-remove` — fired by `<leaflet-line>` on disconnect so a listening parent can drop that vertex. */
export type LeafletLineRemoveEvent = CustomEvent<{ element: HTMLElement }>; export type LeafletLineRemoveEvent = CustomEvent<{ element: HTMLElement }>;
// A CRS is a plain value (methods + a couple of properties, see Leaflet's /**
// own `CRS` interface), not a Layer -- it doesn't fit the leaflet-register * `leaflet-crs-changed` a CRS is a plain value, not a `Layer`, so it doesn't
// protocol above. Any custom element nested inside a component that accepts * fit the `leaflet-register` protocol. A custom element nested inside a
// a `crs` (currently just leaflet-tile-layer-wms) can provide one by firing * component that accepts a `crs` (currently `<leaflet-tile-layer-wms>`)
// this itself, bubbling, on connect -- no base class required, just this * provides one by firing this itself, bubbling, on connect. `crs: null`
// event shape. `crs: null` (e.g. on disconnect) reverts to that component's * reverts to the component's own default.
// own default, mirroring icon-changed's `icon: null`. */
export type LeafletCRSChangedEvent = CustomEvent<{ crs: CRS | null }>; export type LeafletCRSChangedEvent = CustomEvent<{ crs: CRS | null }>;
// The `HTMLElementEventMap` augmentation for these event names lives in // The `HTMLElementEventMap` augmentation for these event names lives in
// `./globals.ts` (npm-only), kept out of this module so JSR -- which rejects // `./globals.ts` (npm-only), kept out of this module so JSR -- which rejects
// `declare global` in its published graph -- can still publish it. // `declare global` in its published graph -- can still publish it.
/** Fire {@link LeafletIconChangedEvent} (`icon-changed`, bubbling) from `el`. `null` clears the icon. */
export function emitIconChanged(el: HTMLElement, icon: Icon | DivIcon | null | undefined) { export function emitIconChanged(el: HTMLElement, icon: Icon | DivIcon | null | undefined) {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('icon-changed', { new CustomEvent('icon-changed', {
@ -43,28 +50,31 @@ export function emitIconChanged(el: HTMLElement, icon: Icon | DivIcon | null | u
); );
} }
/** Fire {@link LeafletLineSyncEvent} (`leaflet-line-sync`, bubbling) from `el` with its current position. */
export function emitLineSync(el: HTMLElement, latlng: [number, number]): void { export function emitLineSync(el: HTMLElement, latlng: [number, number]): void {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('leaflet-line-sync', { bubbles: true, detail: { element: el, latlng } }), new CustomEvent('leaflet-line-sync', { bubbles: true, detail: { element: el, latlng } }),
); );
} }
// Dispatched on `from`, not `el`: by the time disconnectedCallback runs, `el` /**
// has already been detached from its parent, so a bubbling dispatch from * Fire {@link LeafletLineRemoveEvent} (`leaflet-line-remove`, bubbling).
// `el` itself would have nowhere to bubble to. Callers pass the parent they * Dispatched on `from`, not `el`: by `disconnectedCallback` time `el` is
// cached while still connected. * already detached, so a dispatch from it has nowhere to bubble callers pass
* the parent they cached while still connected.
*/
export function emitLineRemove(from: EventTarget, el: HTMLElement): void { export function emitLineRemove(from: EventTarget, el: HTMLElement): void {
from.dispatchEvent( from.dispatchEvent(
new CustomEvent('leaflet-line-remove', { bubbles: true, detail: { element: el } }), new CustomEvent('leaflet-line-remove', { bubbles: true, detail: { element: el } }),
); );
} }
// Dispatches a custom `leaflet-register` event upward through the DOM /**
// tree, carrying a Leaflet object and its host element. Parent components * Fire {@link LeafletRegisterEvent} (`leaflet-register`, bubbling and
// (map, circles, groups, etc.) intercept this event and add the layer, * `composed`) from `el`. The core wiring mechanism: an ancestor component
// bind the popup, or bind the tooltip. This is the core wiring mechanism * intercepts it and adds the layer / binds the popup / binds the tooltip,
// that replaces the parent-child relationship that Leaflet normally * replacing the imperative parent-child calls Leaflet normally needs.
// manages via imperative code. */
export function registerWithParent(el: HTMLElement, obj: unknown) { export function registerWithParent(el: HTMLElement, obj: unknown) {
el.dispatchEvent( el.dispatchEvent(
new CustomEvent('leaflet-register', { new CustomEvent('leaflet-register', {

@ -13,26 +13,33 @@ import { bool, choice, json, num, positional, str, type PropDef } from './props.
// Leaflet classes share no common interface, so the prop fragments below // Leaflet classes share no common interface, so the prop fragments below
// describe structurally what they need from the object they update. // describe structurally what they need from the object they update.
/** Structural type for anything a `latLngProps` prop updates — a marker, circle, popup, tooltip. */
export interface Positioned { export interface Positioned {
getLatLng(): LatLng | undefined; getLatLng(): LatLng | undefined;
setLatLng(latlng: [number, number]): unknown; setLatLng(latlng: [number, number]): unknown;
} }
/** Structural type for anything a `pathProps` style prop updates — any Leaflet `Path`. */
export interface Styleable { export interface Styleable {
setStyle(style: PathOptions): unknown; setStyle(style: PathOptions): unknown;
} }
/** Structural type for anything {@link urlProp} updates — `TileLayer`, `ImageOverlay`, `VideoOverlay`. */
export interface Sourced { export interface Sourced {
setUrl(url: string): unknown; setUrl(url: string): unknown;
} }
/** Structural type for anything {@link getBounds} reads — `ImageOverlay`, `Rectangle`, etc. */
export interface Bounded { export interface Bounded {
getBounds(): LatLngBounds; getBounds(): LatLngBounds;
} }
// Shared `get` for any `bounds` prop backed by Leaflet's getBounds(). Returns /**
// a plain [[south, west], [north, east]] pair rather than the LatLngBounds * Shared `get` for any `bounds` prop, backed by Leaflet's `getBounds()`.
// instance, matching the JSON-encoded shape the attribute round-trips through. * Returns a plain `[[south, west], [north, east]]` pair (not the `LatLngBounds`
* instance), matching the JSON shape the attribute round-trips through.
*/
export function getBounds(obj: Bounded): LatLngBoundsExpression { export function getBounds(obj: Bounded): LatLngBoundsExpression {
const b = obj.getBounds(); const b = obj.getBounds();
return [ return [
@ -46,16 +53,18 @@ interface PositionedHost extends HTMLElement {
lng: number; lng: number;
} }
// A `set` for any option Leaflet only exposes through setStyle(). /** Builds a `set` for a style option Leaflet only exposes through `setStyle()`. */
export function style<T>(key: keyof PathOptions): (obj: Styleable, value: T) => void { export function style<T>(key: keyof PathOptions): (obj: Styleable, value: T) => void {
return (obj, value) => { return (obj, value) => {
obj.setStyle({ [key]: value } as PathOptions); obj.setStyle({ [key]: value } as PathOptions);
}; };
} }
// The source url. Passed positionally by every Leaflet constructor that takes /**
// one, and ignored when blank so clearing the attribute can't request nothing. * The `url` prop the positional source URL for `TileLayer` / `ImageOverlay` /
// No `get`: none of TileLayer/ImageOverlay/VideoOverlay expose a getUrl(). * `VideoOverlay`. Ignored when blank so clearing the attribute can't request an
* empty URL. No `get` (none of those classes expose `getUrl()`).
*/
export const urlProp: PropDef<string, Sourced> & { option: false } = positional( export const urlProp: PropDef<string, Sourced> & { option: false } = positional(
str<Sourced>('', { str<Sourced>('', {
set(obj, value) { set(obj, value) {
@ -64,11 +73,13 @@ export const urlProp: PropDef<string, Sourced> & { option: false } = positional(
}), }),
); );
// lat/lng travel together: both are passed positionally to the Leaflet /**
// constructor rather than as options, setting either re-issues setLatLng with * The `lat` / `lng` prop pair. Both are positional (passed to the Leaflet
// the other's current value, and both are written back whenever the object * constructor); setting either re-issues `setLatLng` with the other axis read
// moves -- which is what keeps the attributes current while a marker is * off the host element, and both write back on `move` which keeps the
// dragged. Shared by marker, circle, circle-marker, popup and tooltip. * attributes live while a marker is dragged. Shared by marker, circle,
* circle-marker, popup and tooltip.
*/
export const latLngProps: { export const latLngProps: {
lat: PropDef<number, Positioned> & { option: false }; lat: PropDef<number, Positioned> & { option: false };
lng: PropDef<number, Positioned> & { option: false }; lng: PropDef<number, Positioned> & { option: false };
@ -93,8 +104,12 @@ export const latLngProps: {
), ),
}; };
// The style options every Path accepts. Defaults match Leaflet's own, so an /**
// absent attribute and an unset option mean the same thing. * Every SVG style option a Leaflet `Path` accepts. The mutable ones (`color`,
* `weight`, `opacity`, `fill*`, ) route through `setStyle()` via {@link style};
* the constructor-only tail (`className`, `interactive`, `pane`, ) has no
* setter. Defaults match Leaflet's own.
*/
export const pathProps: { export const pathProps: {
stroke: PropDef<boolean, Styleable>; stroke: PropDef<boolean, Styleable>;
color: PropDef<string, Styleable>; color: PropDef<string, Styleable>;
@ -133,11 +148,13 @@ export const pathProps: {
pane: str('overlay'), pane: str('overlay'),
}; };
// The GridLayer/TileLayer options every tile source accepts, shared by /**
// leaflet-tile-layer and leaflet-tile-layer-wms (WMSOptions extends * The `GridLayer` / `TileLayer` options common to `<leaflet-tile-layer>` and
// TileLayerOptions). Most are constructor-only -- Leaflet exposes no setter * `<leaflet-tile-layer-wms>` (WMS options extend tile-layer options). Almost
// for them -- so changing the attribute after creation has no effect, same as * all constructor-only. `referrerPolicy` is a hand-written {@link PropDef}
// leaflet-map's zoomSnap. * rather than {@link choice} because Leaflet's `ReferrerPolicy` type has no
* "unset" member.
*/
export const tileLayerProps: { export const tileLayerProps: {
attribution: PropDef<string>; attribution: PropDef<string>;
minZoom: PropDef<number>; minZoom: PropDef<number>;

@ -8,56 +8,78 @@ import {
} from './props.ts'; } from './props.ts';
import { registerWithParent, type LeafletRegisterEvent } from './register.ts'; import { registerWithParent, type LeafletRegisterEvent } from './register.ts';
// How an element joins the component tree: /**
// children -- register with the nearest parent and adopt registering * How an element joins the component tree:
// descendants as layers, popups and tooltips (every layer) *
// self -- register with the nearest parent only (popups, tooltips, * - `children` register with the nearest parent and adopt registering
// controls: they have a parent but manage no children here) * descendants as layers, popups and tooltips (every layer type).
// none -- neither (the map is the root; icons aren't part of the tree) * - `self` register with the nearest parent only (popups, tooltips, controls:
* they have a parent but manage no children here).
* - `none` neither (the map is the tree root; icons aren't tree members).
*/
export type Attach = 'children' | 'self' | 'none'; export type Attach = 'children' | 'self' | 'none';
/** Second argument to {@link WithProps}. */
export interface ElementOptions { export interface ElementOptions {
/** Tree-membership mode; defaults to `'children'`. See {@link Attach}. */
attach?: Attach; attach?: Attach;
// Rebuild the Leaflet object on every attribute change instead of calling /**
// setters, for objects Leaflet gives us no way to mutate in place (icons). * Rebuild the Leaflet object on every attribute change instead of calling
* setters for objects Leaflet gives no way to mutate in place (icons).
*/
recreate?: boolean; recreate?: boolean;
} }
// The members WithProps contributes on top of the property accessors. /** The members {@link WithProps} contributes on top of the generated property accessors. */
export interface LeafletElement<TObj, TProps> { export interface LeafletElement<TObj, TProps> {
/** The wrapped Leaflet object, once created (undefined before connect / after disconnect). */
readonly leafletObject?: TObj; readonly leafletObject?: TObj;
// The one method every component must implement. `options` holds the decoded /**
// value of every prop whose attribute is present, keyed by property name, so * The one method every component must implement. `options` holds the decoded
// it can be handed straight to the Leaflet constructor. * value of every prop whose attribute is present, keyed by property name,
* ready to hand to the Leaflet constructor.
*/
createLeafletObject(options: PropOptionValues<TProps>): TObj | undefined; createLeafletObject(options: PropOptionValues<TProps>): TObj | undefined;
// Called after the object is created and after every recreate. /** Hook called after the object is created and after every recreate; override to react. */
leafletObjectCreated(): void; leafletObjectCreated(): void;
/** Tear the object down and rebuild it from current attributes (used by `recreate` and by WMS CRS children). */
recreateLeafletObject(): void; recreateLeafletObject(): void;
/** Custom-element lifecycle: builds the object and joins the tree. Call `super.connectedCallback()` if you override. */
connectedCallback(): void; connectedCallback(): void;
/** Custom-element lifecycle: leaves the tree and destroys the object. Call `super.disconnectedCallback()` if you override. */
disconnectedCallback(): void; disconnectedCallback(): void;
/** Custom-element lifecycle: pushes an attribute change into the live object. */
attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void; attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
} }
/**
* The constructor type {@link WithProps} returns: an `HTMLElement` subclass with
* a two-way accessor per prop ({@link PropValues}) plus the
* {@link LeafletElement} lifecycle members. Element files annotate their
* `const Base` with this.
*/
export type LeafletElementConstructor<TObj, TProps> = new () => HTMLElement & export type LeafletElementConstructor<TObj, TProps> = new () => HTMLElement &
PropValues<TProps> & PropValues<TProps> &
LeafletElement<TObj, TProps>; LeafletElement<TObj, TProps>;
// Type-only narrowing for addEventListener/removeEventListener so a /**
// component's `leaflet:<name>` events (see #forwardEvents below) type-check * Type-only narrowing for `addEventListener` so a component's `leaflet:<name>`
// against the right Leaflet event payload. Like `declare readonly * events (see `#forwardEvents`) type-check against the right Leaflet payload. A
// leafletObject?: Marker`, a component applies this with a `declare` field -- * component applies it with a `declare addEventListener: LeafletAddEventListener<TheEvents>`
// TObj isn't reliably inferred from the PROPS table alone, so this can't live * field the same zero-runtime idiom as `declare readonly leafletObject?: X`.
// in WithProps()'s own generics; it costs nothing at runtime either way. *
// * Deliberately has **no** generic `(type: string, …)` fallback overload (unlike
// Deliberately has no generic `(type: string, ...)` fallback overload (unlike * the real DOM API): a fallback would silently accept any misspelled
// HTMLElement's real addEventListener): a fallback would silently accept any * `leaflet:*` name. The cost is that a genuinely dynamic event-name string
// unrecognized `leaflet:*` name too, which defeats the point. The cost is * needs a cast.
// that a genuinely dynamic (non-literal) event name string needs a cast. *
* @typeParam TEvents - an event-name payload map from `event-types.ts`
*/
export type LeafletAddEventListener<TEvents> = (<K extends keyof TEvents & string>( export type LeafletAddEventListener<TEvents> = (<K extends keyof TEvents & string>(
type: `leaflet:${K}`, type: `leaflet:${K}`,
listener: (ev: CustomEvent<TEvents[K]>) => void, listener: (ev: CustomEvent<TEvents[K]>) => void,
@ -69,6 +91,7 @@ export type LeafletAddEventListener<TEvents> = (<K extends keyof TEvents & strin
options?: boolean | AddEventListenerOptions, options?: boolean | AddEventListenerOptions,
) => void); ) => void);
/** The `removeEventListener` counterpart of {@link LeafletAddEventListener}. */
export type LeafletRemoveEventListener<TEvents> = (<K extends keyof TEvents & string>( export type LeafletRemoveEventListener<TEvents> = (<K extends keyof TEvents & string>(
type: `leaflet:${K}`, type: `leaflet:${K}`,
listener: (ev: CustomEvent<TEvents[K]>) => void, listener: (ev: CustomEvent<TEvents[K]>) => void,
@ -113,13 +136,26 @@ function resolve<TObj extends Class>(props: PropTable<TObj>): ResolvedProp<TObj>
})); }));
} }
// Builds a custom element base class from a table of property definitions. /**
// * Mixin factory: builds a custom-element base class from a table of
// The generated class owns the whole lifecycle: it derives observedAttributes, * {@link PropDef}s. Always extends `HTMLElement` internally (there is no
// defines two-way property accessors, builds the Leaflet options object, wires * base-class parameter).
// the object into the component tree, keeps attributes in sync with the object *
// (in both directions, without cycles), and re-fires every Leaflet event on the * The generated class owns the whole lifecycle it derives
// element as `leaflet:<type>`. Subclasses implement createLeafletObject(). * `observedAttributes`, defines a two-way property accessor per prop, builds
* the Leaflet options object on connect, wires the object into the component
* tree, keeps attributes and the live object in sync (both directions, no
* cycles), and re-fires every Leaflet event on the element as `leaflet:<type>`.
* A subclass only implements `createLeafletObject()`.
*
* Element files must not write `class Foo extends WithProps({…})` JSR's
* type checker rejects a call expression as a superclass. Use a
* `const Base: LeafletElementConstructor<TheClass, typeof PROPS> = WithProps(PROPS)`
* and `extends Base` instead.
*
* @param props - the `PROPS` table (needs an explicit type annotation)
* @param options - tree-attach mode and `recreate` behaviour; see {@link ElementOptions}
*/
export function WithProps<TObj extends Class, TProps extends PropTable<TObj>>( export function WithProps<TObj extends Class, TProps extends PropTable<TObj>>(
props: TProps, props: TProps,
options: ElementOptions = {}, options: ElementOptions = {},

@ -1,11 +1,20 @@
// The element classes on their own -- no `customElements.define()` runs from /**
// anything in this file or the modules it re-exports. Import a `components/*` * Every `leaflet-*` element class, by name, with **no** `customElements.define()`
// module (or `src/index.ts`) instead when you want the tags actually * side effect nothing in this module or the modules it re-exports registers a
// registered; reach for these when you want to subclass an element, register * tag.
// it under a different tag name, or otherwise customise before defining. *
// * Reach for this entrypoint (`leaflet-web-components/elements`, or
// Every class is the `default` export of its own module, surfaced here under * `jsr:@buddy/leaflet-components/elements`) when you want to subclass an
// a name. Order is irrelevant -- nothing here has a load-time side effect. * element, register it under a different tag name, or otherwise customise
* before defining. To get the tags registered instead, import the package root
* or an individual `components/*` module.
*
* Each class is the `default` export of its own `leaflet-foo.ts` module,
* surfaced here under a `LeafletFooElement` name. Order is irrelevant none of
* these modules has a load-time side effect.
*
* @module
*/
export { default as LeafletMapElement } from './leaflet-map.ts'; export { default as LeafletMapElement } from './leaflet-map.ts';
export { default as LeafletControlLayersElement } from './leaflet-control-layers.ts'; export { default as LeafletControlLayersElement } from './leaflet-control-layers.ts';
export { default as LeafletLayerGroupElement } from './leaflet-layer-group.ts'; export { default as LeafletLayerGroupElement } from './leaflet-layer-group.ts';

@ -19,6 +19,7 @@ const PROPS: typeof latLngProps &
}; };
const Base: LeafletElementConstructor<CircleMarker, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<CircleMarker, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-circle-marker>` — a Leaflet `CircleMarker` (radius in pixels, fixed screen size). */
export default class LeafletCircleMarkerElement extends Base { export default class LeafletCircleMarkerElement extends Base {
declare readonly leafletObject?: CircleMarker; declare readonly leafletObject?: CircleMarker;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -21,6 +21,7 @@ const PROPS = {
} as const; } as const;
const Base: LeafletElementConstructor<Circle, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<Circle, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-circle>` — a Leaflet `Circle` (radius in metres). Takes `pathProps` styling and a `radius`. */
export default class LeafletCircleElement extends Base { export default class LeafletCircleElement extends Base {
declare readonly leafletObject?: Circle; declare readonly leafletObject?: Circle;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -13,6 +13,7 @@ const Base: LeafletElementConstructor<Control.Attribution, typeof PROPS> = WithP
attach: 'self', attach: 'self',
}); });
/** `<leaflet-control-attribution>` — the map's attribution control (`Control.Attribution`). */
export default class LeafletControlAttributionElement extends Base { export default class LeafletControlAttributionElement extends Base {
declare readonly leafletObject?: Control.Attribution; declare readonly leafletObject?: Control.Attribution;

@ -20,6 +20,11 @@ const Base: LeafletElementConstructor<Control.Layers, typeof PROPS> = WithProps(
attach: 'self', attach: 'self',
}); });
/**
* `<leaflet-control-layers>` a layer switcher (`Control.Layers`). Layer
* children register as base layers or overlays (`type="base"`, `active` to
* start visible) instead of being added to the map directly.
*/
export default class LeafletControlLayersElement extends Base { export default class LeafletControlLayersElement extends Base {
declare readonly leafletObject?: Control.Layers; declare readonly leafletObject?: Control.Layers;

@ -19,6 +19,7 @@ const Base: LeafletElementConstructor<Control.Scale, typeof PROPS> = WithProps(P
attach: 'self', attach: 'self',
}); });
/** `<leaflet-control-scale>` — the metric/imperial scale bar (`Control.Scale`). */
export default class LeafletControlScaleElement extends Base { export default class LeafletControlScaleElement extends Base {
declare readonly leafletObject?: Control.Scale; declare readonly leafletObject?: Control.Scale;

@ -21,6 +21,7 @@ const Base: LeafletElementConstructor<Control.Zoom, typeof PROPS> = WithProps(PR
attach: 'self', attach: 'self',
}); });
/** `<leaflet-control-zoom>` — the +/ zoom buttons (`Control.Zoom`). */
export default class LeafletControlZoomElement extends Base { export default class LeafletControlZoomElement extends Base {
declare readonly leafletObject?: Control.Zoom; declare readonly leafletObject?: Control.Zoom;

@ -27,6 +27,11 @@ const Base: LeafletElementConstructor<DivIcon, typeof PROPS> = WithProps(PROPS,
recreate: true, recreate: true,
}); });
/**
* `<leaflet-div-icon>` a Leaflet `DivIcon` (a CSS-styled marker icon). Child
* of `<leaflet-marker>`; renders its own markup when `html` isn't set, and is
* rebuilt on every change.
*/
export default class LeafletDivIconElement extends Base { export default class LeafletDivIconElement extends Base {
declare readonly leafletObject?: DivIcon; declare readonly leafletObject?: DivIcon;

@ -10,7 +10,7 @@ import type { GroupEvents } from '../core/event-types.ts';
const PROPS: Record<never, never> = {}; const PROPS: Record<never, never> = {};
const Base: LeafletElementConstructor<FeatureGroup, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<FeatureGroup, typeof PROPS> = WithProps(PROPS);
// Like leaflet-layer-group, but its children share events and a bounding box. /** `<leaflet-feature-group>` — a Leaflet `FeatureGroup`. Like `<leaflet-layer-group>`, but its children also share events and a bounding box. */
export default class LeafletFeatureGroupElement extends Base { export default class LeafletFeatureGroupElement extends Base {
declare readonly leafletObject?: FeatureGroup; declare readonly leafletObject?: FeatureGroup;
declare addEventListener: LeafletAddEventListener<GroupEvents>; declare addEventListener: LeafletAddEventListener<GroupEvents>;

@ -25,6 +25,7 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<GeoJSON, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<GeoJSON, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-geojson>` — a Leaflet `GeoJSON` layer. `data` holds the GeoJSON; other attributes style every feature. */
export default class LeafletGeoJSONElement extends Base { export default class LeafletGeoJSONElement extends Base {
declare readonly leafletObject?: GeoJSON; declare readonly leafletObject?: GeoJSON;
declare addEventListener: LeafletAddEventListener<GroupEvents>; declare addEventListener: LeafletAddEventListener<GroupEvents>;

@ -36,6 +36,7 @@ const Base: LeafletElementConstructor<Icon, typeof PROPS> = WithProps(PROPS, {
recreate: true, recreate: true,
}); });
/** `<leaflet-icon>` — a Leaflet `Icon` (an image marker icon). Child of `<leaflet-marker>`; rebuilt on every attribute change and swapped in via `setIcon()`. */
export default class LeafletIconElement extends Base { export default class LeafletIconElement extends Base {
declare readonly leafletObject?: Icon; declare readonly leafletObject?: Icon;

@ -51,6 +51,7 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<ImageOverlay, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<ImageOverlay, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-image-overlay>` — a Leaflet `ImageOverlay` (an image pinned to geographic `bounds`). */
export default class LeafletImageOverlayElement extends Base { export default class LeafletImageOverlayElement extends Base {
declare readonly leafletObject?: ImageOverlay; declare readonly leafletObject?: ImageOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -10,8 +10,11 @@ import type { GroupEvents } from '../core/event-types.ts';
const PROPS: Record<never, never> = {}; const PROPS: Record<never, never> = {};
const Base: LeafletElementConstructor<LayerGroup, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<LayerGroup, typeof PROPS> = WithProps(PROPS);
// A passthrough container: it has no options of its own, and children add /**
// themselves to it through the standard registration bubble. * `<leaflet-layer-group>` a Leaflet `LayerGroup`. A passthrough container
* with no options of its own; child layers add themselves through the standard
* registration bubble, and groups can nest.
*/
export default class LeafletLayerGroupElement extends Base { export default class LeafletLayerGroupElement extends Base {
declare readonly leafletObject?: LayerGroup; declare readonly leafletObject?: LayerGroup;
declare addEventListener: LeafletAddEventListener<GroupEvents>; declare addEventListener: LeafletAddEventListener<GroupEvents>;

@ -1,5 +1,11 @@
import { emitLineRemove, emitLineSync } from '../core/register.ts'; import { emitLineRemove, emitLineSync } from '../core/register.ts';
/**
* `<leaflet-line>` one vertex of a `<leaflet-polygon>` / `<leaflet-polyline>`,
* given by `lat` / `lng` attributes. Not a Leaflet object and not built on
* {@link WithProps}: it just announces its position to its parent (on connect,
* on change, and on disconnect) via the `leaflet-line-sync` / `-remove` events.
*/
export default class LeafletLineElement extends HTMLElement { export default class LeafletLineElement extends HTMLElement {
static get observedAttributes(): string[] { static get observedAttributes(): string[] {
return ['lat', 'lng']; return ['lat', 'lng'];

@ -171,8 +171,13 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<LMap, typeof PROPS> = WithProps(PROPS, { attach: 'none' }); const Base: LeafletElementConstructor<LMap, typeof PROPS> = WithProps(PROPS, { attach: 'none' });
// The root of the component tree. Every other component bubbles a /**
// `leaflet-register` event up to here, which is where it stops. * `<leaflet-map>` a Leaflet `Map`, and the root of the component tree: every
* other component bubbles a `leaflet-register` event up to here, where it stops
* (`layer.addTo(this.map)`). Builds its own shadow root (container + Leaflet
* CSS `<link>`), runs a `ResizeObserver` `invalidateSize()`, and treats its
* `css-*` attributes as describing the shadow stylesheet.
*/
export default class LeafletMapElement extends Base { export default class LeafletMapElement extends Base {
declare readonly leafletObject?: LMap; declare readonly leafletObject?: LMap;
declare addEventListener: LeafletAddEventListener<MapEvents>; declare addEventListener: LeafletAddEventListener<MapEvents>;

@ -42,6 +42,7 @@ const PROPS: typeof latLngProps & {
}; };
const Base: LeafletElementConstructor<Marker, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<Marker, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-marker>` — a Leaflet `Marker`. Accepts `<leaflet-popup>`, `<leaflet-tooltip>`, and `<leaflet-icon>` / `<leaflet-div-icon>` children. */
export default class LeafletMarkerElement extends Base { export default class LeafletMarkerElement extends Base {
declare readonly leafletObject?: Marker; declare readonly leafletObject?: Marker;
declare addEventListener: LeafletAddEventListener<MarkerEvents>; declare addEventListener: LeafletAddEventListener<MarkerEvents>;

@ -13,6 +13,7 @@ import { VertexTracker } from '../core/vertex-tracker.ts';
const PROPS = { ...pathProps } as const; const PROPS = { ...pathProps } as const;
const Base: LeafletElementConstructor<Polygon, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<Polygon, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-polygon>` — a Leaflet `Polygon`. Vertices come from `<leaflet-line>` children, not an attribute. */
export default class LeafletPolygonElement extends Base { export default class LeafletPolygonElement extends Base {
declare readonly leafletObject?: Polygon; declare readonly leafletObject?: Polygon;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -24,6 +24,7 @@ const PROPS: typeof pathProps & {
}; };
const Base: LeafletElementConstructor<Polyline, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<Polyline, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-polyline>` — a Leaflet `Polyline`. Vertices come from `<leaflet-line>` children, not an attribute. */
export default class LeafletPolylineElement extends Base { export default class LeafletPolylineElement extends Base {
declare readonly leafletObject?: Polyline; declare readonly leafletObject?: Polyline;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -27,6 +27,11 @@ const PROPS: typeof latLngProps & {
}; };
const Base: LeafletElementConstructor<Popup, typeof PROPS> = WithProps(PROPS, { attach: 'self' }); const Base: LeafletElementConstructor<Popup, typeof PROPS> = WithProps(PROPS, { attach: 'self' });
/**
* `<leaflet-popup>` a Leaflet `Popup`. Child of a layer (bound via
* `bindPopup`) or standalone with its own `lat` / `lng`; content is the
* element's markup, kept in sync via a `MutationObserver`.
*/
export default class LeafletPopupElement extends Base { export default class LeafletPopupElement extends Base {
declare readonly leafletObject?: Popup; declare readonly leafletObject?: Popup;
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>; declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;

@ -17,6 +17,7 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<Rectangle, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<Rectangle, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-rectangle>` — a Leaflet `Rectangle` spanning `bounds`, with `pathProps` styling. */
export default class LeafletRectangleElement extends Base { export default class LeafletRectangleElement extends Base {
declare readonly leafletObject?: Rectangle; declare readonly leafletObject?: Rectangle;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -40,6 +40,7 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<SVGOverlay, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<SVGOverlay, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-svg-overlay>` — a Leaflet `SVGOverlay` (an inline `<svg>` child pinned to geographic `bounds`). */
export default class LeafletSVGOverlayElement extends Base { export default class LeafletSVGOverlayElement extends Base {
declare readonly leafletObject?: SVGOverlay; declare readonly leafletObject?: SVGOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -63,6 +63,11 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<TileLayer.WMS, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<TileLayer.WMS, typeof PROPS> = WithProps(PROPS);
/**
* `<leaflet-tile-layer-wms>` a Leaflet `TileLayer.WMS`. WMS request params
* (`layers`, `styles`, `format`, ) merge through `setParams()`; `crs` takes a
* named CRS or a nested child that fires `leaflet-crs-changed`.
*/
export default class LeafletTileLayerWMSElement extends Base { export default class LeafletTileLayerWMSElement extends Base {
declare readonly leafletObject?: TileLayer.WMS; declare readonly leafletObject?: TileLayer.WMS;
declare addEventListener: LeafletAddEventListener<TileLayerEvents>; declare addEventListener: LeafletAddEventListener<TileLayerEvents>;

@ -14,6 +14,7 @@ const PROPS = {
} as const; } as const;
const Base: LeafletElementConstructor<TileLayer, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<TileLayer, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-tile-layer>` — a Leaflet `TileLayer` (an XYZ raster tile source), e.g. OpenStreetMap. */
export default class LeafletTileLayerElement extends Base { export default class LeafletTileLayerElement extends Base {
declare readonly leafletObject?: TileLayer; declare readonly leafletObject?: TileLayer;
declare addEventListener: LeafletAddEventListener<TileLayerEvents>; declare addEventListener: LeafletAddEventListener<TileLayerEvents>;

@ -27,6 +27,7 @@ const PROPS: typeof latLngProps & {
}; };
const Base: LeafletElementConstructor<Tooltip, typeof PROPS> = WithProps(PROPS, { attach: 'self' }); const Base: LeafletElementConstructor<Tooltip, typeof PROPS> = WithProps(PROPS, { attach: 'self' });
/** `<leaflet-tooltip>` — a Leaflet `Tooltip`. Child of a layer (bound via `bindTooltip`) or standalone; content is the element's markup. */
export default class LeafletTooltipElement extends Base { export default class LeafletTooltipElement extends Base {
declare readonly leafletObject?: Tooltip; declare readonly leafletObject?: Tooltip;
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>; declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;

@ -67,6 +67,7 @@ const PROPS: {
}; };
const Base: LeafletElementConstructor<VideoOverlay, typeof PROPS> = WithProps(PROPS); const Base: LeafletElementConstructor<VideoOverlay, typeof PROPS> = WithProps(PROPS);
/** `<leaflet-video-overlay>` — a Leaflet `VideoOverlay` (a video pinned to geographic `bounds`). `loop` / `autoplay` / `muted` / `playsinline` map to the `<video>`. */
export default class LeafletVideoOverlayElement extends Base { export default class LeafletVideoOverlayElement extends Base {
declare readonly leafletObject?: VideoOverlay; declare readonly leafletObject?: VideoOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>; declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -1,3 +1,29 @@
/**
* [Leaflet.js](https://leafletjs.com/) as native Web Components. Importing this
* module registers every `leaflet-*` custom element (`<leaflet-map>`,
* `<leaflet-marker>`, `<leaflet-tile-layer>`, ) and re-exports the toolkit the
* components are built from the {@link WithProps} mixin, the `PropDef` codec
* factories, the shared prop fragments, and the event-map types so a
* third-party component can be built the same way.
*
* ```ts
* import 'leaflet-web-components'; // or: jsr:@buddy/leaflet-components
* ```
*
* ```html
* <leaflet-map lat="51.5" lng="-0.09" zoom="13" style="height:400px">
* <leaflet-tile-layer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"></leaflet-tile-layer>
* <leaflet-marker lat="51.5" lng="-0.09"></leaflet-marker>
* </leaflet-map>
* ```
*
* `leaflet` is a peer dependency. The `HTMLElementTagNameMap` /
* `HTMLElementEventMap` augmentations ship with the npm build only (JSR
* disallows global augmentation); see the README.
*
* @module
*/
// The order of the `./components/*` side-effect imports below is load-bearing, // The order of the `./components/*` side-effect imports below is load-bearing,
// not cosmetic: each of those modules calls `customElements.define()`, and // not cosmetic: each of those modules calls `customElements.define()`, and
// `customElements.define()` upgrades every matching element already in the // `customElements.define()` upgrades every matching element already in the

@ -54,7 +54,7 @@ describe('real page load order (markup before customElements.define)', () => {
expect(map.leafletObject).toBeUndefined(); expect(map.leafletObject).toBeUndefined();
// Mirrors index.ts's real export order, which is what actually runs when // Mirrors index.ts's real export order, which is what actually runs when
// a page does `import 'leaflet-components'`. // a page does `import 'leaflet-web-components'`.
const lc = await import('../src/index.ts'); const lc = await import('../src/index.ts');
expect(map).toBeInstanceOf(lc.LeafletMapElement); expect(map).toBeInstanceOf(lc.LeafletMapElement);

Loading…
Cancel
Save