refactor: consolidate core modules into utils.ts

Merge path-style.ts and with-props.ts into utils.ts to reduce the
number of core files from 5 to 3 (utils.ts, props.ts, register.ts).
Unify duplicate ../core/utils.ts imports in all component files.
Remove stale re-exports from src/index.ts.
main
Buddy 3 months ago
parent d8bdc2dcfa
commit 7ae092ac98

@ -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(),

@ -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(),

@ -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'),

@ -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'),

@ -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'),

@ -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;

@ -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(),

@ -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(),

@ -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;

@ -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, {

@ -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(),

@ -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({

@ -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({

@ -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(),

@ -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(),

@ -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(),

@ -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(),

@ -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(),

@ -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(),

@ -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(),

@ -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) });
}

@ -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<T = object> = new (...args: any[]) => T;
export function WithProps<TBase extends Ctor<HTMLElement>, TProps extends Record<string, PropDef>>(
Base: TBase,
props: TProps,
): TBase & Ctor<HTMLElement & PropTypesFromTable<TProps>> {
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<HTMLElement & PropTypesFromTable<TProps>>;
}
// 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>) {
function definePropAccessors(proto: object, props: Record<string, PropDef>) {
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<string, PropDef>,
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) });
}

@ -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<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,
): TBase & Ctor<HTMLElement & PropTypesFromTable<TProps>> {
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<HTMLElement & PropTypesFromTable<TProps>>;
}

@ -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';

Loading…
Cancel
Save