fix: prevent tooltip crash from empty-string pane option

buildOptions was passing default values for absent attributes,
including empty-string defaults like pane:'', which caused Leaflet to
look up this._panes[''] and return undefined, crashing in appendChild.

- Skip empty-string defaults in buildOptions so Leaflet uses its own
- Broaden createChildRegisterHandler param from LayerGroup to Layer so
  non-container layers (marker, circle, etc.) can intercept popup/tooltip
  children and call bindPopup/bindTooltip instead of bubbling to the map
- Add child-registration handler to every layer component (all 12)
- Fix disconnectedCallback in container components to unbind popups and
  tooltips and remove child layers properly
main
Buddy 4 months ago
parent 5f3fd5f61a
commit 1e7ea22342

@ -1,5 +1,6 @@
import { CircleMarker } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletCircleMarker extends TypedBase {
#obj?: CircleMarker;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -49,10 +52,14 @@ export class LeafletCircleMarker extends TypedBase {
[this.#num('lat'), this.#num('lng')],
buildOptions(this, PROPS, ['lat', 'lng']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { Circle } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletCircle extends TypedBase {
#obj?: Circle;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -49,10 +52,14 @@ export class LeafletCircle extends TypedBase {
[this.#num('lat'), this.#num('lng')],
buildOptions(this, PROPS, ['lat', 'lng']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,9 +1,7 @@
import { FeatureGroup } from 'leaflet';
import { FeatureGroup, Layer } from 'leaflet';
import { buildOptions, registerWithParent } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
const PROPS = {} as const satisfies Record<string, never>;
export class LeafletFeatureGroup extends HTMLElement {
#obj?: FeatureGroup;
#children = new Map<HTMLElement, ChildEntry>();
@ -14,15 +12,18 @@ export class LeafletFeatureGroup extends HTMLElement {
}
connectedCallback() {
this.#obj = new FeatureGroup([], buildOptions(this, PROPS));
this.#obj = new FeatureGroup([], buildOptions(this, {}));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) {
this.removeEventListener('leaflet-register', this.#childHandler);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
for (const [el, entry] of this.#children) {
if (entry.type === 'popup') this.#obj?.unbindPopup();
else if (entry.type === 'tooltip') this.#obj?.unbindTooltip();
else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
}
this.#children.clear();
this.#obj?.remove();

@ -1,4 +1,4 @@
import { GeoJSON, PathOptions } from 'leaflet';
import { GeoJSON, PathOptions, Layer } from 'leaflet';
import { buildOptions, registerWithParent } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
@ -66,8 +66,11 @@ export class LeafletGeoJSON extends TypedBase {
}
disconnectedCallback() {
if (this.#childHandler) {
this.removeEventListener('leaflet-register', this.#childHandler);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
for (const [el, entry] of this.#children) {
if (entry.type === 'popup') this.#obj?.unbindPopup();
else if (entry.type === 'tooltip') this.#obj?.unbindTooltip();
else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
}
this.#children.clear();
this.#obj?.remove();

@ -1,5 +1,6 @@
import { ImageOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
const PROPS = {
@ -23,6 +24,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletImageOverlay extends TypedBase {
#obj?: ImageOverlay;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -54,10 +57,14 @@ export class LeafletImageOverlay extends TypedBase {
this.#parsedBounds(),
buildOptions(this, PROPS, ['url', 'bounds']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,9 +1,7 @@
import { LayerGroup } from 'leaflet';
import { LayerGroup, Layer } from 'leaflet';
import { buildOptions, registerWithParent } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
const PROPS = {} as const satisfies Record<string, never>;
export class LeafletLayerGroup extends HTMLElement {
#obj?: LayerGroup;
#children = new Map<HTMLElement, ChildEntry>();
@ -14,18 +12,18 @@ export class LeafletLayerGroup extends HTMLElement {
}
connectedCallback() {
this.#obj = new LayerGroup([], buildOptions(this, PROPS));
this.#obj = new LayerGroup([], buildOptions(this, {}));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) {
this.removeEventListener('leaflet-register', this.#childHandler);
}
for (const [el] of this.#children) {
this.#obj?.removeLayer(el as unknown as LayerGroup);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
for (const [el, entry] of this.#children) {
if (entry.type === 'popup') this.#obj?.unbindPopup();
else if (entry.type === 'tooltip') this.#obj?.unbindTooltip();
else if (entry.type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
}
this.#children.clear();
this.#obj?.remove();

@ -1,5 +1,6 @@
import { Marker } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
const PROPS = {
@ -21,6 +22,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletMarker extends TypedBase {
#obj?: Marker;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -50,10 +53,14 @@ export class LeafletMarker extends TypedBase {
[this.#num('lat'), this.#num('lng')],
buildOptions(this, PROPS, ['lat', 'lng']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { Polygon } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
import type { LeafletLine } from './leaflet-line.js';
@ -19,6 +20,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletPolygon extends TypedBase {
#obj?: Polygon;
#observer?: MutationObserver;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -45,6 +48,8 @@ export class LeafletPolygon extends TypedBase {
connectedCallback() {
this.#obj = new Polygon(this.#getCoords(), buildOptions(this, PROPS));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
this.addEventListener('line-updated', this.#syncCoords);
@ -56,6 +61,8 @@ export class LeafletPolygon extends TypedBase {
this.#observer?.disconnect();
this.#observer = undefined;
this.removeEventListener('line-updated', this.#syncCoords);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { Polyline } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
import type { LeafletLine } from './leaflet-line.js';
@ -19,6 +20,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletPolyline extends TypedBase {
#obj?: Polyline;
#observer?: MutationObserver;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -45,6 +48,8 @@ export class LeafletPolyline extends TypedBase {
connectedCallback() {
this.#obj = new Polyline(this.#getCoords(), buildOptions(this, PROPS));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
this.addEventListener('line-updated', this.#syncCoords);
@ -56,6 +61,8 @@ export class LeafletPolyline extends TypedBase {
this.#observer?.disconnect();
this.#observer = undefined;
this.removeEventListener('line-updated', this.#syncCoords);
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { Rectangle, LatLngBoundsExpression } from 'leaflet';
import { registerWithParent, buildOptions } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
@ -18,6 +19,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletRectangle extends TypedBase {
#obj?: Rectangle;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -44,10 +47,14 @@ export class LeafletRectangle extends TypedBase {
connectedCallback() {
this.#obj = new Rectangle(this.#parsedBounds(), buildOptions(this, PROPS, ['bounds']));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { SVGOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
const PROPS = {
@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletSVGOverlay extends TypedBase {
#obj?: SVGOverlay;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -52,10 +55,14 @@ export class LeafletSVGOverlay extends TypedBase {
this.#parsedBounds(),
buildOptions(this, PROPS, ['bounds']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { TileLayer } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
const PROPS = {
@ -21,6 +22,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletTileLayerWMS extends TypedBase {
#obj?: TileLayer.WMS;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -53,10 +56,14 @@ export class LeafletTileLayerWMS extends TypedBase {
url,
buildOptions(this, PROPS, ['url']) as Record<string, string>,
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { TileLayer } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
const PROPS = {
@ -20,6 +21,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletTileLayer extends TypedBase {
#obj?: TileLayer;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -45,10 +48,14 @@ export class LeafletTileLayer extends TypedBase {
connectedCallback() {
const url = this.getAttribute('url') || '';
this.#obj = new TileLayer(url, buildOptions(this, PROPS, ['url']));
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -1,5 +1,6 @@
import { VideoOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
import { createChildRegisterHandler, type ChildEntry } from '../core/register.js';
import type { PropDef, PropTypesFromTable } from '../types/props.js';
const PROPS = {
@ -24,6 +25,8 @@ const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
export class LeafletVideoOverlay extends TypedBase {
#obj?: VideoOverlay;
#children = new Map<HTMLElement, ChildEntry>();
#childHandler?: EventListener;
static get observedAttributes() {
return Object.values(PROPS).map((s) => s.attr);
@ -55,10 +58,14 @@ export class LeafletVideoOverlay extends TypedBase {
this.#parsedBounds(),
buildOptions(this, PROPS, ['url', 'bounds']),
);
this.#childHandler = createChildRegisterHandler(this.#obj, this.#children) as EventListener;
this.addEventListener('leaflet-register', this.#childHandler);
registerWithParent(this, this.#obj);
}
disconnectedCallback() {
if (this.#childHandler) this.removeEventListener('leaflet-register', this.#childHandler);
this.#children.clear();
this.#obj?.remove();
this.#obj = undefined;
}

@ -12,7 +12,7 @@ export type ChildEntry = {
};
export function createChildRegisterHandler(
container: LayerGroup,
layer: Layer,
children: globalThis.Map<HTMLElement, ChildEntry>,
) {
return (e: LeafletRegisterEvent) => {
@ -20,15 +20,15 @@ export function createChildRegisterHandler(
const el = e.detail.element;
if (obj instanceof Popup) {
e.stopPropagation();
container.bindPopup(obj);
layer.bindPopup(obj);
children.set(el, { type: 'popup' });
} else if (obj instanceof Tooltip) {
e.stopPropagation();
container.bindTooltip(obj);
layer.bindTooltip(obj);
children.set(el, { type: 'tooltip' });
} else if (obj instanceof Layer && 'addLayer' in container) {
} else if (obj instanceof Layer && 'addLayer' in layer) {
e.stopPropagation();
container.addLayer(obj);
(layer as unknown as LayerGroup).addLayer(obj);
children.set(el, { type: 'layer' });
}
};

@ -27,7 +27,7 @@ export function buildOptions(
if (exclude.includes(propName)) continue;
const val = el.getAttribute(spec.attr);
if (val === null) {
if ('default' in spec) opts[propName] = spec.default;
if ('default' in spec && spec.default !== '') opts[propName] = spec.default;
continue;
}
if (spec.kind === 'num') opts[propName] = Number(val);

Loading…
Cancel
Save