refactor: replace inheritance hierarchy with composable PROPS-table pattern
Remove LeafletElement and LeafletControl base classes. Every component
now self-describes its attributes via a PROPS table, derives observed-
Attributes from it, and generates prototype getters/setters in static {}.
Registration lifecycle is extracted into standalone helpers
(registerWithParent, createChildRegisterHandler). Common update
patterns (path style) are shared via imported helpers instead of
duplicated across 5+ components.
Components manage their own #obj private field instead of a shared
protected leafletObject. Leaflet-map-style declarative patterns are
now consistent across all 20 components.
main
parent
6b448ba092
commit
5f3fd5f61a
@ -1,54 +1,79 @@
|
|||||||
import { Layer, CircleMarker } from 'leaflet';
|
import { CircleMarker } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
|
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
lat: { kind: 'num', attr: 'lat', default: 0 },
|
||||||
|
lng: { kind: 'num', attr: 'lng', default: 0 },
|
||||||
|
radius: { kind: 'num', attr: 'radius', default: 10 },
|
||||||
|
color: { kind: 'str', attr: 'color', default: '#3388ff' },
|
||||||
|
weight: { kind: 'num', attr: 'weight', default: 3 },
|
||||||
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
fill: { kind: 'bool-on', attr: 'fill' },
|
||||||
|
fillColor: { kind: 'str', attr: 'fill-color', default: '#3388ff' },
|
||||||
|
fillOpacity: { kind: 'num', attr: 'fill-opacity', default: 0.2 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletCircleMarker extends TypedBase {
|
||||||
|
#obj?: CircleMarker;
|
||||||
|
|
||||||
export class LeafletCircleMarker extends LeafletElement {
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return [
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
'lat',
|
}
|
||||||
'lng',
|
|
||||||
'radius',
|
static {
|
||||||
'color',
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
'weight',
|
Object.defineProperty(LeafletCircleMarker.prototype, name, {
|
||||||
'opacity',
|
get(this: LeafletCircleMarker) {
|
||||||
'fill',
|
const val = this.getAttribute(spec.attr);
|
||||||
'fill-color',
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
'fill-opacity',
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
];
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletCircleMarker, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.#obj = new CircleMarker(
|
||||||
|
[this.#num('lat'), this.#num('lng')],
|
||||||
|
buildOptions(this, PROPS, ['lat', 'lng']),
|
||||||
|
);
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): Layer {
|
disconnectedCallback() {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
this.#obj?.remove();
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
this.#obj = undefined;
|
||||||
return new CircleMarker([lat, lng], this.options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
if (this.leafletObject instanceof CircleMarker) {
|
if (!this.#obj) return;
|
||||||
if (property === 'lat' || property === 'lng') {
|
if (name === 'lat' || name === 'lng') {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
} else if (name === 'radius') {
|
||||||
this.leafletObject.setLatLng([lat, lng]);
|
this.#obj.setRadius(this.#num('radius'));
|
||||||
} else if (
|
} else if (isPathStyleAttr(name)) {
|
||||||
[
|
updatePathStyle(this.#obj, name, val);
|
||||||
'color',
|
|
||||||
'weight',
|
|
||||||
'opacity',
|
|
||||||
'fill',
|
|
||||||
'fillColor',
|
|
||||||
'fillOpacity',
|
|
||||||
'stroke',
|
|
||||||
'dashArray',
|
|
||||||
'dashOffset',
|
|
||||||
'lineCap',
|
|
||||||
'lineJoin',
|
|
||||||
].includes(property)
|
|
||||||
) {
|
|
||||||
this.leafletObject.setStyle({ [property]: value });
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#num(name: string): number {
|
||||||
|
const v = this.getAttribute(name);
|
||||||
|
return v !== null
|
||||||
|
? Number(v)
|
||||||
|
: (PROPS[name as keyof typeof PROPS] as { default: number }).default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-circle-marker', LeafletCircleMarker);
|
customElements.define('leaflet-circle-marker', LeafletCircleMarker);
|
||||||
|
|||||||
@ -1,54 +1,79 @@
|
|||||||
import { Circle, Layer } from 'leaflet';
|
import { Circle } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
|
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
lat: { kind: 'num', attr: 'lat', default: 0 },
|
||||||
|
lng: { kind: 'num', attr: 'lng', default: 0 },
|
||||||
|
radius: { kind: 'num', attr: 'radius', default: 1000 },
|
||||||
|
color: { kind: 'str', attr: 'color', default: '#3388ff' },
|
||||||
|
weight: { kind: 'num', attr: 'weight', default: 3 },
|
||||||
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
fill: { kind: 'bool-on', attr: 'fill' },
|
||||||
|
fillColor: { kind: 'str', attr: 'fill-color', default: '#3388ff' },
|
||||||
|
fillOpacity: { kind: 'num', attr: 'fill-opacity', default: 0.2 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletCircle extends TypedBase {
|
||||||
|
#obj?: Circle;
|
||||||
|
|
||||||
export class LeafletCircle extends LeafletElement {
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return [
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
'lat',
|
}
|
||||||
'lng',
|
|
||||||
'radius',
|
static {
|
||||||
'color',
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
'weight',
|
Object.defineProperty(LeafletCircle.prototype, name, {
|
||||||
'opacity',
|
get(this: LeafletCircle) {
|
||||||
'fill',
|
const val = this.getAttribute(spec.attr);
|
||||||
'fill-color',
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
'fill-opacity',
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
];
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletCircle, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.#obj = new Circle(
|
||||||
|
[this.#num('lat'), this.#num('lng')],
|
||||||
|
buildOptions(this, PROPS, ['lat', 'lng']),
|
||||||
|
);
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): Layer {
|
disconnectedCallback() {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
this.#obj?.remove();
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
this.#obj = undefined;
|
||||||
return new Circle([lat, lng], this.options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
if (this.leafletObject instanceof Circle) {
|
if (!this.#obj) return;
|
||||||
if (property === 'lat' || property === 'lng') {
|
if (name === 'lat' || name === 'lng') {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
} else if (name === 'radius') {
|
||||||
this.leafletObject.setLatLng([lat, lng]);
|
this.#obj.setRadius(this.#num('radius'));
|
||||||
} else if (
|
} else if (isPathStyleAttr(name)) {
|
||||||
[
|
updatePathStyle(this.#obj, name, val);
|
||||||
'color',
|
|
||||||
'weight',
|
|
||||||
'opacity',
|
|
||||||
'fill',
|
|
||||||
'fillColor',
|
|
||||||
'fillOpacity',
|
|
||||||
'stroke',
|
|
||||||
'dashArray',
|
|
||||||
'dashOffset',
|
|
||||||
'lineCap',
|
|
||||||
'lineJoin',
|
|
||||||
].includes(property)
|
|
||||||
) {
|
|
||||||
this.leafletObject.setStyle({ [property]: value });
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#num(name: string): number {
|
||||||
|
const v = this.getAttribute(name);
|
||||||
|
return v !== null
|
||||||
|
? Number(v)
|
||||||
|
: (PROPS[name as keyof typeof PROPS] as { default: number }).default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-circle', LeafletCircle);
|
customElements.define('leaflet-circle', LeafletCircle);
|
||||||
|
|||||||
@ -1,44 +1,88 @@
|
|||||||
import {
|
import { ImageOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
|
||||||
ImageOverlay,
|
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
|
||||||
LatLngBounds,
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
LatLngBoundsExpression,
|
|
||||||
LatLngExpression,
|
const PROPS = {
|
||||||
Layer,
|
url: { kind: 'str', attr: 'url', default: '' },
|
||||||
} from 'leaflet';
|
bounds: { kind: 'str', attr: 'bounds', default: '' },
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
alt: { kind: 'str', attr: 'alt', default: '' },
|
||||||
export class LeafletImageOverlay extends LeafletElement {
|
interactive: { kind: 'bool-on', attr: 'interactive' },
|
||||||
|
crossOrigin: { kind: 'str', attr: 'cross-origin', default: '' },
|
||||||
|
errorOverlayUrl: { kind: 'str', attr: 'error-overlay-url', default: '' },
|
||||||
|
zIndex: { kind: 'num', attr: 'z-index', default: 0 },
|
||||||
|
className: { kind: 'str', attr: 'class-name', default: '' },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
const PROP_BY_ATTR = new Map<string, string>(
|
||||||
|
Object.entries(PROPS).map(([name, spec]) => [spec.attr, name]),
|
||||||
|
);
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletImageOverlay extends TypedBase {
|
||||||
|
#obj?: ImageOverlay;
|
||||||
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return [
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
'url',
|
}
|
||||||
'bounds',
|
|
||||||
'opacity',
|
static {
|
||||||
'alt',
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
'interactive',
|
Object.defineProperty(LeafletImageOverlay.prototype, name, {
|
||||||
'cross-origin',
|
get(this: LeafletImageOverlay) {
|
||||||
'error-overlay-url',
|
const val = this.getAttribute(spec.attr);
|
||||||
'z-index',
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
'className',
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
];
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletImageOverlay, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): Layer {
|
connectedCallback() {
|
||||||
const url = this.getAttribute('url') || '';
|
const url = this.getAttribute('url') || '';
|
||||||
const bounds = this.options.bounds as LatLngBoundsExpression;
|
this.#obj = new ImageOverlay(
|
||||||
return new ImageOverlay(url, bounds, this.options);
|
url,
|
||||||
|
this.#parsedBounds(),
|
||||||
|
buildOptions(this, PROPS, ['url', 'bounds']),
|
||||||
|
);
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
if (this.leafletObject instanceof ImageOverlay) {
|
if (!this.#obj) return;
|
||||||
if (property === 'url') {
|
if (name === 'url') {
|
||||||
this.leafletObject.setUrl(value as string);
|
if (val) this.#obj.setUrl(val);
|
||||||
} else if (property === 'bounds') {
|
} else if (name === 'bounds') {
|
||||||
this.leafletObject.setBounds(new LatLngBounds(value as LatLngExpression[]));
|
this.#obj.setBounds(new LatLngBounds(this.#parsedBounds() as LatLngExpression[]));
|
||||||
} else {
|
} else {
|
||||||
super.updateLeafletObject(property, value);
|
const propName = PROP_BY_ATTR.get(name);
|
||||||
|
if (!propName) return;
|
||||||
|
const setter =
|
||||||
|
`set${propName.charAt(0).toUpperCase()}${propName.slice(1)}` as keyof ImageOverlay;
|
||||||
|
if (typeof this.#obj[setter] === 'function') {
|
||||||
|
(this.#obj[setter] as (v: unknown) => void)(parseAttributeValue(val));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#parsedBounds(): LatLngBoundsExpression {
|
||||||
|
const raw = this.getAttribute('bounds');
|
||||||
|
return raw ? (JSON.parse(raw) as LatLngBoundsExpression) : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-image-overlay', LeafletImageOverlay);
|
customElements.define('leaflet-image-overlay', LeafletImageOverlay);
|
||||||
|
|||||||
@ -1,28 +1,83 @@
|
|||||||
import L from 'leaflet';
|
import { Marker } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
lat: { kind: 'num', attr: 'lat', default: 0 },
|
||||||
|
lng: { kind: 'num', attr: 'lng', default: 0 },
|
||||||
|
title: { kind: 'str', attr: 'title', default: '' },
|
||||||
|
alt: { kind: 'str', attr: 'alt', default: '' },
|
||||||
|
draggable: { kind: 'bool-on', attr: 'draggable' },
|
||||||
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
zIndexOffset: { kind: 'num', attr: 'z-index-offset', default: 0 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
const PROP_BY_ATTR = new Map<string, string>(
|
||||||
|
Object.entries(PROPS).map(([name, spec]) => [spec.attr, name]),
|
||||||
|
);
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletMarker extends TypedBase {
|
||||||
|
#obj?: Marker;
|
||||||
|
|
||||||
export class LeafletMarker extends LeafletElement {
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ['lat', 'lng', 'title', 'alt', 'draggable', 'opacity', 'z-index-offset'];
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
static {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
Object.defineProperty(LeafletMarker.prototype, name, {
|
||||||
return new L.Marker([lat, lng], this.options);
|
get(this: LeafletMarker) {
|
||||||
|
const val = this.getAttribute(spec.attr);
|
||||||
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletMarker, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
connectedCallback() {
|
||||||
if (this.leafletObject instanceof L.Marker) {
|
this.#obj = new Marker(
|
||||||
if (property === 'lat' || property === 'lng') {
|
[this.#num('lat'), this.#num('lng')],
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
buildOptions(this, PROPS, ['lat', 'lng']),
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
);
|
||||||
this.leafletObject.setLatLng([lat, lng]);
|
registerWithParent(this, this.#obj);
|
||||||
} else {
|
}
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
|
if (!this.#obj) return;
|
||||||
|
if (name === 'lat' || name === 'lng') {
|
||||||
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
|
} else {
|
||||||
|
const propName = PROP_BY_ATTR.get(name);
|
||||||
|
if (!propName) return;
|
||||||
|
const setter = `set${propName.charAt(0).toUpperCase()}${propName.slice(1)}` as keyof Marker;
|
||||||
|
if (typeof this.#obj[setter] === 'function') {
|
||||||
|
(this.#obj[setter] as (v: unknown) => void)(parseAttributeValue(val));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#num(name: string): number {
|
||||||
|
const v = this.getAttribute(name);
|
||||||
|
return v !== null
|
||||||
|
? Number(v)
|
||||||
|
: (PROPS[name as keyof typeof PROPS] as { default: number }).default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-marker', LeafletMarker);
|
customElements.define('leaflet-marker', LeafletMarker);
|
||||||
|
|||||||
@ -1,68 +1,80 @@
|
|||||||
import L from 'leaflet';
|
import { Polygon } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
import { LeafletLine } from './leaflet-line.js';
|
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
import type { LeafletLine } from './leaflet-line.js';
|
||||||
|
|
||||||
export class LeafletPolygon extends LeafletElement {
|
const PROPS = {
|
||||||
private _observer?: MutationObserver;
|
color: { kind: 'str', attr: 'color', default: '#3388ff' },
|
||||||
static get observedAttributes() {
|
weight: { kind: 'num', attr: 'weight', default: 3 },
|
||||||
return ['color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity'];
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
}
|
fill: { kind: 'bool-on', attr: 'fill' },
|
||||||
|
fillColor: { kind: 'str', attr: 'fill-color', default: '#3388ff' },
|
||||||
|
fillOpacity: { kind: 'num', attr: 'fill-opacity', default: 0.2 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletPolygon extends TypedBase {
|
||||||
|
#obj?: Polygon;
|
||||||
|
#observer?: MutationObserver;
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
static get observedAttributes() {
|
||||||
const coords = this.getCoords();
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
return new L.Polygon(coords, this.options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCoords(): [number, number][] {
|
static {
|
||||||
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
return lines.map((line) => line.latlng);
|
Object.defineProperty(LeafletPolygon.prototype, name, {
|
||||||
|
get(this: LeafletPolygon) {
|
||||||
|
const val = this.getAttribute(spec.attr);
|
||||||
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletPolygon, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
this.#obj = new Polygon(this.#getCoords(), buildOptions(this, PROPS));
|
||||||
this.addEventListener('line-updated', () => {
|
registerWithParent(this, this.#obj);
|
||||||
if (this.leafletObject instanceof L.Polygon) {
|
|
||||||
this.leafletObject.setLatLngs(this.getCoords());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._observer = new MutationObserver(() => {
|
this.addEventListener('line-updated', this.#syncCoords);
|
||||||
if (this.leafletObject instanceof L.Polygon) {
|
this.#observer = new MutationObserver(() => this.#syncCoords());
|
||||||
this.leafletObject.setLatLngs(this.getCoords());
|
this.#observer.observe(this, { childList: true });
|
||||||
}
|
|
||||||
});
|
|
||||||
this._observer.observe(this, { childList: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this._observer?.disconnect();
|
this.#observer?.disconnect();
|
||||||
this._observer = undefined;
|
this.#observer = undefined;
|
||||||
super.disconnectedCallback();
|
this.removeEventListener('line-updated', this.#syncCoords);
|
||||||
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
if (this.leafletObject instanceof L.Polygon) {
|
if (!this.#obj) return;
|
||||||
if (
|
if (isPathStyleAttr(name)) {
|
||||||
[
|
updatePathStyle(this.#obj, name, val);
|
||||||
'color',
|
|
||||||
'weight',
|
|
||||||
'opacity',
|
|
||||||
'fill',
|
|
||||||
'fillColor',
|
|
||||||
'fillOpacity',
|
|
||||||
'stroke',
|
|
||||||
'dashArray',
|
|
||||||
'dashOffset',
|
|
||||||
'lineCap',
|
|
||||||
'lineJoin',
|
|
||||||
].includes(property)
|
|
||||||
) {
|
|
||||||
this.leafletObject.setStyle({ [property]: value });
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#syncCoords = () => {
|
||||||
|
this.#obj?.setLatLngs(this.#getCoords());
|
||||||
|
};
|
||||||
|
|
||||||
|
#getCoords(): [number, number][] {
|
||||||
|
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
||||||
|
return lines.map((line) => line.latlng);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-polygon', LeafletPolygon);
|
customElements.define('leaflet-polygon', LeafletPolygon);
|
||||||
|
|||||||
@ -1,68 +1,80 @@
|
|||||||
import L from 'leaflet';
|
import { Polyline } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
import { LeafletLine } from './leaflet-line.js';
|
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
import type { LeafletLine } from './leaflet-line.js';
|
||||||
|
|
||||||
export class LeafletPolyline extends LeafletElement {
|
const PROPS = {
|
||||||
private _observer?: MutationObserver;
|
color: { kind: 'str', attr: 'color', default: '#3388ff' },
|
||||||
static get observedAttributes() {
|
weight: { kind: 'num', attr: 'weight', default: 3 },
|
||||||
return ['color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity'];
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
}
|
fill: { kind: 'bool-on', attr: 'fill' },
|
||||||
|
fillColor: { kind: 'str', attr: 'fill-color', default: '#3388ff' },
|
||||||
|
fillOpacity: { kind: 'num', attr: 'fill-opacity', default: 0.2 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletPolyline extends TypedBase {
|
||||||
|
#obj?: Polyline;
|
||||||
|
#observer?: MutationObserver;
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
static get observedAttributes() {
|
||||||
const coords = this.getCoords();
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
return new L.Polyline(coords, this.options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCoords(): [number, number][] {
|
static {
|
||||||
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
return lines.map((line) => line.latlng);
|
Object.defineProperty(LeafletPolyline.prototype, name, {
|
||||||
|
get(this: LeafletPolyline) {
|
||||||
|
const val = this.getAttribute(spec.attr);
|
||||||
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletPolyline, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
this.#obj = new Polyline(this.#getCoords(), buildOptions(this, PROPS));
|
||||||
this.addEventListener('line-updated', () => {
|
registerWithParent(this, this.#obj);
|
||||||
if (this.leafletObject instanceof L.Polyline) {
|
|
||||||
this.leafletObject.setLatLngs(this.getCoords());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._observer = new MutationObserver(() => {
|
this.addEventListener('line-updated', this.#syncCoords);
|
||||||
if (this.leafletObject instanceof L.Polyline) {
|
this.#observer = new MutationObserver(() => this.#syncCoords());
|
||||||
this.leafletObject.setLatLngs(this.getCoords());
|
this.#observer.observe(this, { childList: true });
|
||||||
}
|
|
||||||
});
|
|
||||||
this._observer.observe(this, { childList: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this._observer?.disconnect();
|
this.#observer?.disconnect();
|
||||||
this._observer = undefined;
|
this.#observer = undefined;
|
||||||
super.disconnectedCallback();
|
this.removeEventListener('line-updated', this.#syncCoords);
|
||||||
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
if (this.leafletObject instanceof L.Polyline) {
|
if (!this.#obj) return;
|
||||||
if (
|
if (isPathStyleAttr(name)) {
|
||||||
[
|
updatePathStyle(this.#obj, name, val);
|
||||||
'color',
|
|
||||||
'weight',
|
|
||||||
'opacity',
|
|
||||||
'fill',
|
|
||||||
'fillColor',
|
|
||||||
'fillOpacity',
|
|
||||||
'stroke',
|
|
||||||
'dashArray',
|
|
||||||
'dashOffset',
|
|
||||||
'lineCap',
|
|
||||||
'lineJoin',
|
|
||||||
].includes(property)
|
|
||||||
) {
|
|
||||||
this.leafletObject.setStyle({ [property]: value });
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#syncCoords = () => {
|
||||||
|
this.#obj?.setLatLngs(this.#getCoords());
|
||||||
|
};
|
||||||
|
|
||||||
|
#getCoords(): [number, number][] {
|
||||||
|
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
||||||
|
return lines.map((line) => line.latlng);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-polyline', LeafletPolyline);
|
customElements.define('leaflet-polyline', LeafletPolyline);
|
||||||
|
|||||||
@ -1,59 +1,85 @@
|
|||||||
import L from 'leaflet';
|
import { Popup } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
lat: { kind: 'num', attr: 'lat', default: 0 },
|
||||||
|
lng: { kind: 'num', attr: 'lng', default: 0 },
|
||||||
|
maxWidth: { kind: 'num', attr: 'max-width', default: 300 },
|
||||||
|
minWidth: { kind: 'num', attr: 'min-width', default: 50 },
|
||||||
|
maxHeight: { kind: 'num', attr: 'max-height', default: 0 },
|
||||||
|
autoPan: { kind: 'bool-on', attr: 'auto-pan' },
|
||||||
|
closeButton: { kind: 'bool-on', attr: 'close-button' },
|
||||||
|
autoClose: { kind: 'bool-on', attr: 'auto-close' },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletPopup extends TypedBase {
|
||||||
|
#obj?: Popup;
|
||||||
|
#observer?: MutationObserver;
|
||||||
|
|
||||||
export class LeafletPopup extends LeafletElement {
|
|
||||||
private _observer?: MutationObserver;
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return [
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
'lat',
|
|
||||||
'lng',
|
|
||||||
'max-width',
|
|
||||||
'min-width',
|
|
||||||
'max-height',
|
|
||||||
'auto-pan',
|
|
||||||
'close-button',
|
|
||||||
'auto-close',
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
static {
|
||||||
const lat = this.getAttribute('lat');
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
const lng = this.getAttribute('lng');
|
Object.defineProperty(LeafletPopup.prototype, name, {
|
||||||
const options = { ...this.options, content: this.innerHTML };
|
get(this: LeafletPopup) {
|
||||||
const popup = new L.Popup(options);
|
const val = this.getAttribute(spec.attr);
|
||||||
if (lat && lng) {
|
if ('default' in spec && spec.kind === 'num')
|
||||||
popup.setLatLng([parseFloat(lat), parseFloat(lng)]);
|
return val !== null ? Number(val) : spec.default;
|
||||||
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? ('default' in spec ? (spec as { default: string }).default : '');
|
||||||
|
},
|
||||||
|
set(this: LeafletPopup, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return popup;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
this.#obj = new Popup({
|
||||||
this._observer = new MutationObserver(() => {
|
...buildOptions(this, PROPS, ['lat', 'lng']),
|
||||||
if (this.leafletObject instanceof L.Popup) {
|
content: this.innerHTML,
|
||||||
this.leafletObject.setContent(this.innerHTML);
|
});
|
||||||
}
|
if (this.hasAttribute('lat') && this.hasAttribute('lng')) {
|
||||||
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
|
}
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
|
|
||||||
|
this.#observer = new MutationObserver(() => {
|
||||||
|
this.#obj?.setContent(this.innerHTML);
|
||||||
});
|
});
|
||||||
this._observer.observe(this, { childList: true, characterData: true, subtree: true });
|
this.#observer.observe(this, { childList: true, characterData: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this._observer?.disconnect();
|
this.#observer?.disconnect();
|
||||||
this._observer = undefined;
|
this.#observer = undefined;
|
||||||
super.disconnectedCallback();
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string) {
|
||||||
if (this.leafletObject instanceof L.Popup) {
|
if (!this.#obj) return;
|
||||||
if (property === 'lat' || property === 'lng') {
|
if (name === 'lat' || name === 'lng') {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
|
||||||
this.leafletObject.setLatLng([lat, lng]);
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#num(name: string): number {
|
||||||
|
const v = this.getAttribute(name);
|
||||||
|
return v !== null
|
||||||
|
? Number(v)
|
||||||
|
: (PROPS[name as keyof typeof PROPS] as { default: number }).default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-popup', LeafletPopup);
|
customElements.define('leaflet-popup', LeafletPopup);
|
||||||
|
|||||||
@ -1,41 +1,70 @@
|
|||||||
import L from 'leaflet';
|
import { Rectangle, LatLngBoundsExpression } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
|
import { isPathStyleAttr, updatePathStyle } from '../core/path-style.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
bounds: { kind: 'str', attr: 'bounds', default: '' },
|
||||||
|
color: { kind: 'str', attr: 'color', default: '#3388ff' },
|
||||||
|
weight: { kind: 'num', attr: 'weight', default: 3 },
|
||||||
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
fill: { kind: 'bool-on', attr: 'fill' },
|
||||||
|
fillColor: { kind: 'str', attr: 'fill-color', default: '#3388ff' },
|
||||||
|
fillOpacity: { kind: 'num', attr: 'fill-opacity', default: 0.2 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletRectangle extends TypedBase {
|
||||||
|
#obj?: Rectangle;
|
||||||
|
|
||||||
export class LeafletRectangle extends LeafletElement {
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ['bounds', 'color', 'weight', 'opacity', 'fill', 'fill-color', 'fill-opacity'];
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
|
}
|
||||||
|
|
||||||
|
static {
|
||||||
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
|
Object.defineProperty(LeafletRectangle.prototype, name, {
|
||||||
|
get(this: LeafletRectangle) {
|
||||||
|
const val = this.getAttribute(spec.attr);
|
||||||
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletRectangle, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.#obj = new Rectangle(this.#parsedBounds(), buildOptions(this, PROPS, ['bounds']));
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
disconnectedCallback() {
|
||||||
const bounds = this.options.bounds as L.LatLngBoundsExpression;
|
this.#obj?.remove();
|
||||||
return new L.Rectangle(bounds, this.options);
|
this.#obj = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
if (this.leafletObject instanceof L.Rectangle) {
|
if (!this.#obj) return;
|
||||||
if (property === 'bounds') {
|
if (name === 'bounds') {
|
||||||
this.leafletObject.setBounds(value as L.LatLngBoundsExpression);
|
this.#obj.setBounds(this.#parsedBounds());
|
||||||
} else if (
|
} else if (isPathStyleAttr(name)) {
|
||||||
[
|
updatePathStyle(this.#obj, name, val);
|
||||||
'color',
|
|
||||||
'weight',
|
|
||||||
'opacity',
|
|
||||||
'fill',
|
|
||||||
'fillColor',
|
|
||||||
'fillOpacity',
|
|
||||||
'stroke',
|
|
||||||
'dashArray',
|
|
||||||
'dashOffset',
|
|
||||||
'lineCap',
|
|
||||||
'lineJoin',
|
|
||||||
].includes(property)
|
|
||||||
) {
|
|
||||||
this.leafletObject.setStyle({ [property]: value });
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#parsedBounds(): LatLngBoundsExpression {
|
||||||
|
const raw = this.getAttribute('bounds');
|
||||||
|
return raw ? (JSON.parse(raw) as LatLngBoundsExpression) : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-rectangle', LeafletRectangle);
|
customElements.define('leaflet-rectangle', LeafletRectangle);
|
||||||
|
|||||||
@ -1,31 +1,84 @@
|
|||||||
import L from 'leaflet';
|
import { SVGOverlay, LatLngBounds, LatLngBoundsExpression, LatLngExpression } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions, parseAttributeValue } from '../core/utils.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
bounds: { kind: 'str', attr: 'bounds', default: '' },
|
||||||
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
interactive: { kind: 'bool-on', attr: 'interactive' },
|
||||||
|
crossOrigin: { kind: 'str', attr: 'cross-origin', default: '' },
|
||||||
|
zIndex: { kind: 'num', attr: 'z-index', default: 0 },
|
||||||
|
className: { kind: 'str', attr: 'class-name', default: '' },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
const PROP_BY_ATTR = new Map<string, string>(
|
||||||
|
Object.entries(PROPS).map(([name, spec]) => [spec.attr, name]),
|
||||||
|
);
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletSVGOverlay extends TypedBase {
|
||||||
|
#obj?: SVGOverlay;
|
||||||
|
|
||||||
export class LeafletSVGOverlay extends LeafletElement {
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ['bounds', 'opacity', 'alt', 'interactive', 'cross-origin', 'z-index', 'className'];
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
static {
|
||||||
const svg = this.querySelector('svg');
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
const bounds = this.options.bounds as L.LatLngBoundsExpression;
|
Object.defineProperty(LeafletSVGOverlay.prototype, name, {
|
||||||
if (!svg) {
|
get(this: LeafletSVGOverlay) {
|
||||||
// Create a dummy SVG if none provided
|
const val = this.getAttribute(spec.attr);
|
||||||
const dummy = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
return new L.SVGOverlay(dummy, bounds, this.options);
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletSVGOverlay, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return new L.SVGOverlay(svg, bounds, this.options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
connectedCallback() {
|
||||||
if (this.leafletObject instanceof L.SVGOverlay) {
|
const svg = this.querySelector('svg');
|
||||||
if (property === 'bounds') {
|
const dummy = !svg ? document.createElementNS('http://www.w3.org/2000/svg', 'svg') : undefined;
|
||||||
this.leafletObject.setBounds(new L.LatLngBounds(value as L.LatLngExpression[]));
|
this.#obj = new SVGOverlay(
|
||||||
} else {
|
svg ?? dummy!,
|
||||||
super.updateLeafletObject(property, value);
|
this.#parsedBounds(),
|
||||||
|
buildOptions(this, PROPS, ['bounds']),
|
||||||
|
);
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback() {
|
||||||
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, _old: string | null, val: string | null) {
|
||||||
|
if (!this.#obj) return;
|
||||||
|
if (name === 'bounds') {
|
||||||
|
this.#obj.setBounds(new LatLngBounds(this.#parsedBounds() as LatLngExpression[]));
|
||||||
|
} else {
|
||||||
|
const propName = PROP_BY_ATTR.get(name);
|
||||||
|
if (!propName) return;
|
||||||
|
const setter =
|
||||||
|
`set${propName.charAt(0).toUpperCase()}${propName.slice(1)}` as keyof SVGOverlay;
|
||||||
|
if (typeof this.#obj[setter] === 'function') {
|
||||||
|
(this.#obj[setter] as (v: unknown) => void)(parseAttributeValue(val));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#parsedBounds(): LatLngBoundsExpression {
|
||||||
|
const raw = this.getAttribute('bounds');
|
||||||
|
return raw ? (JSON.parse(raw) as LatLngBoundsExpression) : [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-svg-overlay', LeafletSVGOverlay);
|
customElements.define('leaflet-svg-overlay', LeafletSVGOverlay);
|
||||||
|
|||||||
@ -1,50 +1,84 @@
|
|||||||
import L from 'leaflet';
|
import { Tooltip } from 'leaflet';
|
||||||
import { LeafletElement } from '../core/LeafletElement.js';
|
import { registerWithParent, buildOptions } from '../core/utils.js';
|
||||||
|
import type { PropDef, PropTypesFromTable } from '../types/props.js';
|
||||||
|
|
||||||
|
const PROPS = {
|
||||||
|
lat: { kind: 'num', attr: 'lat', default: 0 },
|
||||||
|
lng: { kind: 'num', attr: 'lng', default: 0 },
|
||||||
|
pane: { kind: 'str', attr: 'pane', default: '' },
|
||||||
|
offset: { kind: 'str', attr: 'offset', default: '' },
|
||||||
|
direction: { kind: 'str', attr: 'direction', default: 'auto' },
|
||||||
|
permanent: { kind: 'bool-on', attr: 'permanent' },
|
||||||
|
sticky: { kind: 'bool-on', attr: 'sticky' },
|
||||||
|
opacity: { kind: 'num', attr: 'opacity', default: 1.0 },
|
||||||
|
} satisfies Record<string, PropDef>;
|
||||||
|
|
||||||
|
type PropTypes = PropTypesFromTable<typeof PROPS>;
|
||||||
|
const TypedBase = HTMLElement as unknown as new () => HTMLElement & PropTypes;
|
||||||
|
|
||||||
|
export class LeafletTooltip extends TypedBase {
|
||||||
|
#obj?: Tooltip;
|
||||||
|
#observer?: MutationObserver;
|
||||||
|
|
||||||
export class LeafletTooltip extends LeafletElement {
|
|
||||||
private _observer?: MutationObserver;
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ['lat', 'lng', 'pane', 'offset', 'direction', 'permanent', 'sticky', 'opacity'];
|
return Object.values(PROPS).map((s) => s.attr);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createLeafletObject(): L.Layer {
|
static {
|
||||||
const lat = this.getAttribute('lat');
|
for (const [name, spec] of Object.entries(PROPS)) {
|
||||||
const lng = this.getAttribute('lng');
|
Object.defineProperty(LeafletTooltip.prototype, name, {
|
||||||
const options = { ...this.options, content: this.innerHTML };
|
get(this: LeafletTooltip) {
|
||||||
const tooltip = new L.Tooltip(options);
|
const val = this.getAttribute(spec.attr);
|
||||||
if (lat && lng) {
|
if (spec.kind === 'num') return val !== null ? Number(val) : spec.default;
|
||||||
tooltip.setLatLng([parseFloat(lat), parseFloat(lng)]);
|
if (spec.kind === 'bool-on') return this.hasAttribute(spec.attr);
|
||||||
|
return val ?? spec.default;
|
||||||
|
},
|
||||||
|
set(this: LeafletTooltip, v: number | string | boolean) {
|
||||||
|
if (spec.kind === 'bool-on') this.toggleAttribute(spec.attr, !!v);
|
||||||
|
else this.setAttribute(spec.attr, String(v));
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return tooltip;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
super.connectedCallback();
|
this.#obj = new Tooltip({
|
||||||
this._observer = new MutationObserver(() => {
|
...buildOptions(this, PROPS, ['lat', 'lng']),
|
||||||
if (this.leafletObject instanceof L.Tooltip) {
|
content: this.innerHTML,
|
||||||
this.leafletObject.setContent(this.innerHTML);
|
});
|
||||||
}
|
if (this.hasAttribute('lat') && this.hasAttribute('lng')) {
|
||||||
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
|
}
|
||||||
|
registerWithParent(this, this.#obj);
|
||||||
|
|
||||||
|
this.#observer = new MutationObserver(() => {
|
||||||
|
this.#obj?.setContent(this.innerHTML);
|
||||||
});
|
});
|
||||||
this._observer.observe(this, { childList: true, characterData: true, subtree: true });
|
this.#observer.observe(this, { childList: true, characterData: true, subtree: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
this._observer?.disconnect();
|
this.#observer?.disconnect();
|
||||||
this._observer = undefined;
|
this.#observer = undefined;
|
||||||
super.disconnectedCallback();
|
this.#obj?.remove();
|
||||||
|
this.#obj = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
attributeChangedCallback(name: string) {
|
||||||
if (this.leafletObject instanceof L.Tooltip) {
|
if (!this.#obj) return;
|
||||||
if (property === 'lat' || property === 'lng') {
|
if (name === 'lat' || name === 'lng') {
|
||||||
const lat = parseFloat(this.getAttribute('lat') || '0');
|
this.#obj.setLatLng([this.#num('lat'), this.#num('lng')]);
|
||||||
const lng = parseFloat(this.getAttribute('lng') || '0');
|
|
||||||
this.leafletObject.setLatLng([lat, lng]);
|
|
||||||
} else {
|
|
||||||
super.updateLeafletObject(property, value);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#num(name: string): number {
|
||||||
|
const v = this.getAttribute(name);
|
||||||
|
return v !== null
|
||||||
|
? Number(v)
|
||||||
|
: (PROPS[name as keyof typeof PROPS] as { default: number }).default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define('leaflet-tooltip', LeafletTooltip);
|
customElements.define('leaflet-tooltip', LeafletTooltip);
|
||||||
|
|||||||
@ -1,36 +0,0 @@
|
|||||||
import { Control } from 'leaflet';
|
|
||||||
|
|
||||||
export abstract class LeafletControl extends HTMLElement {
|
|
||||||
protected control?: Control;
|
|
||||||
|
|
||||||
static get observedAttributes(): string[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.control = this.createControl();
|
|
||||||
this.register();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
this.control?.remove();
|
|
||||||
this.control = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract createControl(): Control;
|
|
||||||
|
|
||||||
protected register() {
|
|
||||||
if (this.control) {
|
|
||||||
this.dispatchEvent(
|
|
||||||
new CustomEvent('leaflet-register', {
|
|
||||||
detail: {
|
|
||||||
leafletObject: this.control,
|
|
||||||
element: this,
|
|
||||||
},
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,141 +0,0 @@
|
|||||||
import { Layer, Control, Popup, Tooltip, LayerGroup } from 'leaflet';
|
|
||||||
|
|
||||||
export interface LeafletRegisterEvent extends CustomEvent {
|
|
||||||
detail: {
|
|
||||||
leafletObject: Layer | Control;
|
|
||||||
element: HTMLElement;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export abstract class LeafletElement extends HTMLElement {
|
|
||||||
protected leafletObject?: Layer;
|
|
||||||
protected options: Record<string, unknown> = {};
|
|
||||||
protected _parentLayer?: Layer;
|
|
||||||
protected _parentBindingType?: 'popup' | 'tooltip' | 'layer';
|
|
||||||
private _registerHandler?: EventListener;
|
|
||||||
|
|
||||||
static get observedAttributes(): string[] {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
connectedCallback() {
|
|
||||||
this.initOptions();
|
|
||||||
this.leafletObject = this.createLeafletObject();
|
|
||||||
|
|
||||||
this._registerHandler = ((e: LeafletRegisterEvent) => {
|
|
||||||
if (this.leafletObject) {
|
|
||||||
const childObj = e.detail.leafletObject;
|
|
||||||
const childEl = e.detail.element;
|
|
||||||
if (childObj instanceof Popup) {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.leafletObject.bindPopup(childObj);
|
|
||||||
if (childEl instanceof LeafletElement) {
|
|
||||||
childEl._parentLayer = this.leafletObject;
|
|
||||||
childEl._parentBindingType = 'popup';
|
|
||||||
}
|
|
||||||
} else if (childObj instanceof Tooltip) {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.leafletObject.bindTooltip(childObj);
|
|
||||||
if (childEl instanceof LeafletElement) {
|
|
||||||
childEl._parentLayer = this.leafletObject;
|
|
||||||
childEl._parentBindingType = 'tooltip';
|
|
||||||
}
|
|
||||||
} else if (childObj instanceof Layer && 'addLayer' in this.leafletObject) {
|
|
||||||
e.stopPropagation();
|
|
||||||
(this.leafletObject as LayerGroup).addLayer(childObj);
|
|
||||||
if (childEl instanceof LeafletElement) {
|
|
||||||
childEl._parentLayer = this.leafletObject;
|
|
||||||
childEl._parentBindingType = 'layer';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) as EventListener;
|
|
||||||
this.addEventListener('leaflet-register', this._registerHandler);
|
|
||||||
|
|
||||||
this.register();
|
|
||||||
}
|
|
||||||
|
|
||||||
disconnectedCallback() {
|
|
||||||
if (this._registerHandler) {
|
|
||||||
this.removeEventListener('leaflet-register', this._registerHandler);
|
|
||||||
this._registerHandler = undefined;
|
|
||||||
}
|
|
||||||
if (!this.leafletObject) return;
|
|
||||||
if (this._parentBindingType === 'popup' && this._parentLayer) {
|
|
||||||
this._parentLayer.unbindPopup();
|
|
||||||
} else if (this._parentBindingType === 'tooltip' && this._parentLayer) {
|
|
||||||
this._parentLayer.unbindTooltip();
|
|
||||||
} else if (
|
|
||||||
this._parentBindingType === 'layer' &&
|
|
||||||
this._parentLayer &&
|
|
||||||
'removeLayer' in this._parentLayer
|
|
||||||
) {
|
|
||||||
(this._parentLayer as LayerGroup).removeLayer(this.leafletObject);
|
|
||||||
} else {
|
|
||||||
this.leafletObject.remove();
|
|
||||||
}
|
|
||||||
this._parentLayer = undefined;
|
|
||||||
this._parentBindingType = undefined;
|
|
||||||
this.leafletObject = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected initOptions() {
|
|
||||||
const observed = (this.constructor as typeof LeafletElement).observedAttributes;
|
|
||||||
observed.forEach((attr) => {
|
|
||||||
const val = this.getAttribute(attr);
|
|
||||||
if (val !== null) {
|
|
||||||
this.options[camelCase(attr)] = parseAttributeValue(val);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
||||||
if (oldValue === newValue) return;
|
|
||||||
const propertyName = camelCase(name);
|
|
||||||
const parsedValue = parseAttributeValue(newValue);
|
|
||||||
this.options[propertyName] = parsedValue;
|
|
||||||
|
|
||||||
if (this.leafletObject) {
|
|
||||||
this.updateLeafletObject(propertyName, parsedValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected abstract createLeafletObject(): Layer;
|
|
||||||
|
|
||||||
protected updateLeafletObject(property: string, value: unknown) {
|
|
||||||
const setter = `set${property.charAt(0).toUpperCase()}${property.slice(1)}`;
|
|
||||||
const obj = this.leafletObject as unknown as Record<string, unknown>;
|
|
||||||
if (obj && typeof obj[setter] === 'function') {
|
|
||||||
(obj[setter] as (val: unknown) => void)(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected register() {
|
|
||||||
if (!this.leafletObject) return;
|
|
||||||
const event = new CustomEvent('leaflet-register', {
|
|
||||||
detail: {
|
|
||||||
leafletObject: this.leafletObject,
|
|
||||||
element: this,
|
|
||||||
},
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
this.dispatchEvent(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function camelCase(str: string): string {
|
|
||||||
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAttributeValue(value: string): unknown {
|
|
||||||
if (value === 'true') return true;
|
|
||||||
if (value === 'false') return false;
|
|
||||||
const num = Number(value);
|
|
||||||
if (!isNaN(num) && value !== '') return num;
|
|
||||||
try {
|
|
||||||
return JSON.parse(value);
|
|
||||||
} catch {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,26 @@
|
|||||||
|
import { Path } from 'leaflet';
|
||||||
|
import { parseAttributeValue } from './utils.js';
|
||||||
|
|
||||||
|
export const PATH_STYLE_ATTRS = new Set([
|
||||||
|
'color',
|
||||||
|
'weight',
|
||||||
|
'opacity',
|
||||||
|
'fill',
|
||||||
|
'fill-color',
|
||||||
|
'fill-opacity',
|
||||||
|
'stroke',
|
||||||
|
'dash-array',
|
||||||
|
'dash-offset',
|
||||||
|
'line-cap',
|
||||||
|
'line-join',
|
||||||
|
'fill-rule',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function isPathStyleAttr(name: string): boolean {
|
||||||
|
return PATH_STYLE_ATTRS.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePathStyle(obj: Path, name: string, value: string | null) {
|
||||||
|
const key = name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
|
obj.setStyle({ [key]: parseAttributeValue(value) });
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
import { Layer, LayerGroup, Popup, Tooltip } from 'leaflet';
|
||||||
|
|
||||||
|
export interface LeafletRegisterEvent extends CustomEvent {
|
||||||
|
detail: {
|
||||||
|
leafletObject: Layer;
|
||||||
|
element: HTMLElement;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChildEntry = {
|
||||||
|
type: 'layer' | 'popup' | 'tooltip';
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createChildRegisterHandler(
|
||||||
|
container: LayerGroup,
|
||||||
|
children: globalThis.Map<HTMLElement, ChildEntry>,
|
||||||
|
) {
|
||||||
|
return (e: LeafletRegisterEvent) => {
|
||||||
|
const obj = e.detail.leafletObject;
|
||||||
|
const el = e.detail.element;
|
||||||
|
if (obj instanceof Popup) {
|
||||||
|
e.stopPropagation();
|
||||||
|
container.bindPopup(obj);
|
||||||
|
children.set(el, { type: 'popup' });
|
||||||
|
} else if (obj instanceof Tooltip) {
|
||||||
|
e.stopPropagation();
|
||||||
|
container.bindTooltip(obj);
|
||||||
|
children.set(el, { type: 'tooltip' });
|
||||||
|
} else if (obj instanceof Layer && 'addLayer' in container) {
|
||||||
|
e.stopPropagation();
|
||||||
|
container.addLayer(obj);
|
||||||
|
children.set(el, { type: 'layer' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
import type { PropDef } from '../types/props.js';
|
||||||
|
|
||||||
|
export function camelCase(str: string): string {
|
||||||
|
return str.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseAttributeValue(value: string | null): unknown {
|
||||||
|
if (value === null) return null;
|
||||||
|
if (value === 'true') return true;
|
||||||
|
if (value === 'false') return false;
|
||||||
|
const num = Number(value);
|
||||||
|
if (!isNaN(num) && value !== '') return num;
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildOptions(
|
||||||
|
el: HTMLElement,
|
||||||
|
props: Record<string, PropDef>,
|
||||||
|
exclude: string[] = [],
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const opts: Record<string, unknown> = {};
|
||||||
|
for (const [propName, spec] of Object.entries(props)) {
|
||||||
|
if (exclude.includes(propName)) continue;
|
||||||
|
const val = el.getAttribute(spec.attr);
|
||||||
|
if (val === null) {
|
||||||
|
if ('default' in spec) opts[propName] = spec.default;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (spec.kind === 'num') opts[propName] = Number(val);
|
||||||
|
else if (spec.kind === 'bool-on') opts[propName] = true;
|
||||||
|
else if (spec.kind === 'bool-off') opts[propName] = false;
|
||||||
|
else opts[propName] = val;
|
||||||
|
}
|
||||||
|
return opts;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerWithParent(el: HTMLElement, obj: unknown) {
|
||||||
|
el.dispatchEvent(
|
||||||
|
new CustomEvent('leaflet-register', {
|
||||||
|
detail: { leafletObject: obj, element: el },
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateViaSetter(obj: Record<string, unknown>, name: string, value: unknown) {
|
||||||
|
const prop = camelCase(name);
|
||||||
|
const setter = `set${prop.charAt(0).toUpperCase()}${prop.slice(1)}`;
|
||||||
|
if (typeof obj[setter] === 'function') {
|
||||||
|
(obj[setter] as (v: unknown) => void)(value ?? parseAttributeValue(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
export type NumProp = {
|
||||||
|
kind: 'num';
|
||||||
|
attr: string;
|
||||||
|
default: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StrProp = {
|
||||||
|
kind: 'str';
|
||||||
|
attr: string;
|
||||||
|
default: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BoolOnProp = {
|
||||||
|
kind: 'bool-on';
|
||||||
|
attr: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BoolOffProp = {
|
||||||
|
kind: 'bool-off';
|
||||||
|
attr: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PropDef = NumProp | StrProp | BoolOnProp | BoolOffProp;
|
||||||
|
|
||||||
|
export type PropTypeOf<T extends PropDef> = T extends NumProp
|
||||||
|
? number
|
||||||
|
: T extends StrProp
|
||||||
|
? string
|
||||||
|
: boolean;
|
||||||
|
|
||||||
|
export type PropTypesFromTable<T extends Record<string, PropDef>> = {
|
||||||
|
[K in keyof T]: PropTypeOf<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function attrToPropName<P extends Record<string, PropDef>>(
|
||||||
|
props: P,
|
||||||
|
attr: string,
|
||||||
|
): keyof P | undefined {
|
||||||
|
for (const [name, spec] of Object.entries(props) as [keyof P, PropDef][]) {
|
||||||
|
if (spec.attr === attr) return name;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue