@ -1,170 +1,81 @@
import { Icon , Map as LMap , MapOptions } from 'leaflet' ;
import { Icon , Map as LMap , MapOptions } from 'leaflet' ;
import { LeafletRegisterEvent } from '../core/register.ts' ;
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_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css' ;
const DEFAULT_CSS_INTEGRITY = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=' ;
const DEFAULT_CSS_INTEGRITY = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=' ;
const CSS_ATTRS = [ 'css-url' , 'css-integrity' , 'css-crossorigin' ] as const ;
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 ─────────────────────────────────────────────────────────
// ── Property table ─────────────────────────────────────────────────────────
const PROPS = {
const PROPS = defineProps ( {
// View state — excluded from #buildOptions, initialised via setView()
// View state — excluded from #buildOptions, initialised via setView()
lat : {
lat : num ( 0 , {
kind : 'num' ,
attr : 'lat' ,
default : 0 ,
viewState : true ,
viewState : true ,
event : 'moveend' ,
event : 'moveend' ,
mapGet : ( m : LMap ) = > m . getCenter ( ) ? . lat ,
mapGet : ( m : LMap ) = > m . getCenter ( ) ? . lat ,
mapSet : ( m : LMap , v : number ) = > m . setView ( [ v , m . getCenter ( ) ? . lng ? ? 0 ] , m . getZoom ( ) ) ,
mapSet : ( m : LMap , v : number ) = > m . setView ( [ v , m . getCenter ( ) ? . lng ? ? 0 ] , m . getZoom ( ) ) ,
} ,
} ) ,
lng : {
lng : num ( 0 , {
kind : 'num' ,
attr : 'lng' ,
default : 0 ,
viewState : true ,
viewState : true ,
event : 'moveend' ,
event : 'moveend' ,
mapGet : ( m : LMap ) = > m . getCenter ( ) ? . lng ,
mapGet : ( m : LMap ) = > m . getCenter ( ) ? . lng ,
mapSet : ( m : LMap , v : number ) = > m . setView ( [ m . getCenter ( ) ? . lat ? ? 0 , v ] , m . getZoom ( ) ) ,
mapSet : ( m : LMap , v : number ) = > m . setView ( [ m . getCenter ( ) ? . lat ? ? 0 , v ] , m . getZoom ( ) ) ,
} ,
} ) ,
zoom : {
zoom : num ( 2 , {
kind : 'num' ,
attr : 'zoom' ,
default : 2 ,
viewState : true ,
viewState : true ,
event : 'zoomend' ,
event : 'zoomend' ,
mapGet : ( m : LMap ) = > m . getZoom ( ) ,
mapGet : ( m : LMap ) = > m . getZoom ( ) ,
mapSet : ( m : LMap , v : number ) = > m . setZoom ( v ) ,
mapSet : ( m : LMap , v : number ) = > m . setZoom ( v ) ,
} ,
} ) ,
// Live numeric options — have Leaflet setters
// Live numeric options — have Leaflet setters
minZoom : {
minZoom : num ( 0 , {
kind : 'num' ,
attr : 'min-zoom' ,
default : 0 ,
mapGet : ( m : LMap ) = > m . getMinZoom ( ) ,
mapGet : ( m : LMap ) = > m . getMinZoom ( ) ,
mapSet : ( m : LMap , v : number ) = > m . setMinZoom ( v ) ,
mapSet : ( m : LMap , v : number ) = > m . setMinZoom ( v ) ,
} ,
} ) ,
maxZoom : {
maxZoom : num ( Infinity , {
kind : 'num' ,
attr : 'max-zoom' ,
default : Infinity ,
mapGet : ( m : LMap ) = > m . getMaxZoom ( ) ,
mapGet : ( m : LMap ) = > m . getMaxZoom ( ) ,
mapSet : ( m : LMap , v : number ) = > m . setMaxZoom ( v ) ,
mapSet : ( m : LMap , v : number ) = > m . setMaxZoom ( v ) ,
} ,
} ) ,
// Constructor-only numeric options
// Constructor-only numeric options
zoomSnap : { kind : 'num' , attr : 'zoom-snap' , default : 1 } ,
zoomSnap : num ( 1 ) ,
zoomDelta : { kind : 'num' , attr : 'zoom-delta' , default : 1 } ,
zoomDelta : num ( 1 ) ,
keyboardPanDelta : { kind : 'num' , attr : 'keyboard-pan-delta' , default : 80 } ,
keyboardPanDelta : num ( 80 ) ,
wheelDebounceTime : { kind : 'num' , attr : 'wheel-debounce-time' , default : 40 } ,
wheelDebounceTime : num ( 40 ) ,
wheelPxPerZoomLevel : {
wheelPxPerZoomLevel : num ( 60 ) ,
kind : 'num' ,
inertiaDeceleration : num ( 3000 ) ,
attr : 'wheel-px-per-zoom-level' ,
inertiaMaxSpeed : num ( Infinity ) ,
default : 60 ,
easeLinearity : num ( 0.2 ) ,
} ,
maxBoundsViscosity : num ( 0 ) ,
inertiaDeceleration : {
tapTolerance : num ( 15 ) ,
kind : 'num' ,
zoomAnimationThreshold : num ( 4 ) ,
attr : 'inertia-deceleration' ,
transform3DLimit : num ( 8388608 ) ,
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 ,
} ,
// Boolean defaults-true → disable-* attribute; handler-based options have live mapSet
// Boolean defaults-true → disable-* attribute; handler-based options have live mapSet
scrollWheelZoom : {
scrollWheelZoom : off ( ( m : LMap , v : boolean ) = > ( v ? m . scrollWheelZoom . enable ( ) : m . scrollWheelZoom . disable ( ) ) ) ,
kind : 'bool-off' ,
dragging : off ( ( m : LMap , v : boolean ) = > ( v ? m . dragging . enable ( ) : m . dragging . disable ( ) ) ) ,
attr : 'disable-scroll-wheel-zoom' ,
touchZoom : off ( ( m : LMap , v : boolean ) = > ( v ? m . touchZoom . enable ( ) : m . touchZoom . disable ( ) ) ) ,
mapSet : ( m : LMap , v : boolean ) = > ( v ? m . scrollWheelZoom . enable ( ) : m . scrollWheelZoom . 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 ( ) ) ) ,
dragging : {
keyboard : off ( ( m : LMap , v : boolean ) = > ( v ? m . keyboard . enable ( ) : m . keyboard . disable ( ) ) ) ,
kind : 'bool-off' ,
closePopupOnClick : off ( ) ,
attr : 'disable-dragging' ,
trackResize : off ( ) ,
mapSet : ( m : LMap , v : boolean ) = > ( v ? m . dragging . enable ( ) : m . dragging . disable ( ) ) ,
zoomControl : off ( ) ,
} ,
attributionControl : off ( ) ,
touchZoom : {
inertia : off ( ) ,
kind : 'bool-off' ,
zoomAnimation : off ( ) ,
attr : 'disable-touch-zoom' ,
fadeAnimation : off ( ) ,
mapSet : ( m : LMap , v : boolean ) = > ( v ? m . touchZoom . enable ( ) : m . touchZoom . disable ( ) ) ,
markerZoomAnimation : off ( ) ,
} ,
bounceAtZoomLimits : off ( ) ,
doubleClickZoom : {
tapHold : off ( ) ,
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' } ,
// Boolean defaults-false → normal attribute
// Boolean defaults-false → normal attribute
preferCanvas : { kind : 'bool-on' , attr : 'prefer-canvas' } ,
preferCanvas : on ( ) ,
worldCopyJump : { kind : 'bool-on' , attr : 'world-copy-jump' } ,
worldCopyJump : on ( ) ,
} satisfies Record < string , PropDef > ;
} ) ;
type PropName = keyof typeof PROPS ;
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 ] ) ,
( 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
// Derive property types directly from the PROPS table so adding a prop to the
// table automatically makes it part of the LeafletMap instance type.
// table automatically makes it part of the LeafletMap instance type.
type PropTypes = {
type PropTypes = {
[ K in keyof typeof PROPS ] : ( typeof PROPS ) [ K ] extends { kind : 'num' } ? number : boolean ;
[ 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 ;
const TypedBase = HTMLElement as unknown as new ( ) = > HTMLElement & PropTypes ;
export default class LeafletMap extends TypedBase {
export default class LeafletMap extends TypedBase {