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.

134 lines
5.2 KiB
TypeScript

// Every element property is described by a PropDef: how its value encodes to
// and decodes from an HTML attribute, and how it is pushed into (and read back
// out of) the Leaflet object. `WithProps` in with-props.ts is the only consumer
// -- components just declare a table of these and never touch the plumbing.
export interface PropDef<T = unknown, TObj = unknown> {
// Attribute name. Defaults to the kebab-cased property name. A function
// receives the property name and returns the attribute (see `disabled`).
attribute?: string | ((name: string) => string);
// The property value when the attribute is absent. Keep this equal to
// Leaflet's own default: an absent attribute is left out of the options
// object entirely, so it is Leaflet's default that actually takes effect.
default: T;
// Set through `positional()` for values the Leaflet constructor takes as an
// argument (coordinates, urls, bounds) rather than as an option.
option?: false;
decode(raw: string): T;
// Returning null removes the attribute, which restores Leaflet's default.
encode(value: T): string | null;
// Pushes a new value into the Leaflet object. Defaults to calling the
// matching setter when the object has one (`opacity` -> `setOpacity`).
set?(obj: TObj, value: T, el: HTMLElement): void;
// Reads the live value back out of the Leaflet object. Used by the property
// getter, and by `event` below to write the value back to the attribute.
get?(obj: TObj): T | undefined;
// Leaflet event after which `get` is re-read and synced to the attribute,
// e.g. `move` keeps lat/lng current while a marker is dragged.
event?: string;
}
export type PropTable<T> = Record<string, PropDef<unknown, T>>;
// Everything a codec factory doesn't fill in for you. `option` is not here:
// it has to come from `positional()` to be visible in PropOptionValues.
export type PropOptions<TObj, T> = Partial<
Omit<PropDef<T, TObj>, 'default' | 'decode' | 'encode' | 'option'>
>;
// The value type of a single prop, and of a whole table.
export type PropValue<P> = P extends { default: infer T } ? T : never;
export type PropValues<T> = { [K in keyof T]: PropValue<T[K]> };
// The options object handed to `createLeafletObject`. Partial because a prop
// only appears when its attribute is present; `option: false` props never do.
export type PropOptionValues<T> = Partial<{
[K in keyof T as T[K] extends { option: false } ? never : K]: PropValue<T[K]>;
}>;
// The type `positional()` produces: a PropDef flagged so #buildOptions skips
// it. Spelled out as an alias so element PROPS tables can be given the explicit
// type annotations JSR's "no slow types" check requires without repeating the
// intersection everywhere.
export type Positional<T, TObj = unknown> = PropDef<T, TObj> & { option: false };
// Marks a prop the Leaflet constructor takes as an argument, so it is left out
// of the options object handed to createLeafletObject().
export function positional<T extends PropDef>(def: T): T & { option: false } {
return { ...def, option: false };
}
export function kebab(name: string): string {
return name.replaceAll(/[A-Z]/gu, (c) => `-${c.toLowerCase()}`);
}
export function num<TObj = unknown>(
def = 0,
opts?: PropOptions<TObj, number>,
): PropDef<number, TObj> {
return { default: def, decode: Number, encode: String, ...opts };
}
export function str<TObj = unknown>(
def = '',
opts?: PropOptions<TObj, string>,
): PropDef<string, TObj> {
return { default: def, decode: (raw) => raw, encode: (value) => value, ...opts };
}
// A string attribute whose values Leaflet types as a union -- ControlPosition,
// CrossOrigin, tooltip Direction. Nothing is validated at runtime; this is how
// the options object comes out with the type Leaflet's constructor expects.
export function choice<T extends string, TObj = unknown>(
def: T,
opts?: PropOptions<TObj, T>,
): PropDef<T, TObj> {
return { default: def, decode: (raw) => raw as T, encode: (value) => value, ...opts };
}
// A boolean attribute: present is true, `="false"` is false, absent is `def`.
// Use `bool(true)` for options Leaflet already defaults to true, so that
// `<leaflet-popup auto-pan="false">` can turn them off.
export function bool<TObj = unknown>(
def = false,
opts?: PropOptions<TObj, boolean>,
): PropDef<boolean, TObj> {
return {
default: def,
decode: (raw) => raw !== 'false',
encode: (value) => (value === def ? null : value ? '' : 'false'),
...opts,
};
}
// The inverse of `bool(true)`: `<leaflet-map disable-dragging>` reads as
// `dragging === false`. Named `disable-<kebab>` unless `attribute` says else.
export function disabled<TObj = unknown>(
opts?: PropOptions<TObj, boolean>,
): PropDef<boolean, TObj> {
return {
attribute: (name) => `disable-${kebab(name)}`,
default: true,
decode: (raw) => raw === 'false',
encode: (value) => (value ? null : ''),
...opts,
};
}
// For attributes holding JSON: bounds, icon sizes and anchors, GeoJSON data.
export function json<T, TObj = unknown>(def: T, opts?: PropOptions<TObj, T>): PropDef<T, TObj> {
return {
default: def,
decode: (raw) => JSON.parse(raw) as T,
encode: (value) => JSON.stringify(value),
...opts,
};
}