docs: add why/how comments to all src/core modules
Explain design rationale for every exported utility, mixin, registration helper, and path-style handler
This commit is contained in:
parent
319b7bdb0c
commit
348e865dae
@ -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) });
|
||||
|
||||
@ -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<HTMLElement, ChildEntry>) {
|
||||
return function (e: LeafletRegisterEvent) {
|
||||
const obj = e.detail.leafletObject;
|
||||
@ -36,9 +46,16 @@ export function createChildRegisterHandler(layer: Layer, children: Map<HTMLEleme
|
||||
};
|
||||
}
|
||||
|
||||
// Module-scoped WeakMaps keyed by the parent element so we don't pollute
|
||||
// component instances with #children / #handler fields. This keeps the
|
||||
// data accessible to any caller that holds a reference to the element.
|
||||
const childrenMap = new WeakMap<HTMLElement, Map<HTMLElement, ChildEntry>>();
|
||||
const handlerMap = new WeakMap<HTMLElement, (e: LeafletRegisterEvent) => 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<HTMLElement, ChildEntry> {
|
||||
const children = new Map<HTMLElement, ChildEntry>();
|
||||
const handler = createChildRegisterHandler(layer, children);
|
||||
@ -49,6 +66,9 @@ export function registerChildren(el: HTMLElement, layer: Layer): Map<HTMLElement
|
||||
return children;
|
||||
}
|
||||
|
||||
// Called during disconnectedCallback: tears down the event listener and
|
||||
// removes the WeakMap entries so both the handler and children map can
|
||||
// be GC'd when the element is removed from the DOM.
|
||||
export function unregisterChildren(el: HTMLElement): void {
|
||||
const handler = handlerMap.get(el);
|
||||
if (handler) el.removeEventListener('leaflet-register', handler);
|
||||
@ -56,6 +76,8 @@ export function unregisterChildren(el: HTMLElement): void {
|
||||
handlerMap.delete(el);
|
||||
}
|
||||
|
||||
// Public accessor for the children map. Used by the controls panel and
|
||||
// by any component that needs to inspect its registered descendants.
|
||||
export function getChildren(el: HTMLElement): Map<HTMLElement, ChildEntry> | undefined {
|
||||
return childrenMap.get(el);
|
||||
}
|
||||
|
||||
@ -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<string, PropDef>) {
|
||||
for (const [name, spec] of Object.entries(props)) {
|
||||
Object.defineProperty(proto, name, {
|
||||
@ -39,21 +49,39 @@ export function definePropAccessors(proto: object, props: Record<string, PropDef
|
||||
}
|
||||
}
|
||||
|
||||
// Reads a numeric attribute from an element, falling back to the default
|
||||
// value declared in the PROPS table. Handles the common pattern of
|
||||
// reading lat/lng/radius/opacity with a guaranteed number return.
|
||||
export function numAttr(el: HTMLElement, props: Record<string, PropDef>, 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<string, PropDef>): Map<string, string> {
|
||||
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<string, PropDef>,
|
||||
@ -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<string, PropDef>,
|
||||
@ -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', {
|
||||
|
||||
@ -4,6 +4,15 @@ import { definePropAccessors } from './utils.js';
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Ctor<T = object> = 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<TBase extends Ctor<HTMLElement>, TProps extends Record<string, PropDef>>(
|
||||
Base: TBase,
|
||||
props: TProps,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user