diff --git a/src/components/leaflet-circle-marker.ts b/src/components/leaflet-circle-marker.ts index 1b3e3f2..84462d6 100644 --- a/src/components/leaflet-circle-marker.ts +++ b/src/components/leaflet-circle-marker.ts @@ -1,10 +1,13 @@ import { CircleMarker } from 'leaflet'; -import { buildOptions, numAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; -import { isPathStyleAttr, updatePathStyle } from '../core/path-style.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildOptions, + isPathStyleAttr, + numAttr, + updatePathStyle, +} from '../core/utils.ts'; const PROPS = defineProps({ lat: num(), diff --git a/src/components/leaflet-circle.ts b/src/components/leaflet-circle.ts index e56eda7..9352b3e 100644 --- a/src/components/leaflet-circle.ts +++ b/src/components/leaflet-circle.ts @@ -1,10 +1,13 @@ import { Circle } from 'leaflet'; -import { buildOptions, numAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; -import { isPathStyleAttr, updatePathStyle } from '../core/path-style.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildOptions, + isPathStyleAttr, + numAttr, + updatePathStyle, +} from '../core/utils.ts'; const PROPS = defineProps({ lat: num(), diff --git a/src/components/leaflet-control-attribution.ts b/src/components/leaflet-control-attribution.ts index 0b1b153..2a57071 100644 --- a/src/components/leaflet-control-attribution.ts +++ b/src/components/leaflet-control-attribution.ts @@ -1,8 +1,6 @@ import { Control, ControlPosition } from 'leaflet'; -import { registerWithParent } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { defineProps, str } from '../core/props.ts'; +import { WithProps, registerWithParent } from '../core/utils.ts'; const PROPS = defineProps({ position: str('bottomright'), diff --git a/src/components/leaflet-control-scale.ts b/src/components/leaflet-control-scale.ts index 893d97b..7a6f078 100644 --- a/src/components/leaflet-control-scale.ts +++ b/src/components/leaflet-control-scale.ts @@ -1,8 +1,6 @@ import { Control, ControlPosition } from 'leaflet'; -import { registerWithParent } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { defineProps, num, str, on } from '../core/props.ts'; +import { WithProps, registerWithParent } from '../core/utils.ts'; const PROPS = defineProps({ position: str('bottomleft'), diff --git a/src/components/leaflet-control-zoom.ts b/src/components/leaflet-control-zoom.ts index 0d8f3d7..65a7797 100644 --- a/src/components/leaflet-control-zoom.ts +++ b/src/components/leaflet-control-zoom.ts @@ -1,8 +1,6 @@ import { Control, ControlPosition } from 'leaflet'; -import { registerWithParent } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { defineProps, str } from '../core/props.ts'; +import { WithProps, registerWithParent } from '../core/utils.ts'; const PROPS = defineProps({ position: str('topleft'), diff --git a/src/components/leaflet-feature-group.ts b/src/components/leaflet-feature-group.ts index 0469a6b..29a46de 100644 --- a/src/components/leaflet-feature-group.ts +++ b/src/components/leaflet-feature-group.ts @@ -1,6 +1,6 @@ import { FeatureGroup, Layer } from 'leaflet'; -import { buildOptions } from '../core/utils.ts'; import { registerChildren, unregisterChildren, getChildren } from '../core/register.ts'; +import { buildOptions } from '../core/utils.ts'; export default class LeafletFeatureGroup extends HTMLElement { #obj?: FeatureGroup; diff --git a/src/components/leaflet-geojson.ts b/src/components/leaflet-geojson.ts index 2d3ea55..fb79d29 100644 --- a/src/components/leaflet-geojson.ts +++ b/src/components/leaflet-geojson.ts @@ -1,9 +1,7 @@ import { GeoJSON, PathOptions, Layer } from 'leaflet'; -import { buildOptions } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren, getChildren } from '../core/register.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren, getChildren } from '../core/register.ts'; +import { WithProps, buildOptions } from '../core/utils.ts'; const PROPS = defineProps({ data: str(), diff --git a/src/components/leaflet-image-overlay.ts b/src/components/leaflet-image-overlay.ts index c3857ca..2520e18 100644 --- a/src/components/leaflet-image-overlay.ts +++ b/src/components/leaflet-image-overlay.ts @@ -1,10 +1,14 @@ import { ImageOverlay, LatLngBounds, LatLngExpression } from 'leaflet'; import type { ImageOverlayOptions } from 'leaflet'; -import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildAttrMap, + buildOptions, + parseBoundsAttr, + setLayerAttr, +} from '../core/utils.ts'; const PROPS = defineProps({ url: str(), diff --git a/src/components/leaflet-layer-group.ts b/src/components/leaflet-layer-group.ts index 8686fcf..e144dd7 100644 --- a/src/components/leaflet-layer-group.ts +++ b/src/components/leaflet-layer-group.ts @@ -1,6 +1,6 @@ import { LayerGroup, Layer } from 'leaflet'; -import { buildOptions } from '../core/utils.ts'; import { registerChildren, unregisterChildren, getChildren } from '../core/register.ts'; +import { buildOptions } from '../core/utils.ts'; export default class LeafletLayerGroup extends HTMLElement { #obj?: LayerGroup; diff --git a/src/components/leaflet-map.ts b/src/components/leaflet-map.ts index f4002d1..0305945 100644 --- a/src/components/leaflet-map.ts +++ b/src/components/leaflet-map.ts @@ -1,14 +1,12 @@ import { Icon, Map as LMap, MapOptions } from 'leaflet'; -import { LeafletRegisterEvent } from '../core/register.ts'; import { defineProps, num, off, on, type NumProp, type PropDef } from '../core/props.ts'; +import { LeafletRegisterEvent } from '../core/register.ts'; const DEFAULT_CSS_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css'; const DEFAULT_CSS_INTEGRITY = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY='; const CSS_ATTRS = ['css-url', 'css-integrity', 'css-crossorigin'] as const; -// ── Property table ───────────────────────────────────────────────────────── - const PROPS = defineProps({ // View state — excluded from #buildOptions, initialised via setView() lat: num(0, { diff --git a/src/components/leaflet-marker.ts b/src/components/leaflet-marker.ts index f7b7247..07d17d1 100644 --- a/src/components/leaflet-marker.ts +++ b/src/components/leaflet-marker.ts @@ -1,9 +1,7 @@ import { Marker } from 'leaflet'; -import { buildOptions, numAttr, buildAttrMap, setLayerAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { WithProps, buildAttrMap, buildOptions, numAttr, setLayerAttr } from '../core/utils.ts'; const PROPS = defineProps({ lat: num(), diff --git a/src/components/leaflet-polygon.ts b/src/components/leaflet-polygon.ts index c34bdc1..b5b35aa 100644 --- a/src/components/leaflet-polygon.ts +++ b/src/components/leaflet-polygon.ts @@ -1,10 +1,7 @@ import { Polygon } from 'leaflet'; -import { buildOptions } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { registerChildren, unregisterChildren } from '../core/register.ts'; -import { isPathStyleAttr, updatePathStyle } from '../core/path-style.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { WithProps, buildOptions, isPathStyleAttr, updatePathStyle } from '../core/utils.ts'; import type LeafletLine from './leaflet-line.ts'; const PROPS = defineProps({ diff --git a/src/components/leaflet-polyline.ts b/src/components/leaflet-polyline.ts index a45122b..5867751 100644 --- a/src/components/leaflet-polyline.ts +++ b/src/components/leaflet-polyline.ts @@ -1,10 +1,7 @@ import { Polyline } from 'leaflet'; -import { buildOptions } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; -import { isPathStyleAttr, updatePathStyle } from '../core/path-style.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { WithProps, buildOptions, isPathStyleAttr, updatePathStyle } from '../core/utils.ts'; import type LeafletLine from './leaflet-line.ts'; const PROPS = defineProps({ diff --git a/src/components/leaflet-popup.ts b/src/components/leaflet-popup.ts index 873085c..aa4a01a 100644 --- a/src/components/leaflet-popup.ts +++ b/src/components/leaflet-popup.ts @@ -1,8 +1,6 @@ import { Popup } from 'leaflet'; -import { registerWithParent, buildOptions, numAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { defineProps, num, on } from '../core/props.ts'; +import { WithProps, buildOptions, numAttr, registerWithParent } from '../core/utils.ts'; const PROPS = defineProps({ lat: num(), diff --git a/src/components/leaflet-rectangle.ts b/src/components/leaflet-rectangle.ts index bd3d898..8db8de6 100644 --- a/src/components/leaflet-rectangle.ts +++ b/src/components/leaflet-rectangle.ts @@ -1,10 +1,13 @@ import { Rectangle } from 'leaflet'; -import { buildOptions, parseBoundsAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; -import { isPathStyleAttr, updatePathStyle } from '../core/path-style.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildOptions, + isPathStyleAttr, + parseBoundsAttr, + updatePathStyle, +} from '../core/utils.ts'; const PROPS = defineProps({ bounds: str(), diff --git a/src/components/leaflet-svg-overlay.ts b/src/components/leaflet-svg-overlay.ts index fd01140..bd13514 100644 --- a/src/components/leaflet-svg-overlay.ts +++ b/src/components/leaflet-svg-overlay.ts @@ -1,10 +1,14 @@ import { SVGOverlay, LatLngBounds, LatLngExpression } from 'leaflet'; import type { ImageOverlayOptions } from 'leaflet'; -import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildAttrMap, + buildOptions, + parseBoundsAttr, + setLayerAttr, +} from '../core/utils.ts'; const PROPS = defineProps({ bounds: str(), diff --git a/src/components/leaflet-tile-layer-wms.ts b/src/components/leaflet-tile-layer-wms.ts index 54c069a..a069674 100644 --- a/src/components/leaflet-tile-layer-wms.ts +++ b/src/components/leaflet-tile-layer-wms.ts @@ -1,9 +1,13 @@ import { TileLayer } from 'leaflet'; -import { buildOptions, buildAttrMap, setLayerAttr, parseAttributeValue } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; import { defineProps, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildAttrMap, + buildOptions, + parseAttributeValue, + setLayerAttr, +} from '../core/utils.ts'; const PROPS = defineProps({ url: str(), diff --git a/src/components/leaflet-tile-layer.ts b/src/components/leaflet-tile-layer.ts index 483e4cf..31a1a5b 100644 --- a/src/components/leaflet-tile-layer.ts +++ b/src/components/leaflet-tile-layer.ts @@ -1,9 +1,7 @@ import { TileLayer } from 'leaflet'; -import { buildOptions, buildAttrMap, setLayerAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { registerChildren, unregisterChildren } from '../core/register.ts'; import { defineProps, num, str } from '../core/props.ts'; +import { WithProps, buildAttrMap, buildOptions, setLayerAttr } from '../core/utils.ts'; const PROPS = defineProps({ url: str(), diff --git a/src/components/leaflet-tooltip.ts b/src/components/leaflet-tooltip.ts index c3655d2..72f4959 100644 --- a/src/components/leaflet-tooltip.ts +++ b/src/components/leaflet-tooltip.ts @@ -1,9 +1,7 @@ import { Tooltip } from 'leaflet'; import type { TooltipOptions } from 'leaflet'; -import { registerWithParent, buildOptions, numAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - import { defineProps, num, str, on } from '../core/props.ts'; +import { WithProps, buildOptions, numAttr, registerWithParent } from '../core/utils.ts'; const PROPS = defineProps({ lat: num(), diff --git a/src/components/leaflet-video-overlay.ts b/src/components/leaflet-video-overlay.ts index 0473eba..b4517a1 100644 --- a/src/components/leaflet-video-overlay.ts +++ b/src/components/leaflet-video-overlay.ts @@ -1,10 +1,14 @@ import { VideoOverlay, LatLngBounds, LatLngExpression } from 'leaflet'; import type { VideoOverlayOptions } from 'leaflet'; -import { buildOptions, buildAttrMap, setLayerAttr, parseBoundsAttr } from '../core/utils.ts'; -import { WithProps } from '../core/with-props.ts'; - -import { registerChildren, unregisterChildren } from '../core/register.ts'; import { defineProps, num, str, on } from '../core/props.ts'; +import { registerChildren, unregisterChildren } from '../core/register.ts'; +import { + WithProps, + buildAttrMap, + buildOptions, + parseBoundsAttr, + setLayerAttr, +} from '../core/utils.ts'; const PROPS = defineProps({ url: str(), diff --git a/src/core/path-style.ts b/src/core/path-style.ts deleted file mode 100644 index 09d0827..0000000 --- a/src/core/path-style.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Path } from 'leaflet'; -import { parseAttributeValue } from './utils.ts'; - -// The set of HTML attribute names that map to Leaflet Path style options. -// These are handled especially because Leaflet exposes them through -// setStyle() rather than individual setter methods, and they are shared -// across many vector components (polyline, polygon, circle, rectangle). -const PATH_STYLE_ATTRS = new Set([ - 'color', - 'weight', - 'opacity', - 'fill', - 'fill-color', - 'fill-opacity', - 'stroke', - 'dash-array', - 'dash-offset', - 'line-cap', - 'line-join', - '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/utils.ts b/src/core/utils.ts index 2b9314c..a31a101 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -1,28 +1,31 @@ +import { Path } from 'leaflet'; import type { PropDef, PropTypesFromTable } from './props.ts'; import type { LatLngBoundsExpression } from 'leaflet'; -// 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; - if (value === 'false') return false; - const num = Number(value); - if (!isNaN(num) && value !== '') return num; - try { - return JSON.parse(value); - } catch { - return value; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Ctor = new (...args: any[]) => T; + +export function WithProps, TProps extends Record>( + Base: TBase, + props: TProps, +): TBase & Ctor> { + class WithProps extends Base { + static { + definePropAccessors(WithProps.prototype, props); + } + static get observedAttributes(): string[] { + return Object.values(props).map((s) => s.attr); + } } + + return WithProps as TBase & Ctor>; } // 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) { +function definePropAccessors(proto: object, props: Record) { for (const [name, spec] of Object.entries(props)) { Object.defineProperty(proto, name, { get() { @@ -96,6 +99,23 @@ export function setLayerAttr( return false; } +// 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; + if (value === 'false') return false; + const num = Number(value); + if (!isNaN(num) && value !== '') return num; + try { + return JSON.parse(value); + } catch { + return value; + } +} + // 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 @@ -106,7 +126,7 @@ export function setLayerAttr( // The return type is derived from the PROPS table: each prop name maps // to its kind's value type (number for `num`, boolean for `bool-on`/`bool-off`, // string for `str`). The `const` type parameter makes literal exclude arrays -// narrow correctly so excluded keys are stripped from the return type. +// narrow correctly, so excluded keys are stripped from the return type. export function buildOptions< TProps extends Record, const TExclude extends readonly string[] = [], @@ -146,3 +166,27 @@ export function registerWithParent(el: HTMLElement, obj: unknown) { }), ); } + +const PATH_STYLE_ATTRS = new Set([ + 'color', + 'weight', + 'opacity', + 'fill', + 'fill-color', + 'fill-opacity', + 'stroke', + 'dash-array', + 'dash-offset', + 'line-cap', + 'line-join', + 'fill-rule', +]); + +export function isPathStyleAttr(name: string): boolean { + return PATH_STYLE_ATTRS.has(name); +} + +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/with-props.ts b/src/core/with-props.ts deleted file mode 100644 index ae84434..0000000 --- a/src/core/with-props.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { PropDef, PropTypesFromTable } from './props.ts'; -import { definePropAccessors } from './utils.ts'; - -// 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, -): TBase & Ctor> { - class WithProps extends Base { - static { - definePropAccessors(WithProps.prototype, props); - } - static get observedAttributes(): string[] { - return Object.values(props).map((s) => s.attr); - } - } - - return WithProps as TBase & Ctor>; -} diff --git a/src/index.ts b/src/index.ts index d53ccd6..d685c44 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,4 @@ export * from './core/utils.ts'; -export * from './core/with-props.ts'; -export * from './core/path-style.ts'; export * from './core/register.ts'; export * from './core/props.ts'; export { default as LeafletMap } from './components/leaflet-map.ts';