feat: type leaflet-* components and their leaflet: events
Two augmentations, so TypeScript actually knows about the custom elements:
- HTMLElementTagNameMap (src/index.ts): createElement/querySelector now
infer the exact component class for all 24 tags instead of HTMLElement.
- Per-component addEventListener/removeEventListener overrides, typing
`leaflet:<name>` events against the real Leaflet event payload
(PopupEvent, DragEndEvent, LeafletMouseEvent, etc.) while still accepting
ordinary DOM events normally, and rejecting event names that component
doesn't fire.
src/core/event-types.ts holds reusable event-name -> payload-type
fragments (mirroring shared-props.ts's fragment reuse), composed per
family: MapEvents, MarkerEvents, PathEvents, TileLayerEvents,
DivOverlayLayerEvents, GroupEvents. LeafletAddEventListener<T>/
LeafletRemoveEventListener<T> in with-props.ts are type-only
intersection-of-overloads helpers applied via `declare addEventListener:
...`, the same pattern every component already uses for `declare readonly
leafletObject?: X` -- zero runtime cost. Deliberately no generic `string`
fallback overload: a fallback would silently accept unrecognized
`leaflet:*` names too, defeating the point.
Fixed a real bug found while building this: #forwardEvents was
dispatching the raw pre-merge `data` Leaflet passes to fire(), missing
type/target/sourceTarget that Leaflet's own fire() merges in before
notifying real .on() listeners. Typing `detail` against Leaflet's actual
event interfaces would have been dishonest otherwise, so the merge now
matches Leaflet's own Evented#fire.
Also added 'line-updated' to the existing internal-event HTMLElementEventMap
augmentation in register.ts (needed once addEventListener got overridden
on polygon/polyline, which use it internally) and cleaned up ~35 now-
redundant `as HTMLElement & {...}` casts across the test suite that the
tag name map makes unnecessary.
This commit is contained in:
parent
9b6c7ca598
commit
0419ab781d
@ -1,7 +1,12 @@
|
||||
import { CircleMarker, type CircleMarkerOptions } from 'leaflet';
|
||||
import { num } from '../core/props.ts';
|
||||
import { latLngProps, pathProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletCircleMarker extends WithProps({
|
||||
...latLngProps,
|
||||
@ -9,6 +14,8 @@ export class LeafletCircleMarker extends WithProps({
|
||||
...pathProps,
|
||||
}) {
|
||||
declare readonly leafletObject?: CircleMarker;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
createLeafletObject(options: CircleMarkerOptions): CircleMarker {
|
||||
return new CircleMarker([this.lat, this.lng], options);
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Circle, type CircleOptions } from 'leaflet';
|
||||
import { num, positional } from '../core/props.ts';
|
||||
import { latLngProps, pathProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletCircle extends WithProps({
|
||||
...latLngProps,
|
||||
@ -11,6 +16,8 @@ export class LeafletCircle extends WithProps({
|
||||
radius: positional(num<Circle>(1000, { get: (obj) => obj.getRadius() })),
|
||||
}) {
|
||||
declare readonly leafletObject?: Circle;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
createLeafletObject(options: CircleOptions): Circle {
|
||||
return new Circle([this.lat, this.lng], { ...options, radius: this.radius });
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
import { FeatureGroup } from 'leaflet';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { GroupEvents } from '../core/event-types.ts';
|
||||
|
||||
// Like leaflet-layer-group, but its children share events and a bounding box.
|
||||
export class LeafletFeatureGroup extends WithProps({}) {
|
||||
declare readonly leafletObject?: FeatureGroup;
|
||||
declare addEventListener: LeafletAddEventListener<GroupEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<GroupEvents>;
|
||||
|
||||
createLeafletObject(): FeatureGroup {
|
||||
return new FeatureGroup([]);
|
||||
|
||||
@ -2,7 +2,12 @@ import { GeoJSON, type PathOptions } from 'leaflet';
|
||||
import type { GeoJsonObject } from 'geojson';
|
||||
import { json, positional } from '../core/props.ts';
|
||||
import { pathProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { GroupEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletGeoJSON extends WithProps({
|
||||
data: positional(
|
||||
@ -16,6 +21,8 @@ export class LeafletGeoJSON extends WithProps({
|
||||
...pathProps,
|
||||
}) {
|
||||
declare readonly leafletObject?: GeoJSON;
|
||||
declare addEventListener: LeafletAddEventListener<GroupEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<GroupEvents>;
|
||||
|
||||
// Every prop but `data` is a style option, and GeoJSON takes those nested
|
||||
// under `style` so they apply to each feature it builds.
|
||||
|
||||
@ -6,7 +6,12 @@ import {
|
||||
} from 'leaflet';
|
||||
import { bool, choice, json, num, positional, str } from '../core/props.ts';
|
||||
import { getBounds, urlProp } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletImageOverlay extends WithProps({
|
||||
url: urlProp,
|
||||
@ -25,6 +30,8 @@ export class LeafletImageOverlay extends WithProps({
|
||||
className: str(),
|
||||
}) {
|
||||
declare readonly leafletObject?: ImageOverlay;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
createLeafletObject(options: ImageOverlayOptions): ImageOverlay {
|
||||
return new ImageOverlay(this.url, this.bounds, options);
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import { LayerGroup } from 'leaflet';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { GroupEvents } from '../core/event-types.ts';
|
||||
|
||||
// A passthrough container: it has no options of its own, and children add
|
||||
// themselves to it through the standard registration bubble.
|
||||
export class LeafletLayerGroup extends WithProps({}) {
|
||||
declare readonly leafletObject?: LayerGroup;
|
||||
declare addEventListener: LeafletAddEventListener<GroupEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<GroupEvents>;
|
||||
|
||||
createLeafletObject(): LayerGroup {
|
||||
return new LayerGroup([]);
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Icon, Map as LMap, type MapOptions } from 'leaflet';
|
||||
import { bool, disabled, num, positional, str } from '../core/props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { LeafletLayerEvent, LeafletRegisterEvent } from '../core/register.ts';
|
||||
import type { MapEvents } from '../core/event-types.ts';
|
||||
|
||||
const DEFAULT_CSS_URL = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.css';
|
||||
const DEFAULT_CSS_INTEGRITY = 'sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=';
|
||||
@ -121,6 +126,8 @@ export class LeafletMap extends WithProps(
|
||||
{ attach: 'none' },
|
||||
) {
|
||||
declare readonly leafletObject?: LMap;
|
||||
declare addEventListener: LeafletAddEventListener<MapEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<MapEvents>;
|
||||
|
||||
#container?: HTMLDivElement;
|
||||
#cssLink?: HTMLLinkElement;
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { Icon, Marker, type MarkerOptions } from 'leaflet';
|
||||
import { bool, num, str } from '../core/props.ts';
|
||||
import { latLngProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { LeafletIconChangedEvent } from '../core/register.ts';
|
||||
import type { MarkerEvents } from '../core/event-types.ts';
|
||||
|
||||
const PROPS = {
|
||||
...latLngProps,
|
||||
@ -31,6 +36,8 @@ const PROPS = {
|
||||
|
||||
export class LeafletMarker extends WithProps(PROPS) {
|
||||
declare readonly leafletObject?: Marker;
|
||||
declare addEventListener: LeafletAddEventListener<MarkerEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<MarkerEvents>;
|
||||
|
||||
createLeafletObject(options: MarkerOptions): Marker {
|
||||
return new Marker([this.lat, this.lng], options);
|
||||
|
||||
@ -1,10 +1,17 @@
|
||||
import { Polygon, type PolylineOptions } from 'leaflet';
|
||||
import { pathProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { LeafletLine } from './leaflet-line.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletPolygon extends WithProps({ ...pathProps }) {
|
||||
declare readonly leafletObject?: Polygon;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
#observer?: MutationObserver;
|
||||
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
import { Polyline, type PolylineOptions } from 'leaflet';
|
||||
import { bool, num } from '../core/props.ts';
|
||||
import { pathProps, style } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { LeafletLine } from './leaflet-line.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletPolyline extends WithProps({
|
||||
...pathProps,
|
||||
@ -12,6 +17,8 @@ export class LeafletPolyline extends WithProps({
|
||||
noClip: bool(),
|
||||
}) {
|
||||
declare readonly leafletObject?: Polyline;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
#observer?: MutationObserver;
|
||||
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Popup, type PopupOptions } from 'leaflet';
|
||||
import { bool, num } from '../core/props.ts';
|
||||
import { latLngProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { DivOverlayLayerEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletPopup extends WithProps(
|
||||
{
|
||||
@ -16,6 +21,8 @@ export class LeafletPopup extends WithProps(
|
||||
{ attach: 'self' },
|
||||
) {
|
||||
declare readonly leafletObject?: Popup;
|
||||
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<DivOverlayLayerEvents>;
|
||||
|
||||
#observer?: MutationObserver;
|
||||
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
import { Rectangle, type LatLngBoundsExpression, type PolylineOptions } from 'leaflet';
|
||||
import { json, positional } from '../core/props.ts';
|
||||
import { pathProps, getBounds } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletRectangle extends WithProps({
|
||||
bounds: positional(json<LatLngBoundsExpression, Rectangle>([], { get: getBounds })),
|
||||
...pathProps,
|
||||
}) {
|
||||
declare readonly leafletObject?: Rectangle;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
createLeafletObject(options: PolylineOptions): Rectangle {
|
||||
return new Rectangle(this.bounds, options);
|
||||
|
||||
@ -6,7 +6,12 @@ import {
|
||||
} from 'leaflet';
|
||||
import { bool, choice, json, num, positional, str } from '../core/props.ts';
|
||||
import { getBounds } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletSVGOverlay extends WithProps({
|
||||
bounds: positional(json<LatLngBoundsExpression, SVGOverlay>([], { get: getBounds })),
|
||||
@ -17,6 +22,8 @@ export class LeafletSVGOverlay extends WithProps({
|
||||
className: str(),
|
||||
}) {
|
||||
declare readonly leafletObject?: SVGOverlay;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
createLeafletObject(options: ImageOverlayOptions): SVGOverlay {
|
||||
const svg =
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { CRS, TileLayer, type WMSOptions, type WMSParams } from 'leaflet';
|
||||
import { bool, str, type PropDef } from '../core/props.ts';
|
||||
import { tileLayerProps, urlProp } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { TileLayerEvents } from '../core/event-types.ts';
|
||||
|
||||
// WMS request parameters have no individual setters -- they're merged into the
|
||||
// query string through setParams().
|
||||
@ -43,6 +48,8 @@ export class LeafletTileLayerWMS extends WithProps({
|
||||
crs: crsProp,
|
||||
}) {
|
||||
declare readonly leafletObject?: TileLayer.WMS;
|
||||
declare addEventListener: LeafletAddEventListener<TileLayerEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<TileLayerEvents>;
|
||||
|
||||
createLeafletObject(options: WMSOptions): TileLayer.WMS {
|
||||
return new TileLayer.WMS(this.url, options);
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import { TileLayer, type TileLayerOptions } from 'leaflet';
|
||||
import { tileLayerProps, urlProp } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { TileLayerEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletTileLayer extends WithProps({
|
||||
url: urlProp,
|
||||
...tileLayerProps,
|
||||
}) {
|
||||
declare readonly leafletObject?: TileLayer;
|
||||
declare addEventListener: LeafletAddEventListener<TileLayerEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<TileLayerEvents>;
|
||||
|
||||
createLeafletObject(options: TileLayerOptions): TileLayer {
|
||||
return new TileLayer(this.url, options);
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
import { Tooltip, type Direction, type PointExpression, type TooltipOptions } from 'leaflet';
|
||||
import { bool, choice, json, num, str } from '../core/props.ts';
|
||||
import { latLngProps } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { DivOverlayLayerEvents } from '../core/event-types.ts';
|
||||
|
||||
export class LeafletTooltip extends WithProps(
|
||||
{
|
||||
@ -16,6 +21,8 @@ export class LeafletTooltip extends WithProps(
|
||||
{ attach: 'self' },
|
||||
) {
|
||||
declare readonly leafletObject?: Tooltip;
|
||||
declare addEventListener: LeafletAddEventListener<DivOverlayLayerEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<DivOverlayLayerEvents>;
|
||||
|
||||
#observer?: MutationObserver;
|
||||
|
||||
|
||||
@ -6,7 +6,12 @@ import {
|
||||
} from 'leaflet';
|
||||
import { bool, choice, json, num, positional, str } from '../core/props.ts';
|
||||
import { getBounds, urlProp } from '../core/shared-props.ts';
|
||||
import { WithProps } from '../core/with-props.ts';
|
||||
import {
|
||||
WithProps,
|
||||
type LeafletAddEventListener,
|
||||
type LeafletRemoveEventListener,
|
||||
} from '../core/with-props.ts';
|
||||
import type { PathEvents } from '../core/event-types.ts';
|
||||
|
||||
// Playback options live on the <video> element Leaflet builds, so live updates
|
||||
// go there rather than through a Leaflet setter.
|
||||
@ -36,6 +41,8 @@ export class LeafletVideoOverlay extends WithProps({
|
||||
errorOverlayUrl: str(),
|
||||
}) {
|
||||
declare readonly leafletObject?: VideoOverlay;
|
||||
declare addEventListener: LeafletAddEventListener<PathEvents>;
|
||||
declare removeEventListener: LeafletRemoveEventListener<PathEvents>;
|
||||
|
||||
createLeafletObject(options: VideoOverlayOptions): VideoOverlay {
|
||||
return new VideoOverlay(this.url, this.bounds, options);
|
||||
|
||||
125
src/core/event-types.ts
Normal file
125
src/core/event-types.ts
Normal file
@ -0,0 +1,125 @@
|
||||
// Reusable event-name -> payload-type fragments, mirroring the shared prop
|
||||
// fragments in shared-props.ts. Each key names a Leaflet event; WithProps's
|
||||
// generic fire() forwarding (with-props.ts) re-emits it as a DOM
|
||||
// `leaflet:<name>` CustomEvent whose `detail` is exactly this payload type
|
||||
// (see the merge in #forwardEvents, which matches Leaflet's own Evented#fire).
|
||||
//
|
||||
// Components compose these into one type per family and apply it via
|
||||
// `declare addEventListener: LeafletAddEventListener<TheseEvents>` (see
|
||||
// with-props.ts) -- not through WithProps() itself, since (like
|
||||
// `leafletObject`) the object type isn't reliably inferred from the PROPS
|
||||
// table alone and every component already re-declares it manually.
|
||||
// `ErrorEvent` is aliased below because it's also a global DOM type.
|
||||
import type {
|
||||
DragEndEvent,
|
||||
ErrorEvent as LeafletErrorEvent,
|
||||
LayerEvent,
|
||||
LayersControlEvent,
|
||||
LeafletEvent,
|
||||
LeafletKeyboardEvent,
|
||||
LeafletMouseEvent,
|
||||
LocationEvent,
|
||||
PopupEvent,
|
||||
ResizeEvent,
|
||||
TileErrorEvent,
|
||||
TileEvent,
|
||||
TooltipEvent,
|
||||
ZoomAnimEvent,
|
||||
} from 'leaflet';
|
||||
|
||||
export interface MoveEvents {
|
||||
movestart: LeafletEvent;
|
||||
move: LeafletEvent;
|
||||
moveend: LeafletEvent;
|
||||
}
|
||||
|
||||
export interface LayerAddRemoveEvents {
|
||||
add: LeafletEvent;
|
||||
remove: LeafletEvent;
|
||||
}
|
||||
|
||||
export interface MouseEvents {
|
||||
click: LeafletMouseEvent;
|
||||
dblclick: LeafletMouseEvent;
|
||||
mousedown: LeafletMouseEvent;
|
||||
mouseup: LeafletMouseEvent;
|
||||
mouseover: LeafletMouseEvent;
|
||||
mouseout: LeafletMouseEvent;
|
||||
contextmenu: LeafletMouseEvent;
|
||||
}
|
||||
|
||||
export interface PopupBindEvents {
|
||||
popupopen: PopupEvent;
|
||||
popupclose: PopupEvent;
|
||||
}
|
||||
|
||||
export interface TooltipBindEvents {
|
||||
tooltipopen: TooltipEvent;
|
||||
tooltipclose: TooltipEvent;
|
||||
}
|
||||
|
||||
export interface DragEvents {
|
||||
dragstart: LeafletEvent;
|
||||
drag: LeafletEvent;
|
||||
dragend: DragEndEvent;
|
||||
}
|
||||
|
||||
export interface TileEvents {
|
||||
loading: LeafletEvent;
|
||||
load: LeafletEvent;
|
||||
tileloadstart: TileEvent;
|
||||
tileload: TileEvent;
|
||||
tileunload: TileEvent;
|
||||
tileerror: TileErrorEvent;
|
||||
}
|
||||
|
||||
export interface DivOverlayEvents {
|
||||
contentupdate: LeafletEvent;
|
||||
}
|
||||
|
||||
export interface LayerGroupEvents {
|
||||
layeradd: LayerEvent;
|
||||
layerremove: LayerEvent;
|
||||
}
|
||||
|
||||
// Every non-group layer (marker, path, overlay, tile layer...) can have a
|
||||
// popup/tooltip bound to it regardless of its more specific family.
|
||||
export type BaseLayerEvents = LayerAddRemoveEvents & PopupBindEvents & TooltipBindEvents;
|
||||
|
||||
export type PathEvents = BaseLayerEvents & MouseEvents;
|
||||
|
||||
export type MarkerEvents = BaseLayerEvents & MouseEvents & MoveEvents & DragEvents;
|
||||
|
||||
export type TileLayerEvents = BaseLayerEvents & TileEvents;
|
||||
|
||||
// Popup/Tooltip themselves don't fire popupopen/tooltipopen about
|
||||
// themselves -- that fires on whatever they're bound to -- so this is
|
||||
// LayerAddRemoveEvents rather than the fuller BaseLayerEvents.
|
||||
export type DivOverlayLayerEvents = LayerAddRemoveEvents & MouseEvents & DivOverlayEvents;
|
||||
|
||||
// LayerGroup, FeatureGroup and GeoJSON (itself a FeatureGroup) all get both
|
||||
// their own add/remove and their children's layeradd/layerremove.
|
||||
export type GroupEvents = LayerAddRemoveEvents & LayerGroupEvents;
|
||||
|
||||
export interface MapEvents
|
||||
extends MoveEvents, MouseEvents, PopupBindEvents, TooltipBindEvents, LayerGroupEvents {
|
||||
zoomstart: LeafletEvent;
|
||||
zoomend: LeafletEvent;
|
||||
zoom: LeafletEvent;
|
||||
zoomlevelschange: LeafletEvent;
|
||||
viewreset: LeafletEvent;
|
||||
load: LeafletEvent;
|
||||
unload: LeafletEvent;
|
||||
resize: ResizeEvent;
|
||||
autopanstart: LeafletEvent;
|
||||
locationerror: LeafletErrorEvent;
|
||||
locationfound: LocationEvent;
|
||||
baselayerchange: LayersControlEvent;
|
||||
overlayadd: LayersControlEvent;
|
||||
overlayremove: LayersControlEvent;
|
||||
keypress: LeafletKeyboardEvent;
|
||||
keydown: LeafletKeyboardEvent;
|
||||
keyup: LeafletKeyboardEvent;
|
||||
zoomanim: ZoomAnimEvent;
|
||||
preclick: LeafletMouseEvent;
|
||||
}
|
||||
@ -12,12 +12,17 @@ export type LeafletLayerEvent = CustomEvent<{ layer: Layer }>;
|
||||
|
||||
export type LeafletIconChangedEvent = CustomEvent<{ icon: Icon | null }>;
|
||||
|
||||
// Fired by <leaflet-line> on itself when its lat/lng changes; polygon/polyline
|
||||
// listen for it (bubbling) to know when to re-read their vertices.
|
||||
export type LeafletLineUpdatedEvent = CustomEvent<null>;
|
||||
|
||||
declare global {
|
||||
interface HTMLElementEventMap {
|
||||
'leaflet-register': LeafletRegisterEvent;
|
||||
'leaflet-add-layer': LeafletLayerEvent;
|
||||
'leaflet-remove-layer': LeafletLayerEvent;
|
||||
'icon-changed': LeafletIconChangedEvent;
|
||||
'line-updated': LeafletLineUpdatedEvent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -47,6 +47,39 @@ export type LeafletElementConstructor<TObj, TProps> = new () => HTMLElement &
|
||||
PropValues<TProps> &
|
||||
LeafletElement<TObj, TProps>;
|
||||
|
||||
// Type-only narrowing for addEventListener/removeEventListener so a
|
||||
// component's `leaflet:<name>` events (see #forwardEvents below) type-check
|
||||
// against the right Leaflet event payload. Like `declare readonly
|
||||
// leafletObject?: Marker`, a component applies this with a `declare` field --
|
||||
// TObj isn't reliably inferred from the PROPS table alone, so this can't live
|
||||
// in WithProps()'s own generics; it costs nothing at runtime either way.
|
||||
//
|
||||
// Deliberately has no generic `(type: string, ...)` fallback overload (unlike
|
||||
// HTMLElement's real addEventListener): a fallback would silently accept any
|
||||
// unrecognized `leaflet:*` name too, which defeats the point. The cost is
|
||||
// that a genuinely dynamic (non-literal) event name string needs a cast.
|
||||
export type LeafletAddEventListener<TEvents> = (<K extends keyof TEvents & string>(
|
||||
type: `leaflet:${K}`,
|
||||
listener: (ev: CustomEvent<TEvents[K]>) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
) => void) &
|
||||
(<K extends keyof HTMLElementEventMap>(
|
||||
type: K,
|
||||
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => void,
|
||||
options?: boolean | AddEventListenerOptions,
|
||||
) => void);
|
||||
|
||||
export type LeafletRemoveEventListener<TEvents> = (<K extends keyof TEvents & string>(
|
||||
type: `leaflet:${K}`,
|
||||
listener: (ev: CustomEvent<TEvents[K]>) => void,
|
||||
options?: boolean | EventListenerOptions,
|
||||
) => void) &
|
||||
(<K extends keyof HTMLElementEventMap>(
|
||||
type: K,
|
||||
listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => void,
|
||||
options?: boolean | EventListenerOptions,
|
||||
) => void);
|
||||
|
||||
interface ResolvedProp<TObj extends Class> {
|
||||
name: string;
|
||||
attribute: string;
|
||||
@ -242,7 +275,13 @@ export function WithProps<TObj extends Class, TProps extends PropTable<TObj>>(
|
||||
if (typeof fire !== 'function') return;
|
||||
target.fire = (type, data, propagate) => {
|
||||
const result = fire.call(obj, type, data, propagate);
|
||||
this.dispatchEvent(new CustomEvent(`leaflet:${type}`, { detail: data ?? {} }));
|
||||
// Mirrors Leaflet's own Evented#fire merge (extend({}, data, {type,
|
||||
// target, sourceTarget})) so `detail` matches what a real `.on()`
|
||||
// listener receives -- and so Leaflet's typed event interfaces
|
||||
// (LeafletEvent's type/target/sourceTarget fields) are honest.
|
||||
const sourceTarget = (data as { sourceTarget?: unknown } | undefined)?.sourceTarget ?? obj;
|
||||
const detail = { ...data, type, target: obj, sourceTarget };
|
||||
this.dispatchEvent(new CustomEvent(`leaflet:${type}`, { detail }));
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
56
src/index.ts
56
src/index.ts
@ -24,3 +24,59 @@ export { LeafletPopup } from './components/leaflet-popup.ts';
|
||||
export { LeafletTooltip } from './components/leaflet-tooltip.ts';
|
||||
export { LeafletIcon } from './components/leaflet-icon.ts';
|
||||
export { LeafletDivIcon } from './components/leaflet-div-icon.ts';
|
||||
|
||||
// `export { X } from '...'` re-exports X but doesn't bind it locally, so the
|
||||
// tag map below needs its own type-only imports of the same classes.
|
||||
import type { LeafletMap } from './components/leaflet-map.ts';
|
||||
import type { LeafletMarker } from './components/leaflet-marker.ts';
|
||||
import type { LeafletCircle } from './components/leaflet-circle.ts';
|
||||
import type { LeafletCircleMarker } from './components/leaflet-circle-marker.ts';
|
||||
import type { LeafletLine } from './components/leaflet-line.ts';
|
||||
import type { LeafletPolygon } from './components/leaflet-polygon.ts';
|
||||
import type { LeafletPolyline } from './components/leaflet-polyline.ts';
|
||||
import type { LeafletRectangle } from './components/leaflet-rectangle.ts';
|
||||
import type { LeafletTileLayer } from './components/leaflet-tile-layer.ts';
|
||||
import type { LeafletTileLayerWMS } from './components/leaflet-tile-layer-wms.ts';
|
||||
import type { LeafletImageOverlay } from './components/leaflet-image-overlay.ts';
|
||||
import type { LeafletVideoOverlay } from './components/leaflet-video-overlay.ts';
|
||||
import type { LeafletSVGOverlay } from './components/leaflet-svg-overlay.ts';
|
||||
import type { LeafletLayerGroup } from './components/leaflet-layer-group.ts';
|
||||
import type { LeafletFeatureGroup } from './components/leaflet-feature-group.ts';
|
||||
import type { LeafletGeoJSON } from './components/leaflet-geojson.ts';
|
||||
import type { LeafletControlLayers } from './components/leaflet-control-layers.ts';
|
||||
import type { LeafletControlZoom } from './components/leaflet-control-zoom.ts';
|
||||
import type { LeafletControlAttribution } from './components/leaflet-control-attribution.ts';
|
||||
import type { LeafletControlScale } from './components/leaflet-control-scale.ts';
|
||||
import type { LeafletPopup } from './components/leaflet-popup.ts';
|
||||
import type { LeafletTooltip } from './components/leaflet-tooltip.ts';
|
||||
import type { LeafletIcon } from './components/leaflet-icon.ts';
|
||||
import type { LeafletDivIcon } from './components/leaflet-div-icon.ts';
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'leaflet-map': LeafletMap;
|
||||
'leaflet-marker': LeafletMarker;
|
||||
'leaflet-circle': LeafletCircle;
|
||||
'leaflet-circle-marker': LeafletCircleMarker;
|
||||
'leaflet-line': LeafletLine;
|
||||
'leaflet-polygon': LeafletPolygon;
|
||||
'leaflet-polyline': LeafletPolyline;
|
||||
'leaflet-rectangle': LeafletRectangle;
|
||||
'leaflet-tile-layer': LeafletTileLayer;
|
||||
'leaflet-tile-layer-wms': LeafletTileLayerWMS;
|
||||
'leaflet-image-overlay': LeafletImageOverlay;
|
||||
'leaflet-video-overlay': LeafletVideoOverlay;
|
||||
'leaflet-svg-overlay': LeafletSVGOverlay;
|
||||
'leaflet-layer-group': LeafletLayerGroup;
|
||||
'leaflet-feature-group': LeafletFeatureGroup;
|
||||
'leaflet-geojson': LeafletGeoJSON;
|
||||
'leaflet-control-layers': LeafletControlLayers;
|
||||
'leaflet-control-zoom': LeafletControlZoom;
|
||||
'leaflet-control-attribution': LeafletControlAttribution;
|
||||
'leaflet-control-scale': LeafletControlScale;
|
||||
'leaflet-popup': LeafletPopup;
|
||||
'leaflet-tooltip': LeafletTooltip;
|
||||
'leaflet-icon': LeafletIcon;
|
||||
'leaflet-div-icon': LeafletDivIcon;
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,27 +11,21 @@ import '../../src/components/leaflet-marker.ts';
|
||||
|
||||
describe('leaflet-control-zoom / scale / attribution', () => {
|
||||
it('create their matching Control class with position and options', () => {
|
||||
const zoom = document.createElement('leaflet-control-zoom') as HTMLElement & {
|
||||
leafletObject?: Control.Zoom;
|
||||
};
|
||||
const zoom = document.createElement('leaflet-control-zoom');
|
||||
zoom.setAttribute('position', 'bottomleft');
|
||||
document.body.append(zoom);
|
||||
expect(zoom.leafletObject).toBeInstanceOf(Control.Zoom);
|
||||
expect(zoom.leafletObject?.options.position).toBe('bottomleft');
|
||||
zoom.remove();
|
||||
|
||||
const scale = document.createElement('leaflet-control-scale') as HTMLElement & {
|
||||
leafletObject?: Control.Scale;
|
||||
};
|
||||
const scale = document.createElement('leaflet-control-scale');
|
||||
scale.setAttribute('max-width', '150');
|
||||
document.body.append(scale);
|
||||
expect(scale.leafletObject).toBeInstanceOf(Control.Scale);
|
||||
expect(scale.leafletObject?.options.maxWidth).toBe(150);
|
||||
scale.remove();
|
||||
|
||||
const attribution = document.createElement('leaflet-control-attribution') as HTMLElement & {
|
||||
leafletObject?: Control.Attribution;
|
||||
};
|
||||
const attribution = document.createElement('leaflet-control-attribution');
|
||||
attribution.setAttribute('prefix', 'Test');
|
||||
document.body.append(attribution);
|
||||
expect(attribution.leafletObject).toBeInstanceOf(Control.Attribution);
|
||||
@ -42,9 +36,7 @@ describe('leaflet-control-zoom / scale / attribution', () => {
|
||||
|
||||
describe('leaflet-control-layers', () => {
|
||||
it('sorts existing children into base layers vs overlays by the type attribute', () => {
|
||||
const el = document.createElement('leaflet-control-layers') as HTMLElement & {
|
||||
leafletObject?: Control.Layers;
|
||||
};
|
||||
const el = document.createElement('leaflet-control-layers');
|
||||
const base = document.createElement('leaflet-tile-layer');
|
||||
base.setAttribute('url', 'https://a.example/{z}/{x}/{y}.png');
|
||||
base.setAttribute('name', 'Base');
|
||||
@ -77,9 +69,7 @@ describe('leaflet-control-layers', () => {
|
||||
|
||||
describe('leaflet-layer-group / leaflet-feature-group', () => {
|
||||
it('are passthrough WithProps({}) containers that adopt registering children', () => {
|
||||
const group = document.createElement('leaflet-layer-group') as HTMLElement & {
|
||||
leafletObject?: LayerGroup;
|
||||
};
|
||||
const group = document.createElement('leaflet-layer-group');
|
||||
document.body.append(group);
|
||||
expect(group.leafletObject).toBeInstanceOf(LayerGroup);
|
||||
|
||||
@ -91,9 +81,7 @@ describe('leaflet-layer-group / leaflet-feature-group', () => {
|
||||
expect(group.leafletObject?.getLayers()).toHaveLength(1);
|
||||
group.remove();
|
||||
|
||||
const feature = document.createElement('leaflet-feature-group') as HTMLElement & {
|
||||
leafletObject?: FeatureGroup;
|
||||
};
|
||||
const feature = document.createElement('leaflet-feature-group');
|
||||
document.body.append(feature);
|
||||
expect(feature.leafletObject).toBeInstanceOf(FeatureGroup);
|
||||
feature.remove();
|
||||
|
||||
@ -21,9 +21,7 @@ const FEATURE = {
|
||||
|
||||
describe('leaflet-geojson', () => {
|
||||
it('builds a GeoJSON layer from the data attribute with pathProps nested as style', () => {
|
||||
const el = document.createElement('leaflet-geojson') as HTMLElement & {
|
||||
leafletObject?: GeoJSON;
|
||||
};
|
||||
const el = document.createElement('leaflet-geojson');
|
||||
el.setAttribute('data', JSON.stringify(FEATURE));
|
||||
el.setAttribute('color', '#e67e22');
|
||||
el.setAttribute('fill-opacity', '0.4');
|
||||
@ -39,9 +37,7 @@ describe('leaflet-geojson', () => {
|
||||
});
|
||||
|
||||
it('replaces its layers when the data attribute changes', () => {
|
||||
const el = document.createElement('leaflet-geojson') as HTMLElement & {
|
||||
leafletObject?: GeoJSON;
|
||||
};
|
||||
const el = document.createElement('leaflet-geojson');
|
||||
el.setAttribute('data', JSON.stringify(FEATURE));
|
||||
document.body.append(el);
|
||||
expect(el.leafletObject?.getLayers()).toHaveLength(1);
|
||||
|
||||
@ -5,9 +5,7 @@ import '../../src/components/leaflet-div-icon.ts';
|
||||
|
||||
describe('leaflet-icon', () => {
|
||||
it('creates nothing until icon-url is set, then an Icon once it is', () => {
|
||||
const el = document.createElement('leaflet-icon') as HTMLElement & {
|
||||
leafletObject?: Icon;
|
||||
};
|
||||
const el = document.createElement('leaflet-icon');
|
||||
document.body.append(el);
|
||||
expect(el.leafletObject).toBeUndefined();
|
||||
|
||||
@ -18,9 +16,7 @@ describe('leaflet-icon', () => {
|
||||
});
|
||||
|
||||
it('rebuilds (not mutates) the Icon on every attribute change and re-emits icon-changed', () => {
|
||||
const el = document.createElement('leaflet-icon') as HTMLElement & {
|
||||
leafletObject?: Icon;
|
||||
};
|
||||
const el = document.createElement('leaflet-icon');
|
||||
el.setAttribute('icon-url', 'https://example.com/a.png');
|
||||
const onChanged = vi.fn();
|
||||
el.addEventListener('icon-changed', onChanged);
|
||||
@ -38,9 +34,7 @@ describe('leaflet-icon', () => {
|
||||
|
||||
describe('leaflet-div-icon', () => {
|
||||
it('defaults className to Leaflet’s own default and renders markup as html', () => {
|
||||
const el = document.createElement('leaflet-div-icon') as HTMLElement & {
|
||||
leafletObject?: DivIcon;
|
||||
};
|
||||
const el = document.createElement('leaflet-div-icon');
|
||||
el.innerHTML = '<span>*</span>';
|
||||
document.body.append(el);
|
||||
|
||||
|
||||
@ -8,11 +8,7 @@ import '../../src/components/leaflet-icon.ts';
|
||||
|
||||
describe('leaflet-marker', () => {
|
||||
it('creates a Marker at lat/lng and keeps lat/lng synced while it moves', () => {
|
||||
const el = document.createElement('leaflet-marker') as HTMLElement & {
|
||||
leafletObject?: Marker;
|
||||
lat: number;
|
||||
lng: number;
|
||||
};
|
||||
const el = document.createElement('leaflet-marker');
|
||||
el.setAttribute('lat', '51.5');
|
||||
el.setAttribute('lng', '-0.09');
|
||||
document.body.append(el);
|
||||
@ -35,9 +31,7 @@ describe('leaflet-marker', () => {
|
||||
const map = document.createElement('leaflet-map');
|
||||
document.body.append(map);
|
||||
|
||||
const el = document.createElement('leaflet-marker') as HTMLElement & {
|
||||
leafletObject?: Marker;
|
||||
};
|
||||
const el = document.createElement('leaflet-marker');
|
||||
el.setAttribute('lat', '0');
|
||||
el.setAttribute('lng', '0');
|
||||
el.setAttribute('draggable', '');
|
||||
@ -50,9 +44,7 @@ describe('leaflet-marker', () => {
|
||||
});
|
||||
|
||||
it('swaps in a <leaflet-icon> child via the icon-changed handshake', () => {
|
||||
const el = document.createElement('leaflet-marker') as HTMLElement & {
|
||||
leafletObject?: Marker;
|
||||
};
|
||||
const el = document.createElement('leaflet-marker');
|
||||
el.setAttribute('lat', '0');
|
||||
el.setAttribute('lng', '0');
|
||||
document.body.append(el);
|
||||
@ -61,18 +53,14 @@ describe('leaflet-marker', () => {
|
||||
icon.setAttribute('icon-url', 'https://example.com/icon.png');
|
||||
el.append(icon);
|
||||
|
||||
expect(el.leafletObject?.options.icon).toBe(
|
||||
(icon as HTMLElement & { leafletObject?: unknown }).leafletObject,
|
||||
);
|
||||
expect(el.leafletObject?.options.icon).toBe(icon.leafletObject);
|
||||
el.remove();
|
||||
});
|
||||
});
|
||||
|
||||
describe('leaflet-popup', () => {
|
||||
it('takes its content from innerHTML and stays in sync on mutation', async () => {
|
||||
const el = document.createElement('leaflet-popup') as HTMLElement & {
|
||||
leafletObject?: Popup;
|
||||
};
|
||||
const el = document.createElement('leaflet-popup');
|
||||
el.innerHTML = '<b>hello</b>';
|
||||
document.body.append(el);
|
||||
|
||||
@ -87,18 +75,14 @@ describe('leaflet-popup', () => {
|
||||
});
|
||||
|
||||
it('only sets its own latlng when lat/lng attributes are present', () => {
|
||||
const withLatLng = document.createElement('leaflet-popup') as HTMLElement & {
|
||||
leafletObject?: Popup;
|
||||
};
|
||||
const withLatLng = document.createElement('leaflet-popup');
|
||||
withLatLng.setAttribute('lat', '1');
|
||||
withLatLng.setAttribute('lng', '2');
|
||||
document.body.append(withLatLng);
|
||||
expect(withLatLng.leafletObject?.getLatLng()).toMatchObject({ lat: 1, lng: 2 });
|
||||
withLatLng.remove();
|
||||
|
||||
const withoutLatLng = document.createElement('leaflet-popup') as HTMLElement & {
|
||||
leafletObject?: Popup;
|
||||
};
|
||||
const withoutLatLng = document.createElement('leaflet-popup');
|
||||
document.body.append(withoutLatLng);
|
||||
expect(withoutLatLng.leafletObject?.getLatLng()).toBeUndefined();
|
||||
withoutLatLng.remove();
|
||||
@ -107,9 +91,7 @@ describe('leaflet-popup', () => {
|
||||
|
||||
describe('leaflet-tooltip', () => {
|
||||
it('creates a Tooltip with content and direction/permanent/sticky options', () => {
|
||||
const el = document.createElement('leaflet-tooltip') as HTMLElement & {
|
||||
leafletObject?: Tooltip;
|
||||
};
|
||||
const el = document.createElement('leaflet-tooltip');
|
||||
el.innerHTML = 'hover text';
|
||||
el.setAttribute('direction', 'top');
|
||||
el.setAttribute('permanent', '');
|
||||
|
||||
@ -12,11 +12,7 @@ const BOUNDS: [[number, number], [number, number]] = [
|
||||
|
||||
describe('leaflet-image-overlay', () => {
|
||||
it('creates an ImageOverlay with a live bounds getter (url has no Leaflet getter to read back)', () => {
|
||||
const el = document.createElement('leaflet-image-overlay') as HTMLElement & {
|
||||
leafletObject?: ImageOverlay;
|
||||
url: string;
|
||||
bounds: unknown;
|
||||
};
|
||||
const el = document.createElement('leaflet-image-overlay');
|
||||
el.setAttribute('url', 'https://example.com/a.png');
|
||||
el.setAttribute('bounds', JSON.stringify(BOUNDS));
|
||||
document.body.append(el);
|
||||
@ -42,9 +38,7 @@ describe('leaflet-image-overlay', () => {
|
||||
|
||||
describe('leaflet-video-overlay', () => {
|
||||
it('creates a VideoOverlay and applies the newly added options', () => {
|
||||
const el = document.createElement('leaflet-video-overlay') as HTMLElement & {
|
||||
leafletObject?: VideoOverlay;
|
||||
};
|
||||
const el = document.createElement('leaflet-video-overlay');
|
||||
el.setAttribute('url', 'https://example.com/a.mp4');
|
||||
el.setAttribute('bounds', JSON.stringify(BOUNDS));
|
||||
el.setAttribute('z-index', '5');
|
||||
@ -67,10 +61,7 @@ describe('leaflet-video-overlay', () => {
|
||||
const map = document.createElement('leaflet-map');
|
||||
document.body.append(map);
|
||||
|
||||
const el = document.createElement('leaflet-video-overlay') as HTMLElement & {
|
||||
leafletObject?: VideoOverlay;
|
||||
getElement(): HTMLVideoElement | undefined;
|
||||
};
|
||||
const el = document.createElement('leaflet-video-overlay');
|
||||
el.setAttribute('url', 'https://example.com/a.mp4');
|
||||
el.setAttribute('bounds', JSON.stringify(BOUNDS));
|
||||
el.setAttribute('muted', '');
|
||||
@ -86,10 +77,7 @@ describe('leaflet-video-overlay', () => {
|
||||
|
||||
describe('leaflet-svg-overlay', () => {
|
||||
it('creates an SVGOverlay with a live bounds getter', () => {
|
||||
const el = document.createElement('leaflet-svg-overlay') as HTMLElement & {
|
||||
leafletObject?: SVGOverlay;
|
||||
bounds: unknown;
|
||||
};
|
||||
const el = document.createElement('leaflet-svg-overlay');
|
||||
el.setAttribute('bounds', JSON.stringify(BOUNDS));
|
||||
document.body.append(el);
|
||||
|
||||
|
||||
@ -9,10 +9,7 @@ import '../../src/components/leaflet-line.ts';
|
||||
|
||||
describe('leaflet-circle', () => {
|
||||
it('creates a Circle at lat/lng with radius and pathProps applied', () => {
|
||||
const el = document.createElement('leaflet-circle') as HTMLElement & {
|
||||
leafletObject?: Circle;
|
||||
radius: number;
|
||||
};
|
||||
const el = document.createElement('leaflet-circle');
|
||||
el.setAttribute('lat', '51.5');
|
||||
el.setAttribute('lng', '-0.09');
|
||||
el.setAttribute('radius', '500');
|
||||
@ -29,10 +26,7 @@ describe('leaflet-circle', () => {
|
||||
});
|
||||
|
||||
it('radius property reads the live value and reacts to attribute changes', () => {
|
||||
const el = document.createElement('leaflet-circle') as HTMLElement & {
|
||||
leafletObject?: Circle;
|
||||
radius: number;
|
||||
};
|
||||
const el = document.createElement('leaflet-circle');
|
||||
document.body.append(el);
|
||||
// Leaflet's own default, no attribute set
|
||||
expect(el.radius).toBe(1000);
|
||||
@ -51,10 +45,7 @@ describe('leaflet-circle', () => {
|
||||
|
||||
describe('leaflet-circle-marker', () => {
|
||||
it('creates a CircleMarker with a live radius getter', () => {
|
||||
const el = document.createElement('leaflet-circle-marker') as HTMLElement & {
|
||||
leafletObject?: CircleMarker;
|
||||
radius: number;
|
||||
};
|
||||
const el = document.createElement('leaflet-circle-marker');
|
||||
el.setAttribute('lat', '1');
|
||||
el.setAttribute('lng', '2');
|
||||
el.setAttribute('radius', '15');
|
||||
@ -70,10 +61,7 @@ describe('leaflet-circle-marker', () => {
|
||||
|
||||
describe('leaflet-rectangle', () => {
|
||||
it('creates a Rectangle from the bounds attribute with a live bounds getter', () => {
|
||||
const el = document.createElement('leaflet-rectangle') as HTMLElement & {
|
||||
leafletObject?: Rectangle;
|
||||
bounds: [[number, number], [number, number]];
|
||||
};
|
||||
const el = document.createElement('leaflet-rectangle');
|
||||
el.setAttribute(
|
||||
'bounds',
|
||||
JSON.stringify([
|
||||
@ -94,9 +82,7 @@ describe('leaflet-rectangle', () => {
|
||||
|
||||
describe('leaflet-polygon', () => {
|
||||
it('collects vertices from <leaflet-line> children and updates on change', () => {
|
||||
const el = document.createElement('leaflet-polygon') as HTMLElement & {
|
||||
leafletObject?: Polygon;
|
||||
};
|
||||
const el = document.createElement('leaflet-polygon');
|
||||
const line1 = document.createElement('leaflet-line');
|
||||
line1.setAttribute('lat', '1');
|
||||
line1.setAttribute('lng', '2');
|
||||
@ -127,9 +113,7 @@ describe('leaflet-polygon', () => {
|
||||
|
||||
describe('leaflet-polyline', () => {
|
||||
it('applies smoothFactor/noClip and is unfilled by default', () => {
|
||||
const el = document.createElement('leaflet-polyline') as HTMLElement & {
|
||||
leafletObject?: Polyline;
|
||||
};
|
||||
const el = document.createElement('leaflet-polyline');
|
||||
el.setAttribute('smooth-factor', '2.5');
|
||||
el.setAttribute('no-clip', '');
|
||||
const line = document.createElement('leaflet-line');
|
||||
|
||||
@ -5,9 +5,7 @@ import '../../src/components/leaflet-tile-layer-wms.ts';
|
||||
|
||||
describe('leaflet-tile-layer', () => {
|
||||
it('creates a TileLayer from the url attribute with tileLayerProps options', () => {
|
||||
const el = document.createElement('leaflet-tile-layer') as HTMLElement & {
|
||||
leafletObject?: TileLayer;
|
||||
};
|
||||
const el = document.createElement('leaflet-tile-layer');
|
||||
el.setAttribute('url', 'https://tile.example/{z}/{x}/{y}.png');
|
||||
el.setAttribute('subdomains', 'abcd');
|
||||
el.setAttribute('tms', '');
|
||||
@ -23,10 +21,7 @@ describe('leaflet-tile-layer', () => {
|
||||
});
|
||||
|
||||
it('defaults zIndex to 1, matching Leaflet', () => {
|
||||
const el = document.createElement('leaflet-tile-layer') as HTMLElement & {
|
||||
leafletObject?: TileLayer;
|
||||
zIndex: number;
|
||||
};
|
||||
const el = document.createElement('leaflet-tile-layer');
|
||||
el.setAttribute('url', 'https://tile.example/{z}/{x}/{y}.png');
|
||||
document.body.append(el);
|
||||
expect(el.leafletObject?.options.zIndex).toBe(1);
|
||||
@ -36,9 +31,7 @@ describe('leaflet-tile-layer', () => {
|
||||
|
||||
describe('leaflet-tile-layer-wms', () => {
|
||||
it('shares base tileLayerProps with leaflet-tile-layer (previously missing entirely)', () => {
|
||||
const el = document.createElement('leaflet-tile-layer-wms') as HTMLElement & {
|
||||
leafletObject?: TileLayer.WMS;
|
||||
};
|
||||
const el = document.createElement('leaflet-tile-layer-wms');
|
||||
el.setAttribute('url', 'https://wms.example/service');
|
||||
el.setAttribute('layers', 'basic');
|
||||
el.setAttribute('attribution', 'Example');
|
||||
@ -54,9 +47,7 @@ describe('leaflet-tile-layer-wms', () => {
|
||||
|
||||
it('resolves crs by name to the matching Leaflet CRS instance', async () => {
|
||||
const { CRS } = await import('leaflet');
|
||||
const el = document.createElement('leaflet-tile-layer-wms') as HTMLElement & {
|
||||
leafletObject?: TileLayer.WMS;
|
||||
};
|
||||
const el = document.createElement('leaflet-tile-layer-wms');
|
||||
el.setAttribute('url', 'https://wms.example/service');
|
||||
el.setAttribute('layers', 'basic');
|
||||
el.setAttribute('crs', 'EPSG4326');
|
||||
|
||||
53
test/core/event-types.test.ts
Normal file
53
test/core/event-types.test.ts
Normal file
@ -0,0 +1,53 @@
|
||||
// Compile-time-only checks for the per-component addEventListener/
|
||||
// removeEventListener typing and the HTMLElementTagNameMap augmentation.
|
||||
// expectTypeOf() assertions are checked by `tsc -p tsconfig.test.json`
|
||||
// (part of `npm run typecheck`), not at runtime -- a mismatched
|
||||
// `@ts-expect-error` (an expected error that doesn't actually occur) fails
|
||||
// that typecheck with "Unused '@ts-expect-error' directive".
|
||||
import type { DragEndEvent, Popup } from 'leaflet';
|
||||
import { describe, expectTypeOf, it } from 'vitest';
|
||||
import type { LeafletMap } from '../../src/components/leaflet-map.ts';
|
||||
import type { LeafletMarker } from '../../src/components/leaflet-marker.ts';
|
||||
import type { LeafletControlZoom } from '../../src/components/leaflet-control-zoom.ts';
|
||||
|
||||
describe('HTMLElementTagNameMap augmentation', () => {
|
||||
it('createElement/querySelector infer the right component class', () => {
|
||||
expectTypeOf(document.createElement('leaflet-map')).toEqualTypeOf<LeafletMap>();
|
||||
expectTypeOf(document.createElement('leaflet-marker')).toEqualTypeOf<LeafletMarker>();
|
||||
});
|
||||
});
|
||||
|
||||
describe('per-component addEventListener typing', () => {
|
||||
it('types a known leaflet: event with the right Leaflet event payload', () => {
|
||||
const map = document.createElement('leaflet-map');
|
||||
map.addEventListener('leaflet:popupopen', (e) => {
|
||||
expectTypeOf(e.detail.popup).toEqualTypeOf<Popup>();
|
||||
});
|
||||
|
||||
const marker = document.createElement('leaflet-marker');
|
||||
marker.addEventListener('leaflet:dragend', (e) => {
|
||||
expectTypeOf(e.detail.distance).toEqualTypeOf<DragEndEvent['distance']>();
|
||||
});
|
||||
});
|
||||
|
||||
it('still types ordinary DOM events normally', () => {
|
||||
const marker = document.createElement('leaflet-marker');
|
||||
marker.addEventListener('click', (e) => {
|
||||
expectTypeOf(e).toEqualTypeOf<HTMLElementEventMap['click']>();
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects a leaflet: event name that component does not fire', () => {
|
||||
const marker = document.createElement('leaflet-marker');
|
||||
// @ts-expect-error -- baselayerchange is Map-only, not one of MarkerEvents
|
||||
marker.addEventListener('leaflet:baselayerchange', () => {});
|
||||
});
|
||||
|
||||
it('components with no custom events keep the default HTMLElementEventMap typing', () => {
|
||||
const zoom = document.createElement('leaflet-control-zoom');
|
||||
expectTypeOf(zoom.addEventListener).toEqualTypeOf<LeafletControlZoom['addEventListener']>();
|
||||
zoom.addEventListener('click', (e) => {
|
||||
expectTypeOf(e).toEqualTypeOf<HTMLElementEventMap['click']>();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -333,7 +333,14 @@ describe('WithProps: generic leaflet: event forwarding', () => {
|
||||
|
||||
expect(onNode).toHaveBeenCalledOnce();
|
||||
const event = onNode.mock.calls[0][0] as CustomEvent;
|
||||
expect(event.detail).toEqual({ foo: 1 });
|
||||
// Mirrors Leaflet's own fire() merge: the original data plus
|
||||
// type/target/sourceTarget, matching what a real .on() listener gets.
|
||||
expect(event.detail).toEqual({
|
||||
foo: 1,
|
||||
type: 'zoomend',
|
||||
target: node.leafletObject,
|
||||
sourceTarget: node.leafletObject,
|
||||
});
|
||||
// Does not bubble.
|
||||
expect(onWrapper).not.toHaveBeenCalled();
|
||||
wrapper.remove();
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { FeatureGroup, Map as LMap, Marker, Popup, TileLayer } from 'leaflet';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import '../src/components/leaflet-map.ts';
|
||||
import '../src/components/leaflet-tile-layer.ts';
|
||||
@ -12,31 +11,23 @@ import '../src/components/leaflet-popup.ts';
|
||||
// instead of bubbling past it.
|
||||
describe('component tree wiring', () => {
|
||||
it('wires a map + tile-layer + feature-group + marker + popup tree together', () => {
|
||||
const map = document.createElement('leaflet-map') as HTMLElement & {
|
||||
leafletObject?: LMap;
|
||||
};
|
||||
const map = document.createElement('leaflet-map');
|
||||
map.setAttribute('lat', '51.5');
|
||||
map.setAttribute('lng', '-0.09');
|
||||
map.setAttribute('zoom', '13');
|
||||
|
||||
const tiles = document.createElement('leaflet-tile-layer') as HTMLElement & {
|
||||
leafletObject?: TileLayer;
|
||||
};
|
||||
map.zoomSnap = 0.5;
|
||||
|
||||
const tiles = document.createElement('leaflet-tile-layer');
|
||||
tiles.setAttribute('url', 'https://tile.example/{z}/{x}/{y}.png');
|
||||
|
||||
const group = document.createElement('leaflet-feature-group') as HTMLElement & {
|
||||
leafletObject?: FeatureGroup;
|
||||
};
|
||||
const group = document.createElement('leaflet-feature-group');
|
||||
|
||||
const marker = document.createElement('leaflet-marker') as HTMLElement & {
|
||||
leafletObject?: Marker;
|
||||
};
|
||||
const marker = document.createElement('leaflet-marker');
|
||||
marker.setAttribute('lat', '51.51');
|
||||
marker.setAttribute('lng', '-0.1');
|
||||
|
||||
const popup = document.createElement('leaflet-popup') as HTMLElement & {
|
||||
leafletObject?: Popup;
|
||||
};
|
||||
const popup = document.createElement('leaflet-popup');
|
||||
popup.innerHTML = 'hello';
|
||||
|
||||
marker.append(popup);
|
||||
@ -59,9 +50,7 @@ describe('component tree wiring', () => {
|
||||
expect(marker.leafletObject?.getPopup()).toBe(popup.leafletObject);
|
||||
|
||||
// A marker added later goes through the same path.
|
||||
const marker2 = document.createElement('leaflet-marker') as HTMLElement & {
|
||||
leafletObject?: Marker;
|
||||
};
|
||||
const marker2 = document.createElement('leaflet-marker');
|
||||
marker2.setAttribute('lat', '1');
|
||||
marker2.setAttribute('lng', '2');
|
||||
group.append(marker2);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user