diff --git a/eslint.config.js b/eslint.config.js index db00b85..eb37214 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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 }], + }, + }, +]); diff --git a/package-lock.json b/package-lock.json index a58649f..04e2312 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index cec4b00..cf5d993 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/leaflet-circle-marker.ts b/src/components/leaflet-circle-marker.ts index a0b1ce8..3460b7f 100644 --- a/src/components/leaflet-circle-marker.ts +++ b/src/components/leaflet-circle-marker.ts @@ -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(); diff --git a/src/components/leaflet-circle.ts b/src/components/leaflet-circle.ts index 07b2355..2025e5a 100644 --- a/src/components/leaflet-circle.ts +++ b/src/components/leaflet-circle.ts @@ -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(); diff --git a/src/components/leaflet-control-layers.ts b/src/components/leaflet-control-layers.ts index d757e97..7b48369 100644 --- a/src/components/leaflet-control-layers.ts +++ b/src/components/leaflet-control-layers.ts @@ -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 } }), diff --git a/src/components/leaflet-div-icon.ts b/src/components/leaflet-div-icon.ts index 0570579..09548dd 100644 --- a/src/components/leaflet-div-icon.ts +++ b/src/components/leaflet-div-icon.ts @@ -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([ + '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 = {}; + const opts: Partial = {}; 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); } } diff --git a/src/components/leaflet-feature-group.ts b/src/components/leaflet-feature-group.ts index 7d01480..1d7de08 100644 --- a/src/components/leaflet-feature-group.ts +++ b/src/components/leaflet-feature-group.ts @@ -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(); diff --git a/src/components/leaflet-geojson.ts b/src/components/leaflet-geojson.ts index ea425fe..78e8898 100644 --- a/src/components/leaflet-geojson.ts +++ b/src/components/leaflet-geojson.ts @@ -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); diff --git a/src/components/leaflet-image-overlay.ts b/src/components/leaflet-image-overlay.ts index 8972d7b..1762d5c 100644 --- a/src/components/leaflet-image-overlay.ts +++ b/src/components/leaflet-image-overlay.ts @@ -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); } } } diff --git a/src/components/leaflet-layer-group.ts b/src/components/leaflet-layer-group.ts index 3d00542..e95592c 100644 --- a/src/components/leaflet-layer-group.ts +++ b/src/components/leaflet-layer-group.ts @@ -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(); diff --git a/src/components/leaflet-map.ts b/src/components/leaflet-map.ts index cc4457c..ec16ba1 100644 --- a/src/components/leaflet-map.ts +++ b/src/components/leaflet-map.ts @@ -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( +const ATTR_TO_PROP = new Map( (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 void>(); + #mapEventHandlers = new Map 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(); + const groups = new Map(); 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); } }; diff --git a/src/components/leaflet-marker.ts b/src/components/leaflet-marker.ts index 0aef326..f462299 100644 --- a/src/components/leaflet-marker.ts +++ b/src/components/leaflet-marker.ts @@ -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); diff --git a/src/components/leaflet-polygon.ts b/src/components/leaflet-polygon.ts index ff00f78..a4e9e2a 100644 --- a/src/components/leaflet-polygon.ts +++ b/src/components/leaflet-polygon.ts @@ -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); } } diff --git a/src/components/leaflet-polyline.ts b/src/components/leaflet-polyline.ts index 37c1771..433bee7 100644 --- a/src/components/leaflet-polyline.ts +++ b/src/components/leaflet-polyline.ts @@ -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('leaflet-line')); return lines.map((line) => line.latlng); } } diff --git a/src/components/leaflet-popup.ts b/src/components/leaflet-popup.ts index c91b488..6688d63 100644 --- a/src/components/leaflet-popup.ts +++ b/src/components/leaflet-popup.ts @@ -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; diff --git a/src/components/leaflet-svg-overlay.ts b/src/components/leaflet-svg-overlay.ts index f3852b6..f398b30 100644 --- a/src/components/leaflet-svg-overlay.ts +++ b/src/components/leaflet-svg-overlay.ts @@ -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); } } } diff --git a/src/components/leaflet-tile-layer-wms.ts b/src/components/leaflet-tile-layer-wms.ts index dff4855..95a7db6 100644 --- a/src/components/leaflet-tile-layer-wms.ts +++ b/src/components/leaflet-tile-layer-wms.ts @@ -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) => void)({ [propName]: value, diff --git a/src/components/leaflet-tile-layer.ts b/src/components/leaflet-tile-layer.ts index 23ec492..c56207c 100644 --- a/src/components/leaflet-tile-layer.ts +++ b/src/components/leaflet-tile-layer.ts @@ -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); } } } diff --git a/src/components/leaflet-tooltip.ts b/src/components/leaflet-tooltip.ts index 73ec3fe..9962ea4 100644 --- a/src/components/leaflet-tooltip.ts +++ b/src/components/leaflet-tooltip.ts @@ -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; diff --git a/src/components/leaflet-video-overlay.ts b/src/components/leaflet-video-overlay.ts index 0fa5ecb..8422de8 100644 --- a/src/components/leaflet-video-overlay.ts +++ b/src/components/leaflet-video-overlay.ts @@ -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); } } diff --git a/src/core/props.ts b/src/core/props.ts index c857d27..982282e 100644 --- a/src/core/props.ts +++ b/src/core/props.ts @@ -1,31 +1,31 @@ -export type NumProp = { +export interface NumProp { 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 = { +export interface BoolOffProp { kind: 'bool-off'; attr: string; - mapSet?: (m: T, enabled: boolean) => void; -}; + mapSet?(m: T, enabled: boolean): void; +} -export type PropDef = NumProp | StrProp | BoolOnProp | BoolOffProp; +export type PropDef = NumProp | StrProp | BoolOnProp | BoolOffProp; export type PropTypeOf = T extends NumProp ? number @@ -44,34 +44,46 @@ type StrPropInput = OptionalAttr; type BoolOnPropInput = OptionalAttr; type BoolOffPropInput = OptionalAttr>; -type PropDefInput = - | NumPropInput - | StrPropInput - | BoolOnPropInput - | BoolOffPropInput; - -type PropDefFromInput = - T extends NumPropInput - ? NumProp - : T extends { kind: 'str' } - ? StrProp - : T extends BoolOffPropInput - ? BoolOffProp - : BoolOnProp; +type PropDefInput = NumPropInput | StrPropInput | BoolOnPropInput | BoolOffPropInput; + +type ValueFor = T[K]; + +type PropDefFromInput = (T extends NumPropInput + ? NumProp + : T extends { kind: 'str' } + ? StrProp + : T extends BoolOffPropInput + ? BoolOffProp + : BoolOnProp) & { attr: AttributeValue }; + +type AttributeValue = T extends { attr: string } + ? ValueFor + : T extends BoolOffPropInput + ? AsDisableKebab + : AsKebab; + +type AsKebab = T extends `${infer L}${infer R}` + ? `${L extends Uppercase ? '-' : ''}${Lowercase}${AsKebab}` + : T; + +type AsDisableKebab = `disable-${AsKebab}`; + +function disableCamelToKebab(s: S): AsDisableKebab { + return ('disable-' + s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase())) as AsDisableKebab; +} function camelToKebab(s: string): string { return s.replace(/[A-Z]/g, (c) => '-' + c.toLowerCase()); } export function num( - def: number = 0, - opts?: string | (Omit, 'kind' | 'default' | 'attr'> & { attr?: string }), + def = 0, + opts?: Omit, 'kind' | 'default' | 'attr'> & { attr?: string }, ): NumPropInput { - if (typeof opts === 'string') opts = { attr: opts }; - return { kind: 'num', default: def, ...opts } as NumPropInput; + 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(mapSet?: (m: T, enabled: boolean) => void): BoolOffPropInput { - return { kind: 'bool-off', ...(mapSet ? { mapSet } : {}) } as BoolOffPropInput; + return { kind: 'bool-off', ...(mapSet ? { mapSet } : {}) }; } -export function defineProps>( +export function defineProps>( input: T, -): { [K in keyof T]: PropDefFromInput } { +): { [K in keyof T]: PropDefFromInput> } { const output = {} as Record; 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; 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 }; + return output as unknown as { [K in keyof T]: PropDefFromInput> }; } // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -126,6 +138,7 @@ function definePropAccessors(proto: object, props: Record) { 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, diff --git a/src/core/register.ts b/src/core/register.ts index a4a22b1..846e6a2 100644 --- a/src/core/register.ts +++ b/src/core/register.ts @@ -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, diff --git a/src/core/utils.ts b/src/core/utils.ts index 9552908..04be03a 100644 --- a/src/core/utils.ts +++ b/src/core/utils.ts @@ -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): Map { +export function buildAttrMap>(props: T): Map { 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)[setter]; if (typeof fn === 'function') {