@solidjs/router 2.0.0-next.37 → 2.0.0-next.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -37,28 +37,35 @@ const comparableQuery = search => {
37
37
  };
38
38
 
39
39
  /**
40
- * The link-state rule shared by claimed anchors and `useLinkState`, given
41
- * the location and a link's already-resolved target (path, optional query
42
- * and hash):
40
+ * The link-state rule shared by claimed anchors and `useLinkState`, bound to
41
+ * a location so links matched against the same one share its parse. The
42
+ * returned matcher takes a link's already-resolved target (path, optional
43
+ * query and hash):
43
44
  *
44
45
  * - `current` — same pathname and same query (parameter order and hash aside)
45
46
  * - `active` — same pathname or one under it; the router's root (`base`, or
46
47
  * `/` without one) only matches exactly, since it is a prefix of every
47
48
  * page. `end` makes every link exact-only.
48
49
  */
49
- function matchLink(location, target, base, end) {
50
- if (target === undefined) return {
51
- active: false,
52
- current: false
53
- };
54
- const loc = comparablePath(location.pathname);
55
- const path = comparablePath(target);
56
- const exact = loc === path;
57
- const hashless = target.split("#", 1)[0];
58
- const q = hashless.indexOf("?");
59
- return {
60
- active: exact || !end && path !== "" && path !== comparablePath(base) && loc.startsWith(path + "/"),
61
- current: exact && comparableQuery(location.search) === comparableQuery(q < 0 ? "" : hashless.slice(q))
50
+ function linkMatcher({
51
+ pathname,
52
+ search
53
+ }, base, end) {
54
+ const loc = comparablePath(pathname);
55
+ const root = comparablePath(base);
56
+ return target => {
57
+ if (target === undefined) return {
58
+ active: false,
59
+ current: false
60
+ };
61
+ const path = comparablePath(target);
62
+ const exact = loc === path;
63
+ const hashless = target.split("#", 1)[0];
64
+ const q = hashless.indexOf("?");
65
+ return {
66
+ active: exact || !end && path !== "" && path !== root && loc.startsWith(path + "/"),
67
+ current: exact && comparableQuery(search) === comparableQuery(q < 0 ? "" : hashless.slice(q))
68
+ };
62
69
  };
63
70
  }
64
71
  function resolvePath(base, path, from) {
@@ -253,7 +260,7 @@ function setFormClaimHandler(handler) {
253
260
  * opt-in through `createRouter({ links: pendingLinks })`. Without the
254
261
  * plugin, claims never read pending state and the sweep does not track it.
255
262
  *
256
- * The matching rule is `matchLink`, shared with `useLinkState`. The router
263
+ * The matching rule is `linkMatcher`, shared with `useLinkState`. The router
257
264
  * only touches an `aria-current` it wrote itself: one the author set (a
258
265
  * stepper's `"step"`, a static `"page"`) is left in place, current or not.
259
266
  *
@@ -298,6 +305,11 @@ function setupLinkClaims(router, explicitLinks, links) {
298
305
  if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath)) return;
299
306
  return url;
300
307
  }
308
+
309
+ // every anchor matches against the same location, so its parse is shared
310
+ // until the location changes
311
+ let matched;
312
+ let match;
301
313
  function linkState(a) {
302
314
  // read reactive sources unconditionally so the owning effect stays
303
315
  // subscribed even while the anchor is not router-managed
@@ -305,11 +317,16 @@ function setupLinkClaims(router, explicitLinks, links) {
305
317
  const routing = plugin && plugin.track();
306
318
  const url = managedUrl(a);
307
319
  const target = url && url.pathname + url.search;
320
+ const key = location.pathname + location.search;
308
321
  // no per-anchor `end` opt-out like useLinkState has
322
+ if (key !== matched) {
323
+ matched = key;
324
+ match = linkMatcher(location, basePath);
325
+ }
309
326
  const {
310
327
  active,
311
328
  current
312
- } = matchLink(location, target, basePath);
329
+ } = match(target);
313
330
  const pending = !!routing && plugin.pending(target);
314
331
  return {
315
332
  active,
@@ -381,6 +398,12 @@ function setupLinkClaims(router, explicitLinks, links) {
381
398
  }));
382
399
  }
383
400
 
401
+ /**
402
+ * App bundlers define this (`"true"` when server components are on, including
403
+ * `'external'`, otherwise `"false"`). Not assigned here: the published build
404
+ * keeps the `typeof` check so they can fold the fallback away.
405
+ */
406
+
384
407
  /**
385
408
  * The submit delegation consults this slot instead of importing the action
386
409
  * module: the action side installs its handler on first action creation
@@ -392,24 +415,43 @@ let formHandler;
392
415
  function setRouterFormHandler(handler) {
393
416
  formHandler = handler;
394
417
  }
418
+
419
+ /**
420
+ * Link preload strategies reach route preloading through this slot instead
421
+ * of the event wiring importing it: a strategy installs it when it runs (see
422
+ * preload.ts), so an app with no strategy and no `usePreloadRoute` never
423
+ * ships route preloading.
424
+ */
425
+ let linkPreloader;
426
+ function setLinkPreloader(preloader) {
427
+ linkPreloader = preloader;
428
+ }
395
429
  function setupNativeEvents({
396
- preload = true,
430
+ preload,
397
431
  explicitLinks = false,
398
- actionBase = "/_server",
399
- transformUrl
432
+ actionBase = "/_server"
400
433
  } = {}) {
401
434
  return router => {
402
435
  const basePath = router.base.path();
403
436
  const navigateFromRoute = router.navigatorFactory(router.base);
404
- let preloadTimeout;
405
- let lastElement;
406
437
  function isSvg(el) {
407
438
  return el.namespaceURI === "http://www.w3.org/2000/svg";
408
439
  }
409
440
  function handleAnchor(evt) {
410
441
  if (evt.defaultPrevented || evt.button !== 0 || evt.metaKey || evt.altKey || evt.ctrlKey || evt.shiftKey) return;
442
+ return findAnchor(evt);
443
+ }
444
+
445
+ // no button or modifier gate: focus and touch events carry neither
446
+ function findAnchor(evt) {
411
447
  const a = evt.composedPath().find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
412
- if (!a || explicitLinks && !a.hasAttribute("link")) return;
448
+ const url = a && anchorUrl(a);
449
+ return url && [a, url];
450
+ }
451
+
452
+ /** The anchor's URL when the router manages it, else `undefined`. */
453
+ function anchorUrl(a) {
454
+ if (explicitLinks && !a.hasAttribute("link")) return;
413
455
  const svg = isSvg(a);
414
456
  const href = svg ? a.href.baseVal : a.href;
415
457
  const target = svg ? a.target.baseVal : a.target;
@@ -421,7 +463,7 @@ function setupNativeEvents({
421
463
  // inherit the page origin, so the origin check below won't reject them. #382
422
464
  if (url.protocol !== "https:" && url.protocol !== "http:") return;
423
465
  if (url.origin !== window.location.origin || !isUnderBase(url.pathname, basePath)) return;
424
- return [a, url];
466
+ return url;
425
467
  }
426
468
  function handleAnchorClick(evt) {
427
469
  const res = handleAnchor(evt);
@@ -439,25 +481,6 @@ function setupNativeEvents({
439
481
  state: state ? JSON.parse(state) : undefined
440
482
  }));
441
483
  }
442
- function handleAnchorPreload(evt) {
443
- const res = handleAnchor(evt);
444
- if (!res) return;
445
- const [a, url] = res;
446
- transformUrl && (url.pathname = transformUrl(url.pathname));
447
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
448
- }
449
- function handleAnchorMove(evt) {
450
- clearTimeout(preloadTimeout);
451
- const res = handleAnchor(evt);
452
- if (!res) return lastElement = null;
453
- const [a, url] = res;
454
- if (lastElement === a) return;
455
- transformUrl && (url.pathname = transformUrl(url.pathname));
456
- preloadTimeout = setTimeout(() => {
457
- router.preloadRoute(url, a.getAttribute("preload") !== "false");
458
- lastElement = a;
459
- }, 20);
460
- }
461
484
  function handleFormSubmit(evt) {
462
485
  if (formHandler) return formHandler(evt, router, actionBase);
463
486
  // No form handler means no action module in the client graph at all
@@ -473,37 +496,31 @@ function setupNativeEvents({
473
496
  const url = new URL(ref, document.baseURI);
474
497
  const path = router.parsePath(url.pathname + url.search);
475
498
  if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST") return;
476
- evt.preventDefault();
477
- const data = new FormData(form, evt.submitter);
478
- Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
499
+ // The import sits inside the check so bundlers drop the server-forms
500
+ // chunk. An early return above a still-present import() does not.
501
+ // Missing stays off: an undefined constant does not fold.
502
+ if (typeof __SOLID_SERVER_COMPONENTS__ !== "undefined" && __SOLID_SERVER_COMPONENTS__) {
503
+ evt.preventDefault();
504
+ const data = new FormData(form, evt.submitter);
505
+ Promise.resolve().then(function () { return serverForms; }).then(m => m.submitServerForm(router, path, form, data));
506
+ }
479
507
  }
480
508
  const handleSubmit = evt => dispatchAsInteraction(evt, () => handleFormSubmit(evt));
481
509
 
482
510
  // ensure delegated event run first
483
511
  delegateEvents(["click", "submit"]);
484
512
  document.addEventListener("click", handleAnchorClick);
485
- // preloads are not interactions: those listeners run outside any frame
486
- if (preload) {
487
- document.addEventListener("mousemove", handleAnchorMove, {
488
- passive: true
489
- });
490
- document.addEventListener("focusin", handleAnchorPreload, {
491
- passive: true
492
- });
493
- document.addEventListener("touchstart", handleAnchorPreload, {
494
- passive: true
495
- });
496
- }
497
513
  document.addEventListener("submit", handleSubmit);
498
514
  onCleanup(() => {
499
515
  document.removeEventListener("click", handleAnchorClick);
500
- if (preload) {
501
- document.removeEventListener("mousemove", handleAnchorMove);
502
- document.removeEventListener("focusin", handleAnchorPreload);
503
- document.removeEventListener("touchstart", handleAnchorPreload);
504
- }
505
516
  document.removeEventListener("submit", handleSubmit);
506
517
  });
518
+ // the pre-strategy boolean option is ignored (a dev warning names it)
519
+ if (preload && preload !== true) [].concat(preload).forEach(strategy => strategy({
520
+ anchor: findAnchor,
521
+ url: anchorUrl,
522
+ preload: (url, data) => linkPreloader(router, url, data)
523
+ }));
507
524
  };
508
525
  }
509
526
 
@@ -731,10 +748,10 @@ function pendingTarget(router) {
731
748
  /** Whether `to` is the destination of the in-flight navigation. */
732
749
  function linkPending(router, to, base, end) {
733
750
  const target = pendingTarget(router);
734
- return !!target && matchLink({
751
+ return !!target && linkMatcher({
735
752
  pathname: target.value,
736
753
  search: ""
737
- }, to, base, end).active;
754
+ }, base, end)(to).active;
738
755
  }
739
756
 
740
757
  /**
@@ -909,23 +926,6 @@ const useRouteMatches = () => {
909
926
  return () => router.matches().slice();
910
927
  };
911
928
 
912
- /**
913
- * `usePreloadRoute` returns a function for warming a route by hand — the same
914
- * work link hover/focus intent triggers automatically: the matched routes'
915
- * lazy components load, and with `preloadData` their `preload` functions run.
916
- *
917
- * @example
918
- * ```js
919
- * const preload = usePreloadRoute();
920
- *
921
- * preload(paths.users(2).settings, { preloadData: true });
922
- * ```
923
- */
924
- const usePreloadRoute = () => {
925
- const pre = useRouter().preloadRoute;
926
- return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
927
- };
928
-
929
929
  /**
930
930
  * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
931
931
  *
@@ -1034,7 +1034,7 @@ const useLinkState = (href, options = {}) => {
1034
1034
  const location = router.location;
1035
1035
  const to = useResolvedPath(() => String(href()));
1036
1036
  const base = router.base.path();
1037
- const state = createMemo(() => matchLink(location, to(), base, options.end));
1037
+ const state = createMemo(() => linkMatcher(location, base, options.end)(to()));
1038
1038
  return {
1039
1039
  active: createMemo(() => state().active),
1040
1040
  current: createMemo(() => state().current),
@@ -1373,6 +1373,11 @@ let preloadIntent;
1373
1373
  function getIntent() {
1374
1374
  return preloadIntent || useOptionalContext(RouterContextObj)?.intent?.();
1375
1375
  }
1376
+ function setPreloadIntent(intent) {
1377
+ const prev = preloadIntent;
1378
+ preloadIntent = intent;
1379
+ return prev;
1380
+ }
1376
1381
  let inPreloadFn = false;
1377
1382
  function getInPreloadFn() {
1378
1383
  return inPreloadFn;
@@ -1504,9 +1509,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1504
1509
  }
1505
1510
  return read();
1506
1511
  };
1512
+ const matchPath = pathname => getRouteMatches(branches(), options.transformUrl ? options.transformUrl(pathname) : pathname);
1507
1513
  const matches = createMemo(() => {
1508
- const pathname = typeof options.transformUrl === "function" ? options.transformUrl(location.pathname) : location.pathname;
1509
- const m = getRouteMatches(branches(), pathname);
1514
+ const m = matchPath(location.pathname);
1510
1515
  // An unresolved lazy subtree parks readers on not-ready semantics — the
1511
1516
  // navigation transition (or the SSR stream) holds until the table lands.
1512
1517
  // NotReadyError (not a returned promise) because a match chain is full
@@ -1562,12 +1567,13 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1562
1567
  intent: transitionIntent,
1563
1568
  _source: source,
1564
1569
  _owner: routerOwner,
1570
+ _routeOwner: getContext,
1571
+ _match: matchPath,
1565
1572
  renderPath,
1566
1573
  parsePath,
1567
1574
  navigatorFactory,
1568
1575
  matches,
1569
1576
  beforeLeave,
1570
- preloadRoute,
1571
1577
  singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
1572
1578
  get submissions() {
1573
1579
  return submissions ||= createSignal(isServer ? initSubmissions() : [], {
@@ -1697,71 +1703,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1697
1703
  route = route || useOptionalContext(RouteContextObj) || baseRoute;
1698
1704
  return (to, options) => navigateFromRoute(route, to, options);
1699
1705
  }
1700
- function preloadRoute(url, preloadData) {
1701
- const next = getRouteMatches(branches(), url.pathname);
1702
- // An unresolved lazy subtree in the chain: the placeholder's
1703
- // component.preload (below) kicks the table load; once it lands,
1704
- // preload again so the real inner routes warm too. Preloads are
1705
- // speculative: a failed load (held sync throw or rejection) is ignored
1706
- // here — the real navigation surfaces and retries it.
1707
- const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
1708
- if (boundary) {
1709
- try {
1710
- resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
1711
- } catch {}
1712
- }
1713
- // Data preloads run only for levels a navigation would mount fresh or
1714
- // reuse with changed inputs: this level's params, and search as the
1715
- // declared schema's output or else the raw string. Navigation itself is
1716
- // already this selective (a matching level is reused and re-reads through
1717
- // tracked params), so an unchanged level has nothing new to warm.
1718
- let current;
1719
- try {
1720
- current = untrack(matches);
1721
- } catch {}
1722
- const query = extractSearchParams(url);
1723
- const inputs = (p, q, s, r) => {
1724
- const a = serverRouteArgs(r, p, q);
1725
- a.search === undefined && (a.search = s);
1726
- return a;
1727
- };
1728
- const prevIntent = preloadIntent;
1729
- preloadIntent = "preload";
1730
- for (let match in next) {
1731
- const {
1732
- route,
1733
- params
1734
- } = next[match];
1735
- const {
1736
- preload,
1737
- component
1738
- } = route;
1739
- component?.preload?.();
1740
- const now = current && current[match];
1741
- const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
1742
- inPreloadFn = true;
1743
- preloadData && !unchanged && runWithOwner(getContext(), () => {
1744
- // A server component route's data IS its call: warm the same
1745
- // query entry the render will read, under the same derived args.
1746
- const server = serverRouteOf(component);
1747
- server && server.call(serverRouteArgs(route, params, query));
1748
- preload && preload({
1749
- params,
1750
- location: {
1751
- pathname: url.pathname,
1752
- search: url.search,
1753
- hash: url.hash,
1754
- query,
1755
- state: null,
1756
- key: ""
1757
- },
1758
- intent: "preload"
1759
- });
1760
- });
1761
- inPreloadFn = false;
1762
- }
1763
- preloadIntent = prevIntent;
1764
- }
1765
1706
 
1766
1707
  // Seeds the initial submission from a no-JS form post: the server
1767
1708
  // function runtime redirected back with the outcome in a one-shot flash
@@ -2595,6 +2536,9 @@ function staticIntegration(url, utils) {
2595
2536
  };
2596
2537
  }
2597
2538
  function createRouter(config) {
2539
+ if (DEV && typeof config.preloadLinks === "boolean") {
2540
+ console.warn("`preloadLinks` takes preload strategies, not a boolean, and preloads nothing by default. " + "Use `preloadLinks: intentPreload()` for hover/focus preloading.");
2541
+ }
2598
2542
  const basePath = config.base || "";
2599
2543
  // Routes are immutable per instance, so compilation is shared by every
2600
2544
  // mount, request, and `match()` call — recompiled only when a lazy subtree
@@ -2644,8 +2588,7 @@ function createRouter(config) {
2644
2588
  setupNativeEvents({
2645
2589
  preload: config.preloadLinks,
2646
2590
  explicitLinks: config.explicitLinks,
2647
- actionBase: config.actionBase,
2648
- transformUrl: config.transformUrl
2591
+ actionBase: config.actionBase
2649
2592
  })(routerState);
2650
2593
  setupLinkClaims(routerState, config.explicitLinks, config.links);
2651
2594
  if (routerState.singleFlight) onCleanup(registerFlightRouter(routerState));
@@ -2791,6 +2734,289 @@ const useBeforeLeave = listener => {
2791
2734
  onCleanup(s);
2792
2735
  };
2793
2736
 
2737
+ /**
2738
+ * Warms the routes `url` matches: their lazy components (and any lazy
2739
+ * subtree on the way) load, and with `preloadData` their `preload` functions
2740
+ * run under `intent: "preload"`. Only link preload strategies and
2741
+ * `usePreloadRoute` import it, so apps using neither don't ship it.
2742
+ */
2743
+ function preloadRoute(router, url, preloadData) {
2744
+ const next = router._match(url.pathname);
2745
+ // An unresolved lazy subtree in the chain: the placeholder's
2746
+ // component.preload (below) kicks the table load; once it lands,
2747
+ // preload again so the real inner routes warm too. Preloads are
2748
+ // speculative: a failed load (held sync throw or rejection) is ignored
2749
+ // here — the real navigation surfaces and retries it.
2750
+ const boundary = next.find(m => m.route.lazy && !m.route.lazy.resolved);
2751
+ if (boundary) {
2752
+ try {
2753
+ resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(router, url, preloadData), () => {});
2754
+ } catch {}
2755
+ }
2756
+ // Data preloads run only for levels a navigation would mount fresh or
2757
+ // reuse with changed inputs: this level's params, and search as the
2758
+ // declared schema's output or else the raw string. Navigation itself is
2759
+ // already this selective (a matching level is reused and re-reads through
2760
+ // tracked params), so an unchanged level has nothing new to warm.
2761
+ let current;
2762
+ try {
2763
+ current = untrack(router.matches);
2764
+ } catch {}
2765
+ const {
2766
+ location
2767
+ } = router;
2768
+ const query = extractSearchParams(url);
2769
+ const inputs = (p, q, s, r) => {
2770
+ const a = serverRouteArgs(r, p, q);
2771
+ a.search === undefined && (a.search = s);
2772
+ return a;
2773
+ };
2774
+ const prevIntent = setPreloadIntent("preload");
2775
+ for (let match in next) {
2776
+ const {
2777
+ route,
2778
+ params
2779
+ } = next[match];
2780
+ const {
2781
+ preload,
2782
+ component
2783
+ } = route;
2784
+ component?.preload?.();
2785
+ const now = current && current[match];
2786
+ const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
2787
+ setInPreloadFn(true);
2788
+ preloadData && !unchanged && runWithOwner(router._routeOwner(), () => {
2789
+ // A server component route's data IS its call: warm the same
2790
+ // query entry the render will read, under the same derived args.
2791
+ const server = serverRouteOf(component);
2792
+ server && server.call(serverRouteArgs(route, params, query));
2793
+ preload && preload({
2794
+ params,
2795
+ location: {
2796
+ pathname: url.pathname,
2797
+ search: url.search,
2798
+ hash: url.hash,
2799
+ query,
2800
+ state: null,
2801
+ key: ""
2802
+ },
2803
+ intent: "preload"
2804
+ });
2805
+ });
2806
+ setInPreloadFn(false);
2807
+ }
2808
+ setPreloadIntent(prevIntent);
2809
+ }
2810
+
2811
+ /**
2812
+ * `usePreloadRoute` returns a function for warming a route by hand — the same
2813
+ * work link preloading triggers: the matched routes' lazy components load,
2814
+ * and with `preloadData` their `preload` functions run.
2815
+ *
2816
+ * @example
2817
+ * ```js
2818
+ * const preload = usePreloadRoute();
2819
+ *
2820
+ * preload(paths.users(2).settings, { preloadData: true });
2821
+ * ```
2822
+ */
2823
+ const usePreloadRoute = () => {
2824
+ const router = useRouter();
2825
+ return (url, options) => preloadRoute(router, url instanceof URL ? url : new URL(String(url), mockBase), options?.preloadData);
2826
+ };
2827
+
2828
+ /**
2829
+ * Anchors as the runtime claims them: at creation and again on `href`
2830
+ * writes. `disarm` runs when the creating owner is disposed. Claims fire
2831
+ * before dynamic attributes are applied (solidjs/solid#3923), so whether a
2832
+ * link is opted in, and where it points, is read when it preloads.
2833
+ */
2834
+ function claimAnchors(arm, disarm) {
2835
+ const seen = new WeakSet();
2836
+ setLinkPreloader(preloadRoute);
2837
+ onCleanup(registerElementClaim(node => {
2838
+ if (node.nodeName.toUpperCase() !== "A") return;
2839
+ arm(node);
2840
+ // a re-claim runs under the owner writing `href`, not the anchor's
2841
+ if (seen.has(node)) return;
2842
+ seen.add(node);
2843
+ getOwner() && onCleanup(() => disarm(node));
2844
+ }));
2845
+ }
2846
+
2847
+ // requests made before the browser is idle run together once it is
2848
+ function whenIdle(fn) {
2849
+ let scheduled = false;
2850
+ return () => {
2851
+ if (scheduled) return;
2852
+ scheduled = true;
2853
+ (window.requestIdleCallback || setTimeout)(() => {
2854
+ scheduled = false;
2855
+ fn();
2856
+ });
2857
+ };
2858
+ }
2859
+
2860
+ /**
2861
+ * Preloads an ambient strategy's anchor: one that names the strategy in its
2862
+ * `preload` attribute, or with `all` any but `preload="false"`. Skipped when
2863
+ * the user asked to save data or the connection is 2g (where the Network
2864
+ * Information API is available).
2865
+ */
2866
+ const ambientPreload = ({
2867
+ url,
2868
+ preload
2869
+ }, name, all, data) => a => {
2870
+ const value = a.getAttribute("preload");
2871
+ const connection = navigator.connection;
2872
+ const href = (value === name || all && value !== "false") && !(connection && (connection.saveData || /2g/.test(connection.effectiveType))) && url(a);
2873
+ href && preload(href, data);
2874
+ };
2875
+
2876
+ /**
2877
+ * Preload a link when the pointer rests on it for `delay` ms (default 20),
2878
+ * when it takes focus, and on touchstart. Moving over a link that already
2879
+ * preloaded does not preload it again until the pointer leaves. These
2880
+ * listeners do not check modifier keys or `defaultPrevented`; clicks still do.
2881
+ * Warms route code and, unless `data` is false (default true), runs the
2882
+ * matched routes' `preload` functions.
2883
+ *
2884
+ * @example
2885
+ * ```ts
2886
+ * const Router = createRouter({ routes, preloadLinks: intentPreload() });
2887
+ * ```
2888
+ */
2889
+ const intentPreload = ({
2890
+ delay = 20,
2891
+ data = true
2892
+ } = {}) => ctx => {
2893
+ let timeout;
2894
+ let lastElement;
2895
+ setLinkPreloader(preloadRoute);
2896
+ const onIntent = evt => {
2897
+ // clear before resolving: a throw must not leave the rest timer armed
2898
+ if (evt.type === "mousemove") clearTimeout(timeout);
2899
+ const res = ctx.anchor(evt);
2900
+ const link = res && res[0].getAttribute("preload") !== "false" ? res : undefined;
2901
+ if (evt.type !== "mousemove") return link && ctx.preload(link[1], data);
2902
+ if (!link) return lastElement = undefined;
2903
+ const [a, url] = link;
2904
+ if (lastElement === a) return;
2905
+ timeout = setTimeout(() => {
2906
+ ctx.preload(url, data);
2907
+ lastElement = a;
2908
+ }, delay);
2909
+ };
2910
+ const passive = {
2911
+ passive: true
2912
+ };
2913
+ document.addEventListener("focusin", onIntent, passive);
2914
+ document.addEventListener("touchstart", onIntent, passive);
2915
+ document.addEventListener("mousemove", onIntent, passive);
2916
+ onCleanup(() => {
2917
+ clearTimeout(timeout);
2918
+ document.removeEventListener("focusin", onIntent);
2919
+ document.removeEventListener("touchstart", onIntent);
2920
+ document.removeEventListener("mousemove", onIntent);
2921
+ });
2922
+ };
2923
+
2924
+ /**
2925
+ * Preload a link on `pointerdown` (mouse, touch, or pen), ahead of its click.
2926
+ * Does not check modifier keys or `defaultPrevented`. Warms route code and,
2927
+ * unless `data` is false (default true), route data.
2928
+ */
2929
+ const tapPreload = ({
2930
+ data = true
2931
+ } = {}) => ctx => {
2932
+ setLinkPreloader(preloadRoute);
2933
+ const onTap = evt => {
2934
+ const res = ctx.anchor(evt);
2935
+ res && res[0].getAttribute("preload") !== "false" && ctx.preload(res[1], data);
2936
+ };
2937
+ document.addEventListener("pointerdown", onTap, {
2938
+ passive: true
2939
+ });
2940
+ onCleanup(() => document.removeEventListener("pointerdown", onTap));
2941
+ };
2942
+
2943
+ /**
2944
+ * Preload a link once it has stayed in the viewport for `delay` ms (default
2945
+ * 100), when the browser is next idle (`requestIdleCallback`, or `setTimeout`
2946
+ * where that is missing). Each link preloads once, and again after its `href`
2947
+ * changes; a link that leaves before then is dropped. One `IntersectionObserver`
2948
+ * is shared. `rootMargin` is passed through to it (the observer's own `"0px"`
2949
+ * when omitted). Applies to links with `preload="viewport"`, or with `all`
2950
+ * (default false) to every link except `preload="false"`. Skips Save-Data and
2951
+ * 2g connections. Warms route code only unless `data` is true (default false).
2952
+ */
2953
+ const viewportPreload = ({
2954
+ all,
2955
+ data = false,
2956
+ delay = 100,
2957
+ rootMargin
2958
+ } = {}) => ctx => {
2959
+ if (typeof IntersectionObserver === "undefined") return;
2960
+ const warm = ambientPreload(ctx, "viewport", all, data);
2961
+ // visible anchors: a pending dwell timer, or `true` once due
2962
+ const visible = new Map();
2963
+ let observer;
2964
+ const leave = a => {
2965
+ clearTimeout(visible.get(a));
2966
+ visible.delete(a);
2967
+ };
2968
+ const flush = whenIdle(() => visible.forEach((due, a) => {
2969
+ if (due !== true) return;
2970
+ observer.unobserve(a);
2971
+ visible.delete(a);
2972
+ warm(a);
2973
+ }));
2974
+ const intersect = entries => entries.forEach(({
2975
+ target: a,
2976
+ isIntersecting
2977
+ }) => isIntersecting ? visible.set(a, setTimeout(() => (visible.set(a, true), flush()), delay)) : leave(a));
2978
+ claimAnchors(a => (observer ||= new IntersectionObserver(intersect, {
2979
+ rootMargin
2980
+ })).observe(a), a => {
2981
+ observer.unobserve(a);
2982
+ leave(a);
2983
+ });
2984
+ onCleanup(() => {
2985
+ observer && observer.disconnect();
2986
+ visible.forEach((_, a) => leave(a));
2987
+ });
2988
+ };
2989
+
2990
+ /**
2991
+ * Preload links as soon as the page has loaded and the browser is idle,
2992
+ * including links mounted later. A link removed before the flush is dropped.
2993
+ * Applies to links with `preload="eager"`, or with `all` (default false) to
2994
+ * every link except `preload="false"`. Skips Save-Data and 2g connections.
2995
+ * Warms route code only unless `data` is true (default false).
2996
+ */
2997
+ const eagerPreload = ({
2998
+ all,
2999
+ data = false
3000
+ } = {}) => ctx => {
3001
+ const warm = ambientPreload(ctx, "eager", all, data);
3002
+ const queue = new Set();
3003
+ const flush = whenIdle(() => {
3004
+ queue.forEach(warm);
3005
+ queue.clear();
3006
+ });
3007
+ addEventListener("load", flush, {
3008
+ once: true
3009
+ });
3010
+ claimAnchors(a => {
3011
+ queue.add(a);
3012
+ document.readyState === "complete" && flush();
3013
+ }, a => queue.delete(a));
3014
+ onCleanup(() => {
3015
+ removeEventListener("load", flush);
3016
+ queue.clear();
3017
+ });
3018
+ };
3019
+
2794
3020
  // Server component routes (experimental — rides the experimental server
2795
3021
  // components surface in @solidjs/web; the arg shape may change).
2796
3022
  //
@@ -4220,4 +4446,4 @@ var serverForms = /*#__PURE__*/Object.freeze({
4220
4446
  submitServerForm: submitServerForm
4221
4447
  });
4222
4448
 
4223
- export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, hashHistory, int, liveQuery, memoryHistory, pendingLinks, query, revalidate, serverRouteComponent, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions };
4449
+ export { RouterContextObj as RouterContext, mergeSearchString as _mergeSearchString, action, browserHistory, createBeforeLeave, createRouter, defineRoute, defineRoutes, eagerPreload, hashHistory, int, intentPreload, liveQuery, memoryHistory, pendingLinks, query, revalidate, serverRouteComponent, tapPreload, useAction, useBeforeLeave, useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useRouteMatches, useSearchParams, useSubmissions, viewportPreload };