feat: load Leaflet CSS via CDN <link> instead of bundling

Replace inline CSS import with a configurable CDN <link> to fix 404s on
marker icon PNG files (relative paths now resolve against the CDN URL).

- Remove leaflet/dist/leaflet.css import and --loader:.css=text from build
- Add css-url, css-integrity, css-crossorigin attributes on <leaflet-map>
- Derive Icon.Default.imagePath from CSS URL for JS-loaded marker icons
- Move shadow DOM setup into connectedCallback with reconnection guard
- Use CSS_ATTRS constant for observedAttributes and attributeChangedCallback

CSS attrs default to https://unpkg.com/leaflet@1.9.4/dist/leaflet.css with
SRI integrity and crossorigin='anonymous'. Custom CSS URLs drop the
default integrity; set css-integrity explicitly to re-enable SRI.
main
Buddy 4 months ago
parent 1e7ea22342
commit 41acdc9bb7

@ -12,7 +12,7 @@
} }
}, },
"scripts": { "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}'", "lint": "eslint 'src/**/*.{ts,js}'",
"format": "prettier --write 'src/**/*.{ts,js,json,md}'", "format": "prettier --write 'src/**/*.{ts,js,json,md}'",
"prepublishOnly": "npm run build" "prepublishOnly": "npm run build"

@ -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() { connectedCallback() {
super(); if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' }); this.attachShadow({ mode: 'open' });
this.#container = document.createElement('div'); this.#container = document.createElement('div');
this.#container.style.width = '100%'; this.#container.style.width = '100%';
this.#container.style.height = '100%'; this.#container.style.height = '100%';
this.shadowRoot!.appendChild(this.#container); this.shadowRoot!.appendChild(this.#container);
const style = document.createElement('style'); const style = document.createElement('style');
style.textContent = `:host { display: block; width: 100%; height: 400px; }\n${leafletCSS}`; style.textContent = ':host { display: block; width: 100%; height: 400px; }';
this.shadowRoot!.appendChild(style); this.shadowRoot!.appendChild(style);
} }
this.#applyCss();
connectedCallback() {
// 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) {

Loading…
Cancel
Save