Compare commits

..

1 Commits

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

4
package-lock.json generated

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

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

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

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

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

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

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

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

@ -19,7 +19,6 @@ const PROPS: typeof latLngProps &
};
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 {
declare readonly leafletObject?: CircleMarker;
declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -21,7 +21,6 @@ const PROPS = {
} as const;
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 {
declare readonly leafletObject?: Circle;
declare addEventListener: LeafletAddEventListener<PathEvents>;

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

@ -20,11 +20,6 @@ const Base: LeafletElementConstructor<Control.Layers, typeof PROPS> = WithProps(
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 {
declare readonly leafletObject?: Control.Layers;

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

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

@ -27,11 +27,6 @@ const Base: LeafletElementConstructor<DivIcon, typeof PROPS> = WithProps(PROPS,
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 {
declare readonly leafletObject?: DivIcon;

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

@ -25,7 +25,6 @@ const 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 {
declare readonly leafletObject?: GeoJSON;
declare addEventListener: LeafletAddEventListener<GroupEvents>;

@ -36,7 +36,6 @@ const Base: LeafletElementConstructor<Icon, typeof PROPS> = WithProps(PROPS, {
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 {
declare readonly leafletObject?: Icon;

@ -51,7 +51,6 @@ const 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 {
declare readonly leafletObject?: ImageOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>;

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

@ -1,11 +1,5 @@
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 {
static get observedAttributes(): string[] {
return ['lat', 'lng'];

@ -171,13 +171,8 @@ const PROPS: {
};
const Base: LeafletElementConstructor<LMap, typeof PROPS> = WithProps(PROPS, { attach: 'none' });
/**
* `<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.
*/
// The root of the component tree. Every other component bubbles a
// `leaflet-register` event up to here, which is where it stops.
export default class LeafletMapElement extends Base {
declare readonly leafletObject?: LMap;
declare addEventListener: LeafletAddEventListener<MapEvents>;

@ -42,7 +42,6 @@ const PROPS: typeof latLngProps & {
};
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 {
declare readonly leafletObject?: Marker;
declare addEventListener: LeafletAddEventListener<MarkerEvents>;

@ -13,7 +13,6 @@ import { VertexTracker } from '../core/vertex-tracker.ts';
const PROPS = { ...pathProps } as const;
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 {
declare readonly leafletObject?: Polygon;
declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -24,7 +24,6 @@ const PROPS: typeof pathProps & {
};
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 {
declare readonly leafletObject?: Polyline;
declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -27,11 +27,6 @@ const PROPS: typeof latLngProps & {
};
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 {
declare readonly leafletObject?: Popup;
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;

@ -17,7 +17,6 @@ const 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 {
declare readonly leafletObject?: Rectangle;
declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -40,7 +40,6 @@ const 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 {
declare readonly leafletObject?: SVGOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -63,11 +63,6 @@ const 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 {
declare readonly leafletObject?: TileLayer.WMS;
declare addEventListener: LeafletAddEventListener<TileLayerEvents>;

@ -14,7 +14,6 @@ const PROPS = {
} as const;
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 {
declare readonly leafletObject?: TileLayer;
declare addEventListener: LeafletAddEventListener<TileLayerEvents>;

@ -27,7 +27,6 @@ const PROPS: typeof latLngProps & {
};
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 {
declare readonly leafletObject?: Tooltip;
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;

@ -67,7 +67,6 @@ const 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 {
declare readonly leafletObject?: VideoOverlay;
declare addEventListener: LeafletAddEventListener<PathEvents>;

@ -1,29 +1,3 @@
/**
* [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,
// not cosmetic: each of those modules calls `customElements.define()`, and
// `customElements.define()` upgrades every matching element already in the

Loading…
Cancel
Save