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.
This commit is contained in:
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();
|
||||
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) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user