@ -1,7 +1,11 @@
import { Map as LMap , MapOptions } from 'leaflet' ;
import { Icon , Map as LMap , MapOptions } from 'leaflet' ;
import leafletCSS from 'leaflet/dist/leaflet.css' ;
import { LeafletRegisterEvent } from '../core/register.js' ;
import { LeafletRegisterEvent } from '../core/register.js' ;
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 ─────────────────────────────────────────────────────────────
// ── Prop types ─────────────────────────────────────────────────────────────
type NumProp = {
type NumProp = {
@ -158,11 +162,12 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletMap extends TypedBase {
export class LeafletMap extends TypedBase {
# map? : LMap ;
# map? : LMap ;
# container ! : HTMLDivElement ;
# container ! : HTMLDivElement ;
# cssLink? : HTMLLinkElement ;
# syncing = false ;
# syncing = false ;
# mapEventHandlers = new globalThis . Map < string , ( ) = > void > ( ) ;
# mapEventHandlers = new globalThis . Map < string , ( ) = > void > ( ) ;
static get observedAttributes ( ) : string [ ] {
static get observedAttributes ( ) : string [ ] {
return Object . values ( PROPS ) . map ( ( s ) = > s . attr ) ;
return [ . . . Object . values ( PROPS ) . map ( ( s ) = > s . attr ) , . . . CSS_ATTRS ] ;
}
}
// Generates a getter/setter on the prototype for every entry in PROPS.
// Generates a getter/setter on the prototype for every entry in PROPS.
@ -211,19 +216,19 @@ export class LeafletMap extends TypedBase {
}
}
}
}
constructor ( ) {
super ( ) ;
this . attachShadow ( { mode : 'open' } ) ;
this . # container = document . createElement ( 'div' ) ;
this . # container . style . width = '100%' ;
this . # container . style . height = '100%' ;
this . shadowRoot ! . appendChild ( this . # container ) ;
const style = document . createElement ( 'style' ) ;
style . textContent = ` :host { display: block; width: 100%; height: 400px; } \ n ${ leafletCSS } ` ;
this . shadowRoot ! . appendChild ( style ) ;
}
connectedCallback() {
connectedCallback() {
if ( ! this . shadowRoot ) {
this . attachShadow ( { mode : 'open' } ) ;
this . # container = document . createElement ( 'div' ) ;
this . # container . style . width = '100%' ;
this . # container . style . height = '100%' ;
this . shadowRoot ! . appendChild ( this . # container ) ;
const style = document . createElement ( 'style' ) ;
style . textContent = ':host { display: block; width: 100%; height: 400px; }' ;
this . shadowRoot ! . appendChild ( style ) ;
}
this . # applyCss ( ) ;
// Read view-state from attributes directly — the getters call getCenter()/getZoom()
// Read view-state from attributes directly — the getters call getCenter()/getZoom()
// which throw if invoked before setView(), so we can't use them here.
// which throw if invoked before setView(), so we can't use them here.
const lat = Number ( this . getAttribute ( 'lat' ) ? ? 0 ) ;
const lat = Number ( this . getAttribute ( 'lat' ) ? ? 0 ) ;
@ -280,6 +285,10 @@ export class LeafletMap extends TypedBase {
attributeChangedCallback ( name : string , oldValue : string | null , newValue : string | null ) {
attributeChangedCallback ( name : string , oldValue : string | null , newValue : string | null ) {
if ( oldValue === newValue || ! this . # map || this . # syncing ) return ;
if ( oldValue === newValue || ! this . # map || this . # syncing ) return ;
if ( CSS_ATTRS . includes ( name as ( typeof CSS_ATTRS ) [ number ] ) ) {
this . # applyCss ( ) ;
return ;
}
const propName = ATTR_TO_PROP . get ( name ) ;
const propName = ATTR_TO_PROP . get ( name ) ;
if ( ! propName ) return ;
if ( ! propName ) return ;
const spec = PROPS [ propName ] as PropDef ;
const spec = PROPS [ propName ] as PropDef ;
@ -288,7 +297,46 @@ export class LeafletMap extends TypedBase {
} else if ( spec . kind === 'bool-off' ) {
} else if ( spec . kind === 'bool-off' ) {
spec . mapSet ? . ( this . # map , newValue === null ) ;
spec . mapSet ? . ( this . # map , newValue === null ) ;
}
}
// bool-on: constructor-only, no live update
// skip spec.kind === 'bool-on': constructor-only, no live update
}
# applyCss() {
const sr = this . shadowRoot ;
if ( ! sr ) return ;
if ( this . # cssLink ) {
this . # cssLink . remove ( ) ;
this . # cssLink = undefined ;
}
const customUrl = this . hasAttribute ( 'css-url' ) ;
const url = customUrl ? this . getAttribute ( 'css-url' ) ! : DEFAULT_CSS_URL ;
let integrity : string | undefined ;
if ( this . hasAttribute ( 'css-integrity' ) ) {
integrity = this . getAttribute ( 'css-integrity' ) ! ;
} else if ( ! customUrl ) {
integrity = DEFAULT_CSS_INTEGRITY ;
}
let crossorigin : string | undefined ;
if ( this . hasAttribute ( 'css-crossorigin' ) ) {
const val = this . getAttribute ( 'css-crossorigin' ) ;
crossorigin = val ? ? undefined ;
} else if ( integrity ) {
crossorigin = 'anonymous' ;
}
const link = document . createElement ( 'link' ) ;
link . rel = 'stylesheet' ;
link . href = url ;
if ( integrity ) link . setAttribute ( 'integrity' , integrity ) ;
if ( crossorigin !== undefined ) link . setAttribute ( 'crossorigin' , crossorigin ) ;
sr . appendChild ( link ) ;
this . # cssLink = link ;
// Derive marker icon path from the CSS URL (replaces leaflet.css → images/)
Icon . Default . imagePath = url . replace ( /\/[^/]+$/ , '/images/' ) ;
}
}
# syncAttr ( name : string , value : string ) {
# syncAttr ( name : string , value : string ) {