refactor: enforce strict TypeScript linting rules
Upgrade ESLint config to use flat config with @typescript-eslint/strict-type-checked and stylistic-type-checked presets. - Use defineConfig helper, scope to .ts files only - Replace || with ??, convert type to interface, remove redundant type assertions and non-null assertions - Annotate intentional violations (unbound-method, non-null-assertion) with eslint-disable - Use querySelectorAll<T> for typed selection, Map over globalThis.Map - Add @eslint/js dev dependency
This commit is contained in:
parent
5b0c3ecdb7
commit
e2382f1117
@ -1,24 +1,43 @@
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
import prettierConfig from 'eslint-config-prettier';
|
||||
|
||||
export default [
|
||||
export default defineConfig([
|
||||
{
|
||||
files: ['src/**/*.ts'],
|
||||
ignores: ['dist/'],
|
||||
},
|
||||
|
||||
...tsPlugin.configs['flat/strict-type-checked'].map(c => ({
|
||||
...c,
|
||||
files: ['**/*.ts'],
|
||||
})),
|
||||
|
||||
...tsPlugin.configs['flat/stylistic-type-checked'].map(c => ({
|
||||
...c,
|
||||
files: ['**/*.ts'],
|
||||
})),
|
||||
|
||||
|
||||
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
ecmaVersion: 2020,
|
||||
sourceType: 'module',
|
||||
parserOptions: { project: true },
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': tsPlugin,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
rules: {
|
||||
...tsPlugin.configs.recommended.rules,
|
||||
...prettierConfig.rules,
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/restrict-template-expressions': ['error', { allowNumber: true }],
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
22
package-lock.json
generated
22
package-lock.json
generated
@ -12,6 +12,7 @@
|
||||
"leaflet": "1.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rollup/plugin-terser": "^1.0.0",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/leaflet": "1.9.21",
|
||||
@ -124,6 +125,27 @@
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/js": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
|
||||
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://eslint.org/donate"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^10.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@eslint/object-schema": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
"leaflet": "1.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@rollup/plugin-terser": "^1.0.0",
|
||||
"@rollup/plugin-typescript": "^12.3.0",
|
||||
"@types/leaflet": "1.9.21",
|
||||
|
||||
@ -24,11 +24,15 @@ export class LeafletCircleMarker extends WithProps(HTMLElement, PROPS) {
|
||||
[numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')],
|
||||
buildOptions(this, PROPS, ['lat', 'lng']),
|
||||
);
|
||||
// The `on` method binds the function to `this`, we can ignore the error
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj.on('move', this.#onMove, this);
|
||||
registerChildren(this, this.#obj);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// The `off` method binds the function to `this`, we can ignore the error
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj?.off('move', this.#onMove, this);
|
||||
unregisterChildren(this);
|
||||
this.#obj?.remove();
|
||||
|
||||
@ -24,11 +24,15 @@ export class LeafletCircle extends WithProps(HTMLElement, PROPS) {
|
||||
[numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')],
|
||||
buildOptions(this, PROPS, ['lat', 'lng']),
|
||||
);
|
||||
// the on method binds the function to `this`, we can ignore the error
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj.on('move', this.#onMove, this);
|
||||
registerChildren(this, this.#obj);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// the off method binds the function to `this`, we can ignore the error
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj?.off('move', this.#onMove, this);
|
||||
unregisterChildren(this);
|
||||
this.#obj?.remove();
|
||||
|
||||
@ -65,11 +65,14 @@ export class LeafletControlLayers extends WithProps(HTMLElement, PROPS) {
|
||||
const el = e.detail.element;
|
||||
const layer = e.detail.leafletObject;
|
||||
const name = el.getAttribute('name');
|
||||
if (!name || !layer) return;
|
||||
if (!name) return;
|
||||
const base = el.getAttribute('type') === 'base';
|
||||
const active = el.hasAttribute('active');
|
||||
if (base) this.#obj!.addBaseLayer(layer, name);
|
||||
else this.#obj!.addOverlay(layer, name);
|
||||
if (base) {
|
||||
this.#obj?.addBaseLayer(layer, name);
|
||||
} else {
|
||||
this.#obj?.addOverlay(layer, name);
|
||||
}
|
||||
if (active) {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('leaflet-add-layer', { bubbles: true, detail: { layer } }),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { DivIcon, DivIconOptions, Icon } from 'leaflet';
|
||||
import { DivIcon, DivIconOptions } from 'leaflet';
|
||||
import { defineProps, str } from '../core/props.ts';
|
||||
import { emitIconChanged } from '../core/register.ts';
|
||||
|
||||
@ -12,10 +12,16 @@ const PROPS = defineProps({
|
||||
bgPos: str(),
|
||||
});
|
||||
|
||||
const JSON_KEYS = new Set(['iconSize', 'iconAnchor', 'popupAnchor', 'tooltipAnchor', 'bgPos']);
|
||||
const JSON_KEYS = new Set<keyof typeof PROPS>([
|
||||
'iconSize',
|
||||
'iconAnchor',
|
||||
'popupAnchor',
|
||||
'tooltipAnchor',
|
||||
'bgPos',
|
||||
]);
|
||||
|
||||
export class LeafletDivIcon extends HTMLElement {
|
||||
#obj?: Icon;
|
||||
#obj?: DivIcon;
|
||||
#observer?: MutationObserver;
|
||||
|
||||
connectedCallback() {
|
||||
@ -54,15 +60,17 @@ export class LeafletDivIcon extends HTMLElement {
|
||||
}
|
||||
|
||||
#applyIcon() {
|
||||
const opts: Record<string, unknown> = {};
|
||||
const opts: Partial<DivIconOptions> = {};
|
||||
for (const [key, spec] of Object.entries(PROPS)) {
|
||||
const v = this.getAttribute(spec.attr);
|
||||
if (v === null) continue;
|
||||
opts[key] = JSON_KEYS.has(key) ? JSON.parse(v) : v;
|
||||
const typedKey = key as keyof typeof PROPS;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
opts[typedKey as keyof DivIconOptions] = JSON_KEYS.has(typedKey) ? JSON.parse(v) : v;
|
||||
}
|
||||
const content = this.innerHTML;
|
||||
if (content) opts.html = content;
|
||||
this.#obj = new DivIcon(opts as DivIconOptions) as unknown as Icon;
|
||||
this.#obj = new DivIcon(opts);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ export class LeafletFeatureGroup extends HTMLElement {
|
||||
for (const [el, type] of getChildren(this) ?? []) {
|
||||
if (type === 'popup') this.#obj?.unbindPopup();
|
||||
else if (type === 'tooltip') this.#obj?.unbindTooltip();
|
||||
else if (type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
|
||||
else this.#obj?.removeLayer(el as unknown as Layer);
|
||||
}
|
||||
unregisterChildren(this);
|
||||
this.#obj?.remove();
|
||||
|
||||
@ -2,6 +2,7 @@ import { GeoJSON, PathOptions, Layer } from 'leaflet';
|
||||
import { WithProps, defineProps, num, str, on } from '../core/props.ts';
|
||||
import { registerChildren, unregisterChildren, getChildren } from '../core/register.ts';
|
||||
import { buildOptions } from '../core/utils.ts';
|
||||
import type { GeoJsonObject } from 'geojson';
|
||||
|
||||
const PROPS = defineProps({
|
||||
data: str(),
|
||||
@ -28,7 +29,7 @@ export class LeafletGeoJSON extends WithProps(HTMLElement, PROPS) {
|
||||
|
||||
connectedCallback() {
|
||||
const raw = this.getAttribute('data');
|
||||
const data = raw ? JSON.parse(raw) : undefined;
|
||||
const data = raw ? (JSON.parse(raw) as GeoJsonObject) : null;
|
||||
const styleOpts = Object.fromEntries(
|
||||
Object.entries(buildOptions(this, PROPS, ['data'])).filter(([, v]) => v !== ''),
|
||||
);
|
||||
@ -40,7 +41,7 @@ export class LeafletGeoJSON extends WithProps(HTMLElement, PROPS) {
|
||||
for (const [el, type] of getChildren(this) ?? []) {
|
||||
if (type === 'popup') this.#obj?.unbindPopup();
|
||||
else if (type === 'tooltip') this.#obj?.unbindTooltip();
|
||||
else if (type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
|
||||
else this.#obj?.removeLayer(el as unknown as Layer);
|
||||
}
|
||||
unregisterChildren(this);
|
||||
this.#obj?.remove();
|
||||
@ -55,6 +56,7 @@ export class LeafletGeoJSON extends WithProps(HTMLElement, PROPS) {
|
||||
if (!this.#obj) return;
|
||||
if (name === 'data') {
|
||||
this.#obj.clearLayers();
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
if (val) this.#obj.addData(JSON.parse(val));
|
||||
} else {
|
||||
const propName = PROP_BY_ATTR.get(name);
|
||||
|
||||
@ -22,7 +22,7 @@ export class LeafletImageOverlay extends WithProps(HTMLElement, PROPS) {
|
||||
#obj?: ImageOverlay;
|
||||
|
||||
connectedCallback() {
|
||||
const url = this.getAttribute('url') || '';
|
||||
const url = this.getAttribute('url') ?? '';
|
||||
this.#obj = new ImageOverlay(
|
||||
url,
|
||||
parseBoundsAttr(this),
|
||||
@ -49,9 +49,9 @@ export class LeafletImageOverlay extends WithProps(HTMLElement, PROPS) {
|
||||
this.#obj.setBounds(new LatLngBounds(parseBoundsAttr(this) as LatLngExpression[]));
|
||||
} else if (name === 'alt') {
|
||||
const el = this.#obj.getElement();
|
||||
if (el) (el as HTMLImageElement).alt = val ?? '';
|
||||
if (el) el.alt = val ?? '';
|
||||
} else {
|
||||
setLayerAttr(this.#obj!, PROPS, PROP_BY_ATTR, name, val);
|
||||
setLayerAttr(this.#obj, PROPS, PROP_BY_ATTR, name, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ export class LeafletLayerGroup extends HTMLElement {
|
||||
for (const [el, type] of getChildren(this) ?? []) {
|
||||
if (type === 'popup') this.#obj?.unbindPopup();
|
||||
else if (type === 'tooltip') this.#obj?.unbindTooltip();
|
||||
else if (type === 'layer') this.#obj?.removeLayer(el as unknown as Layer);
|
||||
else this.#obj?.removeLayer(el as unknown as Layer);
|
||||
}
|
||||
unregisterChildren(this);
|
||||
this.#obj?.remove();
|
||||
|
||||
@ -12,14 +12,14 @@ const PROPS = defineProps({
|
||||
lat: num(0, {
|
||||
viewState: true,
|
||||
event: 'moveend',
|
||||
mapGet: (m: LMap) => m.getCenter()?.lat,
|
||||
mapSet: (m: LMap, v: number) => m.setView([v, m.getCenter()?.lng ?? 0], m.getZoom()),
|
||||
mapGet: (m: LMap) => m.getCenter().lat,
|
||||
mapSet: (m: LMap, v: number) => m.setView([v, m.getCenter().lng], m.getZoom()),
|
||||
}),
|
||||
lng: num(0, {
|
||||
viewState: true,
|
||||
event: 'moveend',
|
||||
mapGet: (m: LMap) => m.getCenter()?.lng,
|
||||
mapSet: (m: LMap, v: number) => m.setView([m.getCenter()?.lat ?? 0, v], m.getZoom()),
|
||||
mapGet: (m: LMap) => m.getCenter().lng,
|
||||
mapSet: (m: LMap, v: number) => m.setView([m.getCenter().lat, v], m.getZoom()),
|
||||
}),
|
||||
zoom: num(2, {
|
||||
viewState: true,
|
||||
@ -81,7 +81,7 @@ const PROPS = defineProps({
|
||||
|
||||
type PropName = keyof typeof PROPS;
|
||||
|
||||
const ATTR_TO_PROP = new globalThis.Map<string, PropName>(
|
||||
const ATTR_TO_PROP = new Map<string, PropName>(
|
||||
(Object.entries(PROPS) as [PropName, PropDef][]).map(([name, spec]) => [spec.attr, name]),
|
||||
);
|
||||
|
||||
@ -98,7 +98,7 @@ export class LeafletMap extends TypedBase {
|
||||
#container!: HTMLDivElement;
|
||||
#cssLink?: HTMLLinkElement;
|
||||
#syncing = false;
|
||||
#mapEventHandlers = new globalThis.Map<string, () => void>();
|
||||
#mapEventHandlers = new Map<string, () => void>();
|
||||
#resizeObserver?: ResizeObserver;
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
@ -113,7 +113,7 @@ export class LeafletMap extends TypedBase {
|
||||
Object.defineProperty(LeafletMap.prototype, propName, {
|
||||
get(this: LeafletMap) {
|
||||
const val = spec.mapGet && this.#map ? spec.mapGet(this.#map) : undefined;
|
||||
return val !== undefined ? val : +(this.getAttribute(spec.attr) ?? spec.default);
|
||||
return val ?? +(this.getAttribute(spec.attr) ?? spec.default);
|
||||
},
|
||||
set(this: LeafletMap, v: number) {
|
||||
if (spec.event) {
|
||||
@ -157,9 +157,11 @@ export class LeafletMap extends TypedBase {
|
||||
this.#container = document.createElement('div');
|
||||
this.#container.style.width = '100%';
|
||||
this.#container.style.height = '100%';
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.shadowRoot!.appendChild(this.#container);
|
||||
const style = document.createElement('style');
|
||||
style.textContent = ':host { display: block; width: 100%; height: 400px; }';
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
this.shadowRoot!.appendChild(style);
|
||||
}
|
||||
this.#applyCss();
|
||||
@ -180,7 +182,7 @@ export class LeafletMap extends TypedBase {
|
||||
this.#resizeObserver.observe(this);
|
||||
|
||||
// Register one handler per unique map event, updating all props that share it
|
||||
const groups = new globalThis.Map<string, NumProp[]>();
|
||||
const groups = new Map<string, NumProp[]>();
|
||||
for (const spec of Object.values(PROPS) as PropDef[]) {
|
||||
if (spec.kind === 'num' && spec.event && spec.mapGet) {
|
||||
const g = groups.get(spec.event) ?? [];
|
||||
@ -254,11 +256,11 @@ export class LeafletMap extends TypedBase {
|
||||
}
|
||||
|
||||
const customUrl = this.hasAttribute('css-url');
|
||||
const url = customUrl ? this.getAttribute('css-url')! : DEFAULT_CSS_URL;
|
||||
const url = customUrl ? this.getAttribute('css-url') : DEFAULT_CSS_URL;
|
||||
|
||||
let integrity: string | undefined;
|
||||
let integrity: string | null = null;
|
||||
if (this.hasAttribute('css-integrity')) {
|
||||
integrity = this.getAttribute('css-integrity')!;
|
||||
integrity = this.getAttribute('css-integrity') ?? '';
|
||||
} else if (!customUrl) {
|
||||
integrity = DEFAULT_CSS_INTEGRITY;
|
||||
}
|
||||
@ -273,14 +275,14 @@ export class LeafletMap extends TypedBase {
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = url;
|
||||
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/');
|
||||
Icon.Default.imagePath = url?.replace(/\/[^/]+$/, '/images/');
|
||||
}
|
||||
|
||||
#syncAttr(name: string, value: string) {
|
||||
@ -303,12 +305,12 @@ export class LeafletMap extends TypedBase {
|
||||
if (this.hasAttribute(spec.attr)) o[propName] = true;
|
||||
}
|
||||
}
|
||||
return o as MapOptions;
|
||||
return o;
|
||||
}
|
||||
|
||||
#handleLeafletRegister = (e: LeafletRegisterEvent) => {
|
||||
e.stopPropagation();
|
||||
if (this.#map && e.detail.leafletObject) {
|
||||
if (this.#map) {
|
||||
e.detail.leafletObject.addTo(this.#map);
|
||||
}
|
||||
};
|
||||
|
||||
@ -24,12 +24,14 @@ export class LeafletMarker extends WithProps(HTMLElement, PROPS) {
|
||||
[numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')],
|
||||
buildOptions(this, PROPS, ['lat', 'lng']),
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj.on('dragend move', this.#onChange, this);
|
||||
registerChildren(this, this.#obj);
|
||||
this.addEventListener('icon-changed', this.#onIconChanged);
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj?.off('dragend move', this.#onChange, this);
|
||||
this.removeEventListener('icon-changed', this.#onIconChanged);
|
||||
unregisterChildren(this);
|
||||
@ -55,7 +57,7 @@ export class LeafletMarker extends WithProps(HTMLElement, PROPS) {
|
||||
const el = this.#obj.getElement();
|
||||
if (el) (el as HTMLImageElement).alt = val ?? '';
|
||||
} else {
|
||||
setLayerAttr(this.#obj!, PROPS, PROP_BY_ATTR, name, val);
|
||||
setLayerAttr(this.#obj, PROPS, PROP_BY_ATTR, name, val);
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,11 +70,11 @@ export class LeafletMarker extends WithProps(HTMLElement, PROPS) {
|
||||
this.#syncing = false;
|
||||
}
|
||||
|
||||
#onIconChanged(e: LeafletIconChangedEvent) {
|
||||
#onIconChanged = (e: LeafletIconChangedEvent) => {
|
||||
if (!this.#obj) return;
|
||||
if (e.detail.icon) this.#obj.setIcon(e.detail.icon);
|
||||
else this.#obj.setIcon(new Icon.Default());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
customElements.define('leaflet-marker', LeafletMarker);
|
||||
|
||||
@ -22,7 +22,9 @@ export class LeafletPolygon extends WithProps(HTMLElement, PROPS) {
|
||||
registerChildren(this, this.#obj);
|
||||
|
||||
this.addEventListener('line-updated', this.#syncCoords);
|
||||
this.#observer = new MutationObserver(() => this.#syncCoords());
|
||||
this.#observer = new MutationObserver(() => {
|
||||
this.#syncCoords();
|
||||
});
|
||||
this.#observer.observe(this, { childList: true });
|
||||
}
|
||||
|
||||
@ -51,7 +53,7 @@ export class LeafletPolygon extends WithProps(HTMLElement, PROPS) {
|
||||
};
|
||||
|
||||
#getCoords(): [number, number][] {
|
||||
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
||||
const lines: LeafletLine[] = Array.from(this.querySelectorAll('leaflet-line'));
|
||||
return lines.map((line) => line.latlng);
|
||||
}
|
||||
}
|
||||
|
||||
@ -22,7 +22,9 @@ export class LeafletPolyline extends WithProps(HTMLElement, PROPS) {
|
||||
registerChildren(this, this.#obj);
|
||||
|
||||
this.addEventListener('line-updated', this.#syncCoords);
|
||||
this.#observer = new MutationObserver(() => this.#syncCoords());
|
||||
this.#observer = new MutationObserver(() => {
|
||||
this.#syncCoords();
|
||||
});
|
||||
this.#observer.observe(this, { childList: true });
|
||||
}
|
||||
|
||||
@ -51,7 +53,7 @@ export class LeafletPolyline extends WithProps(HTMLElement, PROPS) {
|
||||
};
|
||||
|
||||
#getCoords(): [number, number][] {
|
||||
const lines = Array.from(this.querySelectorAll('leaflet-line')) as LeafletLine[];
|
||||
const lines = Array.from(this.querySelectorAll<LeafletLine>('leaflet-line'));
|
||||
return lines.map((line) => line.latlng);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@ export class LeafletPopup extends WithProps(HTMLElement, PROPS) {
|
||||
if (this.hasAttribute('lat') && this.hasAttribute('lng')) {
|
||||
this.#obj.setLatLng([numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')]);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj.on('move', this.#onChange, this);
|
||||
registerWithParent(this, this.#obj);
|
||||
|
||||
@ -40,6 +41,7 @@ export class LeafletPopup extends WithProps(HTMLElement, PROPS) {
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj?.off('move', this.#onChange, this);
|
||||
this.#observer?.disconnect();
|
||||
this.#observer = undefined;
|
||||
|
||||
@ -22,6 +22,7 @@ export class LeafletSVGOverlay extends WithProps(HTMLElement, PROPS) {
|
||||
const svg = this.querySelector('svg');
|
||||
const dummy = !svg ? document.createElementNS('http://www.w3.org/2000/svg', 'svg') : undefined;
|
||||
this.#obj = new SVGOverlay(
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
svg ?? dummy!,
|
||||
parseBoundsAttr(this),
|
||||
buildOptions(this, PROPS, ['bounds']) as ImageOverlayOptions,
|
||||
@ -44,7 +45,7 @@ export class LeafletSVGOverlay extends WithProps(HTMLElement, PROPS) {
|
||||
if (name === 'bounds') {
|
||||
this.#obj.setBounds(new LatLngBounds(parseBoundsAttr(this) as LatLngExpression[]));
|
||||
} else {
|
||||
setLayerAttr(this.#obj!, PROPS, PROP_BY_ATTR, name, val);
|
||||
setLayerAttr(this.#obj, PROPS, PROP_BY_ATTR, name, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ export class LeafletTileLayerWMS extends WithProps(HTMLElement, PROPS) {
|
||||
#obj?: TileLayer.WMS;
|
||||
|
||||
connectedCallback() {
|
||||
const url = this.getAttribute('url') || '';
|
||||
const url = this.getAttribute('url') ?? '';
|
||||
this.#obj = new TileLayer.WMS(url, buildOptions(this, PROPS, ['url']));
|
||||
registerChildren(this, this.#obj);
|
||||
}
|
||||
@ -41,8 +41,8 @@ export class LeafletTileLayerWMS extends WithProps(HTMLElement, PROPS) {
|
||||
} else {
|
||||
const propName = PROP_BY_ATTR.get(name);
|
||||
if (!propName) return;
|
||||
const spec = PROPS[propName as keyof typeof PROPS];
|
||||
if (!setLayerAttr(this.#obj!, PROPS, PROP_BY_ATTR, name, val)) {
|
||||
const spec = PROPS[propName];
|
||||
if (!setLayerAttr(this.#obj, PROPS, PROP_BY_ATTR, name, val)) {
|
||||
const value = spec.kind === 'bool-on' ? val !== null : parseAttributeValue(val);
|
||||
(this.#obj.setParams as unknown as (params: Record<string, unknown>) => void)({
|
||||
[propName]: value,
|
||||
|
||||
@ -18,7 +18,7 @@ export class LeafletTileLayer extends WithProps(HTMLElement, PROPS) {
|
||||
#obj?: TileLayer;
|
||||
|
||||
connectedCallback() {
|
||||
const url = this.getAttribute('url') || '';
|
||||
const url = this.getAttribute('url') ?? '';
|
||||
this.#obj = new TileLayer(url, buildOptions(this, PROPS, ['url']));
|
||||
registerChildren(this, this.#obj);
|
||||
}
|
||||
@ -38,7 +38,7 @@ export class LeafletTileLayer extends WithProps(HTMLElement, PROPS) {
|
||||
if (name === 'url') {
|
||||
if (val) this.#obj.setUrl(val);
|
||||
} else {
|
||||
setLayerAttr(this.#obj!, PROPS, PROP_BY_ATTR, name, val);
|
||||
setLayerAttr(this.#obj, PROPS, PROP_BY_ATTR, name, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ export class LeafletTooltip extends WithProps(HTMLElement, PROPS) {
|
||||
if (this.hasAttribute('lat') && this.hasAttribute('lng')) {
|
||||
this.#obj.setLatLng([numAttr(this, PROPS, 'lat'), numAttr(this, PROPS, 'lng')]);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj.on('move', this.#onChange, this);
|
||||
registerWithParent(this, this.#obj);
|
||||
|
||||
@ -41,6 +42,7 @@ export class LeafletTooltip extends WithProps(HTMLElement, PROPS) {
|
||||
}
|
||||
|
||||
disconnectedCallback() {
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
this.#obj?.off('move', this.#onChange, this);
|
||||
this.#observer?.disconnect();
|
||||
this.#observer = undefined;
|
||||
|
||||
@ -23,7 +23,7 @@ export class LeafletVideoOverlay extends WithProps(HTMLElement, PROPS) {
|
||||
#obj?: VideoOverlay;
|
||||
|
||||
connectedCallback() {
|
||||
const url = this.getAttribute('url') || '';
|
||||
const url = this.getAttribute('url') ?? '';
|
||||
this.#obj = new VideoOverlay(
|
||||
url,
|
||||
parseBoundsAttr(this),
|
||||
@ -59,10 +59,10 @@ export class LeafletVideoOverlay extends WithProps(HTMLElement, PROPS) {
|
||||
if (name === 'loop') el.loop = val !== null;
|
||||
else if (name === 'autoplay') el.autoplay = val !== null;
|
||||
else if (name === 'muted') el.muted = val !== null;
|
||||
else if (name === 'playsinline') el.playsInline = val !== null;
|
||||
else el.playsInline = val !== null;
|
||||
}
|
||||
} else {
|
||||
setLayerAttr(this.#obj!, PROPS, PROP_BY_ATTR, name, val);
|
||||
setLayerAttr(this.#obj, PROPS, PROP_BY_ATTR, name, val);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,31 +1,31 @@
|
||||
export type NumProp<T = unknown> = {
|
||||
export interface NumProp<T = unknown> {
|
||||
kind: 'num';
|
||||
attr: string;
|
||||
default: number;
|
||||
mapGet?: (m: T) => number | undefined;
|
||||
mapSet?: (m: T, v: number) => void;
|
||||
mapGet?(m: T): number | undefined;
|
||||
mapSet?(m: T, v: number): void;
|
||||
viewState?: boolean;
|
||||
event?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type StrProp = {
|
||||
export interface StrProp {
|
||||
kind: 'str';
|
||||
attr: string;
|
||||
default: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type BoolOnProp = {
|
||||
export interface BoolOnProp {
|
||||
kind: 'bool-on';
|
||||
attr: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type BoolOffProp<T = unknown> = {
|
||||
export interface BoolOffProp<T = unknown> {
|
||||
kind: 'bool-off';
|
||||
attr: string;
|
||||
mapSet?: (m: T, enabled: boolean) => void;
|
||||
};
|
||||
mapSet?(m: T, enabled: boolean): void;
|
||||
}
|
||||
|
||||
export type PropDef = NumProp<unknown> | StrProp | BoolOnProp | BoolOffProp<unknown>;
|
||||
export type PropDef = NumProp | StrProp | BoolOnProp | BoolOffProp;
|
||||
|
||||
export type PropTypeOf<T extends PropDef> = T extends NumProp
|
||||
? number
|
||||
@ -44,34 +44,46 @@ type StrPropInput = OptionalAttr<StrProp>;
|
||||
type BoolOnPropInput = OptionalAttr<BoolOnProp>;
|
||||
type BoolOffPropInput<T = unknown> = OptionalAttr<BoolOffProp<T>>;
|
||||
|
||||
type PropDefInput =
|
||||
| NumPropInput<unknown>
|
||||
| StrPropInput
|
||||
| BoolOnPropInput
|
||||
| BoolOffPropInput<unknown>;
|
||||
type PropDefInput = NumPropInput | StrPropInput | BoolOnPropInput | BoolOffPropInput;
|
||||
|
||||
type PropDefFromInput<T> =
|
||||
T extends NumPropInput<infer U>
|
||||
? NumProp<U>
|
||||
: T extends { kind: 'str' }
|
||||
? StrProp
|
||||
: T extends BoolOffPropInput<infer U>
|
||||
? BoolOffProp<U>
|
||||
: BoolOnProp;
|
||||
type ValueFor<T, K extends keyof T> = T[K];
|
||||
|
||||
type PropDefFromInput<T extends PropDefInput, Key extends string> = (T extends NumPropInput<infer U>
|
||||
? NumProp<U>
|
||||
: T extends { kind: 'str' }
|
||||
? StrProp
|
||||
: T extends BoolOffPropInput<infer U>
|
||||
? BoolOffProp<U>
|
||||
: BoolOnProp) & { attr: AttributeValue<T, Key> };
|
||||
|
||||
type AttributeValue<T extends PropDefInput, Key extends string> = T extends { attr: string }
|
||||
? ValueFor<T, 'attr'>
|
||||
: T extends BoolOffPropInput
|
||||
? AsDisableKebab<Key>
|
||||
: AsKebab<Key>;
|
||||
|
||||
type AsKebab<T extends string> = T extends `${infer L}${infer R}`
|
||||
? `${L extends Uppercase<L> ? '-' : ''}${Lowercase<L>}${AsKebab<R>}`
|
||||
: T;
|
||||
|
||||
type AsDisableKebab<T extends string> = `disable-${AsKebab<T>}`;
|
||||
|
||||
function disableCamelToKebab<S extends string>(s: S): AsDisableKebab<S> {
|
||||
return ('disable-' + s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())) as AsDisableKebab<S>;
|
||||
}
|
||||
|
||||
function camelToKebab(s: string): string {
|
||||
return s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase());
|
||||
}
|
||||
|
||||
export function num<T = unknown>(
|
||||
def: number = 0,
|
||||
opts?: string | (Omit<NumProp<T>, 'kind' | 'default' | 'attr'> & { attr?: string }),
|
||||
def = 0,
|
||||
opts?: Omit<NumProp<T>, 'kind' | 'default' | 'attr'> & { attr?: string },
|
||||
): NumPropInput<T> {
|
||||
if (typeof opts === 'string') opts = { attr: opts };
|
||||
return { kind: 'num', default: def, ...opts } as NumPropInput<T>;
|
||||
return { kind: 'num', default: def, ...opts };
|
||||
}
|
||||
|
||||
export function str(def: string = '', attr?: string): StrPropInput {
|
||||
export function str(def = '', attr?: string): StrPropInput {
|
||||
return { kind: 'str', default: def, ...(attr ? { attr } : {}) };
|
||||
}
|
||||
|
||||
@ -80,21 +92,21 @@ export function on(attr?: string): BoolOnPropInput {
|
||||
}
|
||||
|
||||
export function off<T = unknown>(mapSet?: (m: T, enabled: boolean) => void): BoolOffPropInput<T> {
|
||||
return { kind: 'bool-off', ...(mapSet ? { mapSet } : {}) } as BoolOffPropInput<T>;
|
||||
return { kind: 'bool-off', ...(mapSet ? { mapSet } : {}) };
|
||||
}
|
||||
|
||||
export function defineProps<T extends Record<string, unknown>>(
|
||||
export function defineProps<T extends Record<string, PropDefInput>>(
|
||||
input: T,
|
||||
): { [K in keyof T]: PropDefFromInput<T[K]> } {
|
||||
): { [K in keyof T]: PropDefFromInput<T[K], Extract<K, string>> } {
|
||||
const output = {} as Record<string, PropDef>;
|
||||
for (const key of Object.keys(input)) {
|
||||
const val = input[key] as PropDefInput;
|
||||
const val = input[key];
|
||||
const { attr: override, ...rest } = val as unknown as Record<string, unknown>;
|
||||
const attr =
|
||||
override ?? (rest.kind === 'bool-off' ? 'disable-' + camelToKebab(key) : camelToKebab(key));
|
||||
output[key] = { ...rest, attr } as PropDef;
|
||||
override ?? (rest.kind === 'bool-off' ? disableCamelToKebab(key) : camelToKebab(key));
|
||||
output[key] = { ...(rest as object), attr } as PropDef;
|
||||
}
|
||||
return output as unknown as { [K in keyof T]: PropDefFromInput<T[K]> };
|
||||
return output as unknown as { [K in keyof T]: PropDefFromInput<T[K], Extract<K, string>> };
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -126,6 +138,7 @@ function definePropAccessors(proto: object, props: Record<string, PropDef>) {
|
||||
set(v: unknown) {
|
||||
const el = this as HTMLElement;
|
||||
if (spec.kind === 'bool-on') el.toggleAttribute(spec.attr, !!v);
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
else el.setAttribute(spec.attr, `${v}`);
|
||||
},
|
||||
configurable: true,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Icon, Layer, LayerGroup, Popup, Tooltip } from 'leaflet';
|
||||
import { DivIcon, Icon, Layer, LayerGroup, Popup, Tooltip } from 'leaflet';
|
||||
import { registerWithParent } from './utils.ts';
|
||||
|
||||
// Custom event type for the bubbling registration protocol. Carries the
|
||||
@ -22,7 +22,7 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
export function emitIconChanged(el: HTMLElement, icon: Icon | null | undefined) {
|
||||
export function emitIconChanged(el: HTMLElement, icon: Icon | DivIcon | null | undefined) {
|
||||
el.dispatchEvent(
|
||||
new CustomEvent('icon-changed', {
|
||||
bubbles: true,
|
||||
|
||||
@ -24,7 +24,7 @@ export function parseBoundsAttr(el: HTMLElement): LatLngBoundsExpression {
|
||||
// For example, { fillColor: { attr: 'fill-color' } } becomes
|
||||
// { 'fill-color' → 'fillColor' }. Used by setLayerAttr to find the
|
||||
// Leaflet setter name when an attribute changes at runtime.
|
||||
export function buildAttrMap(props: Record<string, PropDef>): Map<string, string> {
|
||||
export function buildAttrMap<T extends Record<string, PropDef>>(props: T): Map<string, keyof T> {
|
||||
return new Map(Object.entries(props).map(([name, spec]) => [spec.attr, name]));
|
||||
}
|
||||
|
||||
@ -44,7 +44,7 @@ export function setLayerAttr(
|
||||
): boolean {
|
||||
const propName = attrMap.get(name);
|
||||
if (!propName) return false;
|
||||
const spec = props[propName as keyof typeof props];
|
||||
const spec = props[propName];
|
||||
const setter = `set${propName.charAt(0).toUpperCase()}${propName.slice(1)}`;
|
||||
const fn = (obj as Record<string, unknown>)[setter];
|
||||
if (typeof fn === 'function') {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user