refactor: switch leaflet-map to defineProps with factory helpers

Extend num() factory to accept optional extra fields (mapGet, mapSet,
viewState, event). Add off() factory and BoolOffPropInput type. Update
defineProps to derive disable- prefix for bool-off attrs. Remove
~40 lines of local type definitions from leaflet-map.ts.
main
Buddy 3 months ago
parent 10027c9fe7
commit f95cc3b1f6

@ -1,170 +1,81 @@
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';
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;
// ── Prop types ─────────────────────────────────────────────────────────────
type NumProp = {
kind: 'num';
attr: string;
default: number;
viewState?: true;
mapGet?: (m: LMap) => number | undefined;
mapSet?: (m: LMap, v: number) => void;
event?: string;
};
type BoolOffProp = {
kind: 'bool-off';
attr: string;
mapSet?: (m: LMap, enabled: boolean) => void;
};
type BoolOnProp = {
kind: 'bool-on';
attr: string;
};
type PropDef = NumProp | BoolOffProp | BoolOnProp;
// ── Property table ─────────────────────────────────────────────────────────
const PROPS = {
const PROPS = defineProps({
// View state — excluded from #buildOptions, initialised via setView()
lat: {
kind: 'num',
attr: 'lat',
default: 0,
lat: num(0, {
viewState: true,
event: 'moveend',
mapGet: (m: LMap) => m.getCenter()?.lat,
mapSet: (m: LMap, v: number) => m.setView([v, m.getCenter()?.lng ?? 0], m.getZoom()),
},
lng: {
kind: 'num',
attr: 'lng',
default: 0,
}),
lng: num(0, {
viewState: true,
event: 'moveend',
mapGet: (m: LMap) => m.getCenter()?.lng,
mapSet: (m: LMap, v: number) => m.setView([m.getCenter()?.lat ?? 0, v], m.getZoom()),
},
zoom: {
kind: 'num',
attr: 'zoom',
default: 2,
}),
zoom: num(2, {
viewState: true,
event: 'zoomend',
mapGet: (m: LMap) => m.getZoom(),
mapSet: (m: LMap, v: number) => m.setZoom(v),
},
}),
// Live numeric options — have Leaflet setters
minZoom: {
kind: 'num',
attr: 'min-zoom',
default: 0,
minZoom: num(0, {
mapGet: (m: LMap) => m.getMinZoom(),
mapSet: (m: LMap, v: number) => m.setMinZoom(v),
},
maxZoom: {
kind: 'num',
attr: 'max-zoom',
default: Infinity,
}),
maxZoom: num(Infinity, {
mapGet: (m: LMap) => m.getMaxZoom(),
mapSet: (m: LMap, v: number) => m.setMaxZoom(v),
},
}),
// Constructor-only numeric options
zoomSnap: { kind: 'num', attr: 'zoom-snap', default: 1 },
zoomDelta: { kind: 'num', attr: 'zoom-delta', default: 1 },
keyboardPanDelta: { kind: 'num', attr: 'keyboard-pan-delta', default: 80 },
wheelDebounceTime: { kind: 'num', attr: 'wheel-debounce-time', default: 40 },
wheelPxPerZoomLevel: {
kind: 'num',
attr: 'wheel-px-per-zoom-level',
default: 60,
},
inertiaDeceleration: {
kind: 'num',
attr: 'inertia-deceleration',
default: 3000,
},
inertiaMaxSpeed: {
kind: 'num',
attr: 'inertia-max-speed',
default: Infinity,
},
easeLinearity: { kind: 'num', attr: 'ease-linearity', default: 0.2 },
maxBoundsViscosity: { kind: 'num', attr: 'max-bounds-viscosity', default: 0 },
tapTolerance: { kind: 'num', attr: 'tap-tolerance', default: 15 },
zoomAnimationThreshold: {
kind: 'num',
attr: 'zoom-animation-threshold',
default: 4,
},
transform3DLimit: {
kind: 'num',
attr: 'transform-3d-limit',
default: 8388608,
},
zoomSnap: num(1),
zoomDelta: num(1),
keyboardPanDelta: num(80),
wheelDebounceTime: num(40),
wheelPxPerZoomLevel: num(60),
inertiaDeceleration: num(3000),
inertiaMaxSpeed: num(Infinity),
easeLinearity: num(0.2),
maxBoundsViscosity: num(0),
tapTolerance: num(15),
zoomAnimationThreshold: num(4),
transform3DLimit: num(8388608),
// Boolean defaults-true → disable-* attribute; handler-based options have live mapSet
scrollWheelZoom: {
kind: 'bool-off',
attr: 'disable-scroll-wheel-zoom',
mapSet: (m: LMap, v: boolean) => (v ? m.scrollWheelZoom.enable() : m.scrollWheelZoom.disable()),
},
dragging: {
kind: 'bool-off',
attr: 'disable-dragging',
mapSet: (m: LMap, v: boolean) => (v ? m.dragging.enable() : m.dragging.disable()),
},
touchZoom: {
kind: 'bool-off',
attr: 'disable-touch-zoom',
mapSet: (m: LMap, v: boolean) => (v ? m.touchZoom.enable() : m.touchZoom.disable()),
},
doubleClickZoom: {
kind: 'bool-off',
attr: 'disable-double-click-zoom',
mapSet: (m: LMap, v: boolean) => (v ? m.doubleClickZoom.enable() : m.doubleClickZoom.disable()),
},
boxZoom: {
kind: 'bool-off',
attr: 'disable-box-zoom',
mapSet: (m: LMap, v: boolean) => (v ? m.boxZoom.enable() : m.boxZoom.disable()),
},
keyboard: {
kind: 'bool-off',
attr: 'disable-keyboard',
mapSet: (m: LMap, v: boolean) => (v ? m.keyboard.enable() : m.keyboard.disable()),
},
closePopupOnClick: { kind: 'bool-off', attr: 'disable-close-popup-on-click' },
trackResize: { kind: 'bool-off', attr: 'disable-track-resize' },
zoomControl: { kind: 'bool-off', attr: 'disable-zoom-control' },
attributionControl: { kind: 'bool-off', attr: 'disable-attribution-control' },
inertia: { kind: 'bool-off', attr: 'disable-inertia' },
zoomAnimation: { kind: 'bool-off', attr: 'disable-zoom-animation' },
fadeAnimation: { kind: 'bool-off', attr: 'disable-fade-animation' },
markerZoomAnimation: {
kind: 'bool-off',
attr: 'disable-marker-zoom-animation',
},
bounceAtZoomLimits: {
kind: 'bool-off',
attr: 'disable-bounce-at-zoom-limits',
},
tapHold: { kind: 'bool-off', attr: 'disable-tap-hold' },
scrollWheelZoom: off((m: LMap, v: boolean) => (v ? m.scrollWheelZoom.enable() : m.scrollWheelZoom.disable())),
dragging: off((m: LMap, v: boolean) => (v ? m.dragging.enable() : m.dragging.disable())),
touchZoom: off((m: LMap, v: boolean) => (v ? m.touchZoom.enable() : m.touchZoom.disable())),
doubleClickZoom: off((m: LMap, v: boolean) => (v ? m.doubleClickZoom.enable() : m.doubleClickZoom.disable())),
boxZoom: off((m: LMap, v: boolean) => (v ? m.boxZoom.enable() : m.boxZoom.disable())),
keyboard: off((m: LMap, v: boolean) => (v ? m.keyboard.enable() : m.keyboard.disable())),
closePopupOnClick: off(),
trackResize: off(),
zoomControl: off(),
attributionControl: off(),
inertia: off(),
zoomAnimation: off(),
fadeAnimation: off(),
markerZoomAnimation: off(),
bounceAtZoomLimits: off(),
tapHold: off(),
// Boolean defaults-false → normal attribute
preferCanvas: { kind: 'bool-on', attr: 'prefer-canvas' },
worldCopyJump: { kind: 'bool-on', attr: 'world-copy-jump' },
} satisfies Record<string, PropDef>;
preferCanvas: on(),
worldCopyJump: on(),
});
type PropName = keyof typeof PROPS;
@ -172,17 +83,12 @@ const ATTR_TO_PROP = new globalThis.Map<string, PropName>(
(Object.entries(PROPS) as [PropName, PropDef][]).map(([name, spec]) => [spec.attr, name]),
);
// ── Element ────────────────────────────────────────────────────────────────
// Derive property types directly from the PROPS table so adding a prop to the
// table automatically makes it part of the LeafletMap instance type.
type PropTypes = {
[K in keyof typeof PROPS]: (typeof PROPS)[K] extends { kind: 'num' } ? number : boolean;
};
// Cast HTMLElement to a typed base whose instances include PropTypes. This is
// the mixin pattern — TypeScript sees LeafletMap as having all prop types, the
// runtime still extends HTMLElement, and no interface merge is required.
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export default class LeafletMap extends TypedBase {

@ -2,6 +2,10 @@ export type NumProp = {
kind: 'num';
attr: string;
default: number;
mapGet?: (m: any) => number | undefined;
mapSet?: (m: any, v: number) => void;
viewState?: boolean;
event?: string;
};
export type StrProp = {
@ -18,6 +22,7 @@ export type BoolOnProp = {
export type BoolOffProp = {
kind: 'bool-off';
attr: string;
mapSet?: (m: any, enabled: boolean) => void;
};
export type PropDef = NumProp | StrProp | BoolOnProp | BoolOffProp;
@ -39,20 +44,26 @@ type OptionalAttr<T> = Omit<T, 'attr'> & { attr?: string };
type NumPropInput = OptionalAttr<NumProp>;
type StrPropInput = OptionalAttr<StrProp>;
type BoolOnPropInput = OptionalAttr<BoolOnProp>;
type BoolOffPropInput = OptionalAttr<BoolOffProp>;
type PropDefInput = NumPropInput | StrPropInput | BoolOnPropInput;
type PropDefInput = NumPropInput | StrPropInput | BoolOnPropInput | BoolOffPropInput;
type PropDefFromInput<T> =
T extends { kind: 'num' } ? NumProp
: T extends { kind: 'str' } ? StrProp
: T extends { kind: 'bool-off' } ? BoolOffProp
: BoolOnProp;
function camelToKebab(s: string): string {
return s.replace(/[A-Z]/g, c => '-' + c.toLowerCase());
}
export function num(def: number = 0, attr?: string): NumPropInput {
return { kind: 'num', default: def, ...(attr ? { attr } : {}) };
export function num(
def: number = 0,
opts?: string | (Omit<NumProp, 'kind' | 'default' | 'attr'> & { attr?: string }),
): NumPropInput {
if (typeof opts === 'string') opts = { attr: opts };
return { kind: 'num', default: def, ...opts };
}
export function str(def: string = '', attr?: string): StrPropInput {
@ -63,6 +74,10 @@ export function on(attr?: string): BoolOnPropInput {
return { kind: 'bool-on', ...(attr ? { attr } : {}) };
}
export function off(mapSet?: (m: any, enabled: boolean) => void): BoolOffPropInput {
return { kind: 'bool-off', ...(mapSet ? { mapSet } : {}) };
}
export function defineProps<T extends Record<string, PropDefInput>>(
input: T,
): { [K in keyof T]: PropDefFromInput<T[K]> } {
@ -70,7 +85,8 @@ export function defineProps<T extends Record<string, PropDefInput>>(
for (const key of Object.keys(input)) {
const val = input[key] as PropDefInput;
const { attr: override, ...rest } = val as any;
output[key] = { ...rest, attr: override ?? camelToKebab(key) } as PropDef;
const attr = override ?? (rest.kind === 'bool-off' ? 'disable-' + camelToKebab(key) : camelToKebab(key));
output[key] = { ...rest, attr } as PropDef;
}
return output as any;
}

Loading…
Cancel
Save