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": {
"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"

@ -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<string, () => 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();
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; }\n${leafletCSS}`;
style.textContent = ':host { display: block; width: 100%; height: 400px; }';
this.shadowRoot!.appendChild(style);
}
this.#applyCss();
connectedCallback() {
// 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) {

Loading…
Cancel
Save