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
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,
|
|
};
|
|
}
|