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.
This commit is contained in:
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…
Reference in New Issue
Block a user