diff --git a/package.json b/package.json index 96ac872..b555fc7 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ } }, "scripts": { - "build": "rm -rf dist && tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --loader:.css=text --outfile=dist/index.js", + "build": "rm -rf dist && tsc --emitDeclarationOnly && esbuild src/index.ts --bundle --format=esm --outfile=dist/index.js", "lint": "eslint 'src/**/*.{ts,js}'", "format": "prettier --write 'src/**/*.{ts,js,json,md}'", "prepublishOnly": "npm run build" diff --git a/src/components/leaflet-map.ts b/src/components/leaflet-map.ts index f19aae4..a383ca3 100644 --- a/src/components/leaflet-map.ts +++ b/src/components/leaflet-map.ts @@ -1,7 +1,11 @@ -import { Map as LMap, MapOptions } from 'leaflet'; -import leafletCSS from 'leaflet/dist/leaflet.css'; +import { Icon, Map as LMap, MapOptions } from 'leaflet'; 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 ───────────────────────────────────────────────────────────── type NumProp = { @@ -158,11 +162,12 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes; export class LeafletMap extends TypedBase { #map?: LMap; #container!: HTMLDivElement; + #cssLink?: HTMLLinkElement; #syncing = false; #mapEventHandlers = new globalThis.Map void>(); 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. @@ -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() { + 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() // which throw if invoked before setView(), so we can't use them here. 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) { 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); if (!propName) return; const spec = PROPS[propName] as PropDef; @@ -288,7 +297,46 @@ export class LeafletMap extends TypedBase { } else if (spec.kind === 'bool-off') { 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) {