@orieljs/live 0.1.0-alpha.4 → 0.1.0-alpha.5

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
@@ -219,9 +219,25 @@ import { inject as inject3 } from "@orieljs/container";
219
219
  // Testable.ts
220
220
  import { AssertionError } from "assert";
221
221
  import { isDeepStrictEqual } from "util";
222
- import { LiveManager as LiveManager2, token } from "@orieljs/contracts";
222
+ import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
223
223
  import { Container, currentContainer, inject as inject2 } from "@orieljs/container";
224
224
 
225
+ // flash.ts
226
+ function trackFlash(session, { saved = true } = {}) {
227
+ if (session === void 0) return () => void 0;
228
+ const before = new Set(session.get("_flash.new", []));
229
+ return (redirected) => {
230
+ if (redirected) return;
231
+ const flashed = session.get("_flash.new", []);
232
+ const fresh = flashed.filter((key) => !before.has(key));
233
+ if (fresh.length === 0) return;
234
+ session.put("_flash.new", flashed.filter((key) => before.has(key)));
235
+ if (!saved) return session.forget(...fresh);
236
+ const old = session.get("_flash.old", []);
237
+ session.put("_flash.old", [...old, ...fresh.filter((key) => !old.includes(key))]);
238
+ };
239
+ }
240
+
225
241
  // errors.ts
226
242
  var LiveError = class extends Error {
227
243
  name = "LiveError";
@@ -327,6 +343,15 @@ function requestScope() {
327
343
  const container = currentContainer() ?? Container.getInstance();
328
344
  return container?.bound(TestRequestScope) === true ? container.make(TestRequestScope) : (run) => run();
329
345
  }
346
+ function currentSession() {
347
+ const container = currentContainer();
348
+ if (container?.bound(Session) !== true) return void 0;
349
+ try {
350
+ return container.make(Session);
351
+ } catch {
352
+ return void 0;
353
+ }
354
+ }
330
355
  var Testable = class _Testable {
331
356
  constructor(manager, scope, result) {
332
357
  this.manager = manager;
@@ -381,7 +406,12 @@ var Testable = class _Testable {
381
406
  return this.#update({}, [{ method: "$refresh", params: [] }]);
382
407
  }
383
408
  async #update(updates, calls) {
384
- const result = await this.scope(() => this.manager.updateComponent(this.#snapshot, updates, calls));
409
+ const result = await this.scope(async () => {
410
+ const flashed = trackFlash(currentSession(), { saved: false });
411
+ const updated = await this.manager.updateComponent(this.#snapshot, updates, calls);
412
+ flashed(updated.effects.redirect !== void 0);
413
+ return updated;
414
+ });
385
415
  this.#snapshot = JSON.stringify(result.snapshot);
386
416
  if (result.html !== void 0) this.#html = result.html;
387
417
  this.#effects = result.effects;
@@ -477,16 +507,17 @@ var Live = Object.assign(LiveComponent, {
477
507
  });
478
508
 
479
509
  // LiveComponentManager.ts
480
- import { AsyncLocalStorage } from "async_hooks";
510
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
481
511
  import { randomBytes } from "crypto";
482
512
  import {
483
513
  ClassRegistry,
484
514
  ConfigRepository,
485
- CurrentRoute,
515
+ CurrentRoute as CurrentRoute2,
486
516
  Encrypter,
487
517
  HttpRequest,
488
518
  Redirector,
489
519
  Router,
520
+ Session as Session2,
490
521
  ViewFactory
491
522
  } from "@orieljs/contracts";
492
523
  import { currentContainer as currentContainer2 } from "@orieljs/container";
@@ -541,10 +572,12 @@ async function discover(directory) {
541
572
  }
542
573
 
543
574
  // persistent.ts
544
- var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest"];
575
+ import { CurrentRoute } from "@orieljs/contracts";
576
+ var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest", "can"];
577
+ var ROUTER_METHODS = ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups", "substituteBindings"];
545
578
  function canMatch(router) {
546
579
  const methods = router;
547
- return ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups"].every((name) => typeof methods[name] === "function");
580
+ return ROUTER_METHODS.every((name) => typeof methods[name] === "function");
548
581
  }
549
582
  function isMatchingRoute(route) {
550
583
  const candidate = route;
@@ -573,7 +606,7 @@ function expand(references, aliases, groups, out = [], visiting = /* @__PURE__ *
573
606
  }
574
607
  return out;
575
608
  }
576
- function persistentMiddleware(router, persistent, method, path) {
609
+ function persistentPass(router, persistent, method, path) {
577
610
  if (!canMatch(router)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's router.");
578
611
  const routes = router.getRoutesByMatchOrder();
579
612
  if (!Array.isArray(routes) || !routes.every(isMatchingRoute)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's routes.");
@@ -581,36 +614,80 @@ function persistentMiddleware(router, persistent, method, path) {
581
614
  const target = routePath(path);
582
615
  const patterns = router.getPatterns();
583
616
  const version = router.getPatternVersion();
584
- const route = routes.find((candidate) => candidate.methods.includes(verb) && candidate.match(target, patterns, version) !== void 0);
585
- if (route === void 0) return [];
617
+ let matched;
618
+ for (const candidate of routes) {
619
+ const values2 = candidate.methods.includes(verb) ? candidate.match(target, patterns, version) : void 0;
620
+ if (values2 !== void 0) {
621
+ matched = { route: candidate, values: values2 };
622
+ break;
623
+ }
624
+ }
625
+ if (matched === void 0) return void 0;
626
+ const { route, values } = matched;
586
627
  const aliases = router.getMiddleware();
587
628
  const groups = router.getMiddlewareGroups();
588
629
  const wanted = new Set(persistent.flatMap((name) => Object.hasOwn(aliases, name) ? [aliases[name]] : []));
589
630
  const excluded = new Set(expand(route.excludedMiddleware(), aliases, groups).map((entry) => entry.cls));
590
631
  const ids = /* @__PURE__ */ new Map();
591
632
  const seen = /* @__PURE__ */ new Set();
592
- return expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
633
+ const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
593
634
  if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
594
635
  const key = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
595
636
  if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key)) return false;
596
637
  seen.add(key);
597
638
  return true;
598
639
  });
640
+ const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
641
+ for (const name of ["auth", "guest"]) if (Object.hasOwn(aliases, name)) early.add(aliases[name]);
642
+ const before = middleware.filter((entry) => early.has(entry.cls));
643
+ const after = middleware.filter((entry) => !early.has(entry.cls));
644
+ return { router, route, values, middleware: [...before, ...after], at: before.length };
645
+ }
646
+ function persistentMiddleware(router, persistent, method, path) {
647
+ return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
648
+ }
649
+ function currentRoute(container) {
650
+ if (!container.bound(CurrentRoute)) return void 0;
651
+ try {
652
+ return container.make(CurrentRoute);
653
+ } catch {
654
+ return void 0;
655
+ }
599
656
  }
600
- async function throughMiddleware(container, request, middleware, destination) {
657
+ async function throughMiddleware(container, request, pass, destination) {
658
+ const original = currentRoute(container);
659
+ const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
660
+ let swapped = false;
661
+ const show = () => {
662
+ container.instance(CurrentRoute, page);
663
+ swapped = true;
664
+ };
665
+ const restore = () => {
666
+ if (!swapped) return;
667
+ if (original === void 0) container.forgetInstance(CurrentRoute);
668
+ else container.instance(CurrentRoute, original);
669
+ swapped = false;
670
+ };
601
671
  let ran;
602
672
  const run = async (index) => {
603
- if (index === middleware.length) {
673
+ if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container));
674
+ if (index === pass.middleware.length) {
675
+ restore();
604
676
  ran = { result: await destination() };
605
677
  return new Response(null, { status: 204 });
606
678
  }
607
- const { cls, parameters } = middleware[index];
679
+ const { cls, parameters } = pass.middleware[index];
608
680
  const instance = container.make(cls);
609
681
  const next = () => run(index + 1);
610
682
  return instance.handle(request, next, ...parameters);
611
683
  };
612
- const response = await run(0);
613
- return ran ? ran.result : response;
684
+ show();
685
+ try {
686
+ const response = await run(0);
687
+ return ran ? ran.result : response;
688
+ } finally {
689
+ restore();
690
+ }
614
691
  }
615
692
 
616
693
  // security.ts
@@ -759,9 +836,68 @@ function isUpdatable(instance, path) {
759
836
  return !isModel(node) && !(node instanceof Collection2);
760
837
  }
761
838
 
839
+ // WithPagination.ts
840
+ import { AsyncLocalStorage } from "async_hooks";
841
+ var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");
842
+ function toPage(value) {
843
+ const page = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
844
+ return Number.isFinite(page) && page >= 1 ? Math.min(Math.floor(page), Number.MAX_SAFE_INTEGER) : 1;
845
+ }
846
+ async function hook(instance, name, ...args) {
847
+ const method = instance[name];
848
+ if (typeof method === "function") await method.apply(instance, args);
849
+ }
850
+ function WithPagination(Base) {
851
+ class WithPaginationMixin extends Base {
852
+ page = 1;
853
+ nextPage() {
854
+ return this.setPage(toPage(this.page) + 1);
855
+ }
856
+ previousPage() {
857
+ return this.setPage(Math.max(toPage(this.page) - 1, 1));
858
+ }
859
+ gotoPage(page) {
860
+ return this.setPage(page);
861
+ }
862
+ resetPage() {
863
+ return this.setPage(1);
864
+ }
865
+ async setPage(page) {
866
+ const next = toPage(page);
867
+ await hook(this, "updatingPage", next);
868
+ this.page = next;
869
+ await hook(this, "updatedPage", next);
870
+ }
871
+ }
872
+ Object.defineProperty(WithPaginationMixin.prototype, PAGINATED, { value: true });
873
+ return WithPaginationMixin;
874
+ }
875
+ function isPaginated(component) {
876
+ return component[PAGINATED] === true;
877
+ }
878
+ var rendering = new AsyncLocalStorage();
879
+ function renderPaginated(component, url, run) {
880
+ return rendering.run(isPaginated(component) ? { component, url } : void 0, run);
881
+ }
882
+ function outsidePagination(run) {
883
+ return rendering.run(void 0, run);
884
+ }
885
+ var livePaginationState = {
886
+ currentPage(pageName) {
887
+ const current = rendering.getStore();
888
+ if (current === void 0 || pageName !== "page") return void 0;
889
+ const page = toPage(current.component.page);
890
+ current.component.page = page;
891
+ return page;
892
+ },
893
+ currentPath() {
894
+ return rendering.getStore()?.url;
895
+ }
896
+ };
897
+
762
898
  // LiveComponentManager.ts
763
- var renderContext = new AsyncLocalStorage();
764
- var pageRequest = new AsyncLocalStorage();
899
+ var renderContext = new AsyncLocalStorage2();
900
+ var pageRequest = new AsyncLocalStorage2();
765
901
  function optional(container, token2) {
766
902
  if (!container.bound(token2)) return void 0;
767
903
  try {
@@ -774,7 +910,7 @@ var isRecord = (value) => typeof value === "object" && value !== null && !Array.
774
910
  function setOwn(object, key, value) {
775
911
  Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
776
912
  }
777
- async function hook(instance, name, ...args) {
913
+ async function hook2(instance, name, ...args) {
778
914
  const method = instance[name];
779
915
  if (typeof method === "function") await method.apply(instance, args);
780
916
  }
@@ -825,7 +961,7 @@ var LiveComponentManager = class {
825
961
  setOwn(context.children, childKey, existing);
826
962
  return `<${existing[0]} o:id="${escapeAttribute(existing[1])}"></${existing[0]}>`;
827
963
  }
828
- const result = await this.mountComponent(component, params);
964
+ const result = await outsidePagination(() => this.mountComponent(component, params));
829
965
  setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
830
966
  return result.html;
831
967
  }
@@ -837,14 +973,14 @@ var LiveComponentManager = class {
837
973
  const state = stateOf(instance);
838
974
  state.id = randomBytes(15).toString("base64url");
839
975
  state.name = name;
840
- await hook(instance, "boot");
976
+ await hook2(instance, "boot");
841
977
  for (const [property, value] of Object.entries(params)) {
842
978
  if (Object.hasOwn(instance, property)) instance[property] = value;
843
979
  }
844
980
  this.prepareComputed(instance);
845
- await hook(instance, "mount", params);
846
981
  const request = optional(this.container(), HttpRequest);
847
982
  const page = pageRequest.getStore() ?? { path: request?.path() ?? "", method: request?.method() ?? "GET" };
983
+ await renderPaginated(instance, this.pageUrl(page.path), () => hook2(instance, "mount", params));
848
984
  const memo = { id: state.id, name, path: page.path, method: page.method, children: {}, errors: {} };
849
985
  const { html, tag } = await this.render(instance, memo, {});
850
986
  const effects = this.effects(instance);
@@ -872,22 +1008,24 @@ var LiveComponentManager = class {
872
1008
  state.id = snapshot.memo.id;
873
1009
  state.name = snapshot.memo.name;
874
1010
  state.errors = { ...snapshot.memo.errors };
875
- await hook(instance, "boot");
1011
+ await hook2(instance, "boot");
876
1012
  const registry = this.registry();
877
1013
  for (const [property, value] of Object.entries(snapshot.data)) {
878
1014
  Object.defineProperty(instance, property, { value: await hydrate(value, registry), enumerable: true, writable: true, configurable: true });
879
1015
  }
880
1016
  this.prepareComputed(instance);
881
- await hook(instance, "hydrate");
882
1017
  const returns = [];
883
- try {
884
- for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
885
- for (const call of calls) returns.push(await this.call(instance, call.method, call.params));
886
- } catch (error) {
887
- const errors = error.errors;
888
- if (error.kind !== "Validation" || !isRecord(errors)) throw error;
889
- state.errors = { ...errors };
890
- }
1018
+ await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
1019
+ await hook2(instance, "hydrate");
1020
+ try {
1021
+ for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
1022
+ for (const call of calls) returns.push(await this.call(instance, call.method, call.params));
1023
+ } catch (error) {
1024
+ const errors = error.errors;
1025
+ if (error.kind !== "Validation" || !isRecord(errors)) throw error;
1026
+ state.errors = { ...errors };
1027
+ }
1028
+ });
891
1029
  const memo = { ...snapshot.memo, children: { ...snapshot.memo.children } };
892
1030
  const effects = this.effects(instance);
893
1031
  let html;
@@ -902,31 +1040,36 @@ var LiveComponentManager = class {
902
1040
  return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
903
1041
  }
904
1042
  /**
905
- * The route action of POST /oriel/live/update. Each component's update runs behind the auth middleware of the route
906
- * that rendered its page (Livewire's persistent middleware): a signed-out tab gets a 401, never the action. When a
907
- * middleware answers instead (a redirect), that is the response.
1043
+ * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
1044
+ * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
1045
+ * bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
1046
+ * hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
1047
+ * update shows in this response only, unless the update redirects.
908
1048
  */
909
1049
  async handle(request) {
910
1050
  if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
911
1051
  const container = this.container();
912
1052
  const persistent = optional(container, ConfigRepository)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
913
1053
  const components = [];
1054
+ const flashed = trackFlash(optional(container, Session2));
914
1055
  for (const component of this.payload(request).components) {
915
1056
  const { memo } = this.verify(component.snapshot);
916
- const middleware = persistentMiddleware(container.make(Router), persistent, memo.method, memo.path);
1057
+ const pass = persistentPass(container.make(Router), persistent, memo.method, memo.path);
1058
+ const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
917
1059
  const result = await pageRequest.run(
918
1060
  { path: memo.path, method: memo.method },
919
- () => throughMiddleware(container, request, middleware, () => this.updateComponent(component.snapshot, component.updates, component.calls))
1061
+ () => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container, request, pass, update)
920
1062
  );
921
1063
  if (result instanceof Response) return result;
922
1064
  components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
923
1065
  }
1066
+ flashed(components.some((component) => component.effects.redirect !== void 0));
924
1067
  return { components };
925
1068
  }
926
1069
  /** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
927
1070
  async page(component) {
928
1071
  const container = this.container();
929
- const params = { ...optional(container, CurrentRoute)?.parameters ?? {} };
1072
+ const params = { ...optional(container, CurrentRoute2)?.parameters ?? {} };
930
1073
  const cls = component.constructor;
931
1074
  const result = await this.mountComponent(cls, params, component);
932
1075
  const redirect = result.effects.redirect;
@@ -995,8 +1138,8 @@ var LiveComponentManager = class {
995
1138
  const [root, ...rest] = path.split(".");
996
1139
  const key = rest.length > 0 ? rest.join(".") : void 0;
997
1140
  const studly = Str2.studly(root);
998
- await hook(instance, "updating", path, value);
999
- await hook(instance, `updating${studly}`, value, key);
1141
+ await hook2(instance, "updating", path, value);
1142
+ await hook2(instance, `updating${studly}`, value, key);
1000
1143
  const target = instance;
1001
1144
  if (rest.length === 0) target[root] = value;
1002
1145
  else {
@@ -1014,8 +1157,8 @@ var LiveComponentManager = class {
1014
1157
  assertSlot(node, last);
1015
1158
  Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
1016
1159
  }
1017
- await hook(instance, "updated", path, value);
1018
- await hook(instance, `updated${studly}`, value, key);
1160
+ await hook2(instance, "updated", path, value);
1161
+ await hook2(instance, `updated${studly}`, value, key);
1019
1162
  }
1020
1163
  /** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
1021
1164
  async call(instance, method, params) {
@@ -1043,18 +1186,35 @@ var LiveComponentManager = class {
1043
1186
  if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
1044
1187
  return instance[method](...params);
1045
1188
  }
1046
- /** Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. */
1189
+ /**
1190
+ * Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
1191
+ * WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
1192
+ * its update's hooks and actions.
1193
+ */
1047
1194
  async render(instance, memo, previous) {
1048
- await hook(instance, "rendering");
1195
+ await hook2(instance, "rendering");
1049
1196
  const context = { previous, children: {}, index: 0 };
1050
- const html = await renderContext.run(context, async () => {
1051
- const output = await instance.render();
1052
- return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
1053
- });
1054
- await hook(instance, "rendered", html);
1197
+ const html = await renderContext.run(
1198
+ context,
1199
+ () => renderPaginated(instance, this.pageUrl(memo.path), async () => {
1200
+ const output = await instance.render();
1201
+ return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
1202
+ })
1203
+ );
1204
+ await hook2(instance, "rendered", html);
1055
1205
  memo.children = context.children;
1056
1206
  return { html: html.trim(), tag: rootTag(html, memo.name) };
1057
1207
  }
1208
+ /** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
1209
+ pageUrl(path) {
1210
+ const relative3 = `/${path.replace(/^\/+/, "")}`;
1211
+ const url = optional(this.container(), HttpRequest)?.url();
1212
+ try {
1213
+ return url === void 0 ? relative3 : new URL(relative3, url).href;
1214
+ } catch {
1215
+ return relative3;
1216
+ }
1217
+ }
1058
1218
  /** Dispatches, redirects: the effects every response carries. */
1059
1219
  effects(instance) {
1060
1220
  const state = stateOf(instance);
@@ -1073,7 +1233,7 @@ var LiveComponentManager = class {
1073
1233
  }
1074
1234
  /** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
1075
1235
  async snapshot(instance, memo) {
1076
- await hook(instance, "dehydrate");
1236
+ await hook2(instance, "dehydrate");
1077
1237
  const registry = this.registry();
1078
1238
  const data = {};
1079
1239
  for (const [property, value] of Object.entries(instance)) data[property] = dehydrate(value, registry, `${memo.name}.${property}`);
@@ -1083,7 +1243,7 @@ var LiveComponentManager = class {
1083
1243
  };
1084
1244
 
1085
1245
  // LiveServiceProvider.ts
1086
- import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, Router as Router2 } from "@orieljs/contracts";
1246
+ import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
1087
1247
  import { inject as inject4 } from "@orieljs/container";
1088
1248
  import { ServiceProvider } from "@orieljs/support";
1089
1249
 
@@ -1160,6 +1320,7 @@ var LiveServiceProvider = class extends ServiceProvider {
1160
1320
  this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
1161
1321
  this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
1162
1322
  this.app.singleton(LiveManager4, (container) => container.make(LiveComponentManager));
1323
+ this.app.singleton(PaginationState, () => livePaginationState);
1163
1324
  }
1164
1325
  async boot() {
1165
1326
  const manager = this.app.make(LiveComponentManager);
@@ -1173,6 +1334,42 @@ var LiveServiceProvider = class extends ServiceProvider {
1173
1334
  }
1174
1335
  }
1175
1336
  };
1337
+
1338
+ // Pagination.ts
1339
+ function urlWindow(current, last, onEachSide) {
1340
+ const range = (from, to) => Array.from({ length: Math.max(to - from + 1, 0) }, (_, index) => from + index);
1341
+ if (last < onEachSide * 2 + 8) return range(1, last);
1342
+ const size = onEachSide + 4;
1343
+ if (current <= size) return [...range(1, size + onEachSide), "...", ...range(last - 1, last)];
1344
+ if (current > last - size) return [...range(1, 2), "...", ...range(last - (size + (onEachSide - 1)), last)];
1345
+ return [...range(1, 2), "...", ...range(current - onEachSide, current + onEachSide), "...", ...range(last - 1, last)];
1346
+ }
1347
+ function button(label, action, attributes = "") {
1348
+ const live2 = action === void 0 ? " disabled" : ` o:click="${escapeAttribute(action)}"`;
1349
+ return `<button type="button" class="btn btn-secondary"${attributes}${live2}>${escapeAttribute(label)}</button>`;
1350
+ }
1351
+ function Pagination({ paginator, class: className }) {
1352
+ if (typeof paginator.nextCursor === "function") {
1353
+ throw new LiveError("<Pagination> needs paginate() or simplePaginate(): a cursor paginator has no page numbers.");
1354
+ }
1355
+ const current = paginator.currentPage();
1356
+ const onFirst = paginator.onFirstPage();
1357
+ const hasMore = paginator.hasMorePages();
1358
+ if (onFirst && !hasMore) return { toHtml: () => "" };
1359
+ const parts = [button("\xAB Previous", onFirst ? void 0 : "previousPage")];
1360
+ if (typeof paginator.lastPage === "function") {
1361
+ const onEachSide = Number.isInteger(paginator.onEachSideCount) && paginator.onEachSideCount >= 0 ? paginator.onEachSideCount : 3;
1362
+ for (const page of urlWindow(current, paginator.lastPage(), onEachSide)) {
1363
+ if (page === "...") parts.push('<span aria-hidden="true">...</span>');
1364
+ else if (page === current) parts.push(`<button type="button" class="btn" aria-current="page" disabled>${page}</button>`);
1365
+ else parts.push(button(String(page), `gotoPage(${page})`, ` aria-label="Go to page ${page}"`));
1366
+ }
1367
+ }
1368
+ parts.push(button("Next \xBB", hasMore ? "nextPage" : void 0));
1369
+ const classes = className === void 0 || className === "" ? "" : ` class="${escapeAttribute(className)}"`;
1370
+ const html = `<nav aria-label="Pagination"${classes}>${parts.join("")}</nav>`;
1371
+ return { toHtml: () => html };
1372
+ }
1176
1373
  export {
1177
1374
  Component,
1178
1375
  Computed,
@@ -1188,11 +1385,13 @@ export {
1188
1385
  MethodNotCallableError,
1189
1386
  On,
1190
1387
  PERSISTENT_MIDDLEWARE,
1388
+ Pagination,
1191
1389
  PendingEvent,
1192
1390
  PropertyNotUpdatableError,
1193
1391
  TestRequestScope,
1194
1392
  Testable,
1195
1393
  UPDATE_URI,
1394
+ WithPagination,
1196
1395
  componentName,
1197
1396
  live,
1198
1397
  persistentMiddleware