diff --git a/src/core/path-style.ts b/src/core/path-style.ts index eeed421..075a0a1 100644 --- a/src/core/path-style.ts +++ b/src/core/path-style.ts @@ -1,6 +1,10 @@ import { Path } from 'leaflet'; import { parseAttributeValue } from './utils.js'; +// The set of HTML attribute names that map to Leaflet Path style options. +// These are handled specially because Leaflet exposes them through +// setStyle() rather than individual setter methods, and they are shared +// across many vector components (polyline, polygon, circle, rectangle). export const PATH_STYLE_ATTRS = new Set([ 'color', 'weight', @@ -16,10 +20,18 @@ export const PATH_STYLE_ATTRS = new Set([ 'fill-rule', ]); +// Quick check for whether an attribute name is a path-style attribute. +// Used in attributeChangedCallback to decide whether to route through +// setLayerAttr or updatePathStyle. export function isPathStyleAttr(name: string): boolean { return PATH_STYLE_ATTRS.has(name); } +// Applies a single style change to a Leaflet Path by converting the +// kebab-case attribute name to camelCase and calling setStyle() with +// the parsed value. This is called instead of the generic setLayerAttr +// dispatcher because Path.setStyle is a bulk-update method that merges +// into the existing style object rather than replacing it. export function updatePathStyle(obj: Path, name: string, value: string | null) { const key = name.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); obj.setStyle({ [key]: parseAttributeValue(value) }); diff --git a/src/core/register.ts b/src/core/register.ts index 0a3dbe3..b6f6e8d 100644 --- a/src/core/register.ts +++ b/src/core/register.ts @@ -1,6 +1,9 @@ import { Layer, LayerGroup, Popup, Tooltip } from 'leaflet'; import { registerWithParent } from './utils.js'; +// 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; @@ -12,10 +15,17 @@ declare global { } } +// Tracks whether a child registered as a plain layer, popup, or tooltip -- +// used by the controls panel and by cleanup logic. export type ChildEntry = { type: 'layer' | 'popup' | 'tooltip'; }; +// Builds the event handler for a given parent layer. On each +// leaflet-register event from a descendant, it checks the Leaflet type: +// Popup → bindPopup, Tooltip → bindTooltip, Layer → addLayer (if the +// parent supports it). The handler stops propagation so the event +// bubbles no further (the nearest parent claims the child). export function createChildRegisterHandler(layer: Layer, children: Map) { return function (e: LeafletRegisterEvent) { const obj = e.detail.leafletObject; @@ -36,9 +46,16 @@ export function createChildRegisterHandler(layer: Layer, children: Map>(); const handlerMap = new WeakMap void>(); +// Called during connectedCallback: creates the child tracker and event +// handler, wires up the listener, and registers this element with its +// own parent (so nested structures like polygon→line→point work). +// Returns the children map for the caller to keep a reference. export function registerChildren(el: HTMLElement, layer: Layer): Map { const children = new Map(); const handler = createChildRegisterHandler(layer, children); @@ -49,6 +66,9 @@ export function registerChildren(el: HTMLElement, layer: Layer): Map | undefined { return childrenMap.get(el); } diff --git a/src/core/utils.ts b/src/core/utils.ts index 769e318..718f43d 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,10 +1,16 @@ import type { PropDef } from '../types/props.js'; import type { LatLngBoundsExpression } from 'leaflet'; +// Converts kebab-case to camelCase (e.g. "fill-color" → "fillColor"). +// Used when component PROPS tables map attribute names to Leaflet option keys. export function camelCase(str: string): string { return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase()); } +// Parses an HTML attribute string into a typed JS value: +// null → null, "true"/"false" → boolean, numeric strings → number, +// valid JSON → parsed object/array, otherwise the raw string. +// This is the bridge between HTML attribute strings and Leaflet options. export function parseAttributeValue(value: string | null): unknown { if (value === null) return null; if (value === 'true') return true; @@ -18,6 +24,10 @@ export function parseAttributeValue(value: string | null): unknown { } } +// Installs reactive getter/setter pairs on a prototype for every entry in +// a PROPS table. Each getter reads from the attribute (coerced to the +// correct type), each setter writes via setAttribute/toggleAttribute. +// Used by the withProps() mixin so components have `el.lat = 51.5` sugar. export function definePropAccessors(proto: object, props: Record) { for (const [name, spec] of Object.entries(props)) { Object.defineProperty(proto, name, { @@ -39,21 +49,39 @@ export function definePropAccessors(proto: object, props: Record, name: string): number { const v = el.getAttribute(name); if (v !== null) return Number(v); return (props[name] as { default: number }).default; } +// Reads the `bounds` attribute from an element and parses it as JSON +// into a LatLngBoundsExpression. Returns an empty array when no +// attribute is set. Shared by rectangle, image-overlay, video-overlay, +// and svg-overlay, all of which accept a `bounds` attribute. export function parseBoundsAttr(el: HTMLElement): LatLngBoundsExpression { const raw = el.getAttribute('bounds'); return raw ? (JSON.parse(raw) as LatLngBoundsExpression) : []; } +// Builds a reverse lookup map from HTML attribute name → PROP key. +// For example, { fillColor: { attr: 'fill-color' } } becomes +// { 'fill-color' → 'fillColor' }. Used by setLayerAttr to find the +// Leaflet setter name when an attribute changes at runtime. export function buildAttrMap(props: Record): Map { return new Map(Object.entries(props).map(([name, spec]) => [spec.attr, name])); } +// Generic dispatcher for runtime attribute changes on Leaflet objects. +// Given the attribute name, it looks up the PROP key, constructs the +// matching Leaflet setter name (e.g. "opacity" → "setOpacity"), and +// calls it with the parsed value. Returns true if a setter was found +// and called, false otherwise (caller can fall back, as WMS does with +// setParams). Handles bool-on props by passing the presence/absence of +// the attribute rather than its string value. export function setLayerAttr( obj: object, props: Record, @@ -74,6 +102,12 @@ export function setLayerAttr( return false; } +// Builds the initial Leaflet options object from the PROPS table and the +// element's current attributes at connection time. Props listed in +// `exclude` are skipped (they're passed separately to Leaflet +// constructors, like coordinates or URLs). Null attributes use the +// default from PROPS if one exists (skipping empty-string defaults to +// avoid Leaflet rejecting them). export function buildOptions( el: HTMLElement, props: Record, @@ -95,6 +129,12 @@ export function buildOptions( return opts; } +// 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', { diff --git a/src/core/with-props.ts b/src/core/with-props.ts index 5358b08..d07a538 100644 --- a/src/core/with-props.ts +++ b/src/core/with-props.ts @@ -4,6 +4,15 @@ import { definePropAccessors } from './utils.js'; // eslint-disable-next-line @typescript-eslint/no-explicit-any type Ctor = new (...args: any[]) => T; +// Mixin factory that turns a PROPS table into reactive property +// accessors + observedAttributes in one shot. Every component that +// follows the PROPS-table pattern uses this instead of manually +// defining a static initialization block, getters/setters, and an +// observedAttributes getter. +// +// Usage: `class LeafletFoo extends withProps(HTMLElement, PROPS)` +// Returns an intersection type so consumers see the generated +// properties from PropTypesFromTable. export function withProps, TProps extends Record>( Base: TBase, props: TProps,