You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
leaflet-components/src/core/attributes.ts

98 lines
3.5 KiB
TypeScript

import type { LatLngBoundsExpression } from 'leaflet';
import { Path } from 'leaflet';
import type { PropDef } from './props.ts';
// 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 +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<T extends Record<string, PropDef>>(props: T): Map<string, keyof T> {
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>,
attrMap: Map<string, string>,
name: string,
val: string | null,
): boolean {
const propName = attrMap.get(name);
if (!propName) return false;
const spec = props[propName];
const setter = `set${propName.charAt(0).toUpperCase()}${propName.slice(1)}`;
const fn = (obj as Record<string, unknown>)[setter];
if (typeof fn === 'function') {
const value = spec.kind === 'bool-on' ? val !== null : parseAttributeValue(val);
(fn as (v: unknown) => void)(value);
return true;
}
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 = +value;
if (!isNaN(num) && value !== '') return num;
try {
return JSON.parse(value);
} catch {
return value;
}
}
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) });
}