6 Commits (6ff1b75ed32fc529e0b8f7f6c2136ce830d85c63)

Author SHA1 Message Date
Buddy cd2c8d0fc5 feat(leaflet-map): fit-to-markers view
Add fit-to-markers / fit-padding / fit-max-zoom attributes to <leaflet-map>.
When fit-to-markers is set the map ignores lat/lng/zoom and fitBounds()es a
box around every locatable layer (getLatLng / getBounds); tile layers and
open popups/tooltips are skipped.

The only reframe trigger is a component registering into the tree -- initial
load and later additions. Panning, zooming, opening a popup and removing a
marker all leave the view untouched. Reframes coalesce onto a microtask so
load produces one fitBounds call, not one per marker.
2 weeks ago
Buddy 449767335b chore: migrate formatter from prettier to oxfmt
Replace prettier with oxfmt (oxc's Prettier-compatible formatter), config
seeded via `oxfmt --migrate=prettier` and kept as `oxfmt.config.ts`
(defineConfig default export) so settings match the old prettier.config.js.
The `format` script now runs `oxfmt`; docs and the local tool allowlist
updated to match.
3 weeks ago
Buddy ef7828128b feat: make WMS crs extensible via a nested provider element, not a registry
Replaces the mutable registerCRS() registry (never released beyond this
branch) with a web-components-first design: any custom element, no base
class required, can provide a CRS Leaflet doesn't ship by nesting inside
<leaflet-tile-layer-wms> and firing a bubbling leaflet-crs-changed event
(detail: { crs: CRS | null }, mirroring icon-changed's { icon: null }
pattern for "revert to default"). The plain `crs="EPSG4326"` attribute
stays as a convenience shortcut for the 4 CRSes Leaflet itself ships; a
nested provider takes priority over it when both are present.

This surfaced a real, previously-undiscovered bug in with-props.ts:
recreateLeafletObject() was declared on the public LeafletElement
interface but only ever implemented as a private #recreateLeafletObject()
-- calling it would throw "is not a function" at runtime despite
type-checking cleanly. Fixed by making it public, and it now also
re-dispatches registerWithParent() for attach !== 'none' components,
which it never did before (previously only safe for attach: 'none'
components like icons, since recreate would otherwise rebuild a layer
without ever re-adding it to whatever registered it the first time --
exactly what leaflet-tile-layer-wms needs when a nested provider's crs
arrives after it already constructed itself with the default).
4 weeks ago
Buddy 3845e53329 fix: remove parent-reaches-into-child patterns, fix a real load-order bug
Two parent-looks-for-child patterns found and removed, per the
child-emits-events-parent-never-reaches-in principle:

- leaflet-control-layers: #childLayers() queried children directly via
  querySelectorAll + reading their leafletObject/attributes, in two call
  sites. Verified empirically that a parent's connectedCallback always
  completes before a freshly-connected child's does (even for an
  already-built subtree attached in one shot), which means both call
  sites always ran against unpopulated children -- dead code. Deleted;
  #onChildRegister (already event-driven) was doing all the real work.

- leaflet-polygon/leaflet-polyline: #coords() queried <leaflet-line>
  children via querySelectorAll + read their .latlng property directly,
  using a MutationObserver + a payload-less event only as a "something
  changed, rescan everyone" signal. Rewritten to be purely event-driven:
  <leaflet-line> now fires leaflet-line-sync (connect + every lat/lng
  change, carrying its own position) and leaflet-line-remove (disconnect)
  on itself; a new shared VertexTracker (src/core/vertex-tracker.ts)
  turns that event stream into an ordered coordinate list, using
  compareDocumentPosition only to place a newly-registered vertex at its
  real document position rather than assuming registration order matches
  DOM order. No querySelectorAll, no MutationObserver, no reading a
  child's property.

Testing the polygon rewrite in an actual browser (not jsdom) surfaced two
real bugs, both fixed:

1. disconnectedCallback fires *after* a node is already detached from its
   parent, so leaflet-line's removal event had nowhere to bubble to.
   Fixed by caching parentNode while still connected and dispatching from
   that cached reference instead of from the (by-then-detached) node.

2. A load-order hazard affecting every "parent listens for a specific
   child tag's announcement" relationship in this codebase:
   customElements.define() upgrades every matching element already
   parsed into the page immediately and synchronously, so whichever tag
   gets defined first wins a race -- a child tag defined before its
   listening parent fires its one-shot connect-time announcement into a
   parent that doesn't exist yet, and it's lost for good. This broke
   polygon/polyline (introduced by this rewrite, since leaflet-line was
   exported before leaflet-polygon) and, independently, was already
   silently broken for leaflet-control-layers (a base/overlay layer
   leaked directly onto the map, bypassing the control entirely) and
   latently for leaflet-layer-group/leaflet-feature-group. src/index.ts's
   export order now encodes and documents the real dependency hierarchy
   (map -> containers that listen for child announcements -> concrete
   layer types -> their own children).

   Added test/load-order.test.ts, which is structurally different from
   every other test file: it never statically imports a component
   module, building the DOM with plain undefined elements first and only
   dynamically import()ing src/index.ts afterward -- reproducing a real
   page's actual load order, which every other test's "define everything
   first" pattern cannot catch. Confirmed it fails against the old
   ordering and passes against the fix.
4 weeks ago
Buddy 0419ab781d 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.
4 weeks ago
Buddy 9b6c7ca598 test: add a Vitest + jsdom test suite
No test coverage existed before this. Adds vitest + jsdom (2 new
devDependencies) and a suite that runs entirely without a browser: real
Leaflet objects work fine under jsdom for everything this library needs to
verify (option/attribute wiring, event forwarding), given a ResizeObserver
stub in test/setup.ts (jsdom's only real gap here).

- test/core/with-props.test.ts: the WithProps mixin itself, against a fake
  Leaflet-like class -- attribute<->setter dispatch, get/attribute/default
  fallback order, event-driven attribute sync-back, positional/recreate/
  attach modes, and the generic fire()-patch event forwarding. Child
  registration (popup/tooltip/layer binding) uses real Popup/Tooltip/Marker
  instances since #onChildRegister discriminates by instanceof.
- test/core/props.test.ts: the codec functions in isolation.
- test/components/*.test.ts: grouped smoke tests across all components.
- test/integration.test.ts: full tree wiring (map + tile-layer +
  feature-group + marker + popup).

Caught and fixed one real bug along the way: urlProp's "live url getter"
from the last refactor was dead code -- neither ImageOverlay nor
VideoOverlay actually expose getUrl(). Removed it and the now-pointless
getUrl?() from the Sourced interface in shared-props.ts.

tsconfig.test.json keeps test/ out of the tsc build (dist/ stays
test-free) while still typechecking it; oxlint.config.ts now covers test/
too, with max-classes-per-file relaxed there since testing a class
factory means many small one-off element classes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
4 weeks ago