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

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
@@ -2,6 +2,50 @@
2
2
  import { LiveManager, UrlGenerator, ValidationFactory } from "@orieljs/contracts";
3
3
  import { inject } from "@orieljs/container";
4
4
 
5
+ // errors.ts
6
+ var LiveError = class extends Error {
7
+ name = "LiveError";
8
+ kind = "Server";
9
+ status = 500;
10
+ };
11
+ var CorruptComponentPayloadError = class extends LiveError {
12
+ name = "CorruptComponentPayloadError";
13
+ kind = "Authorization";
14
+ status = 403;
15
+ constructor(component = "unknown") {
16
+ super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
17
+ }
18
+ };
19
+ var MethodNotCallableError = class extends LiveError {
20
+ name = "MethodNotCallableError";
21
+ kind = "Authorization";
22
+ status = 403;
23
+ constructor(method, component) {
24
+ super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
25
+ }
26
+ };
27
+ var PropertyNotUpdatableError = class extends LiveError {
28
+ name = "PropertyNotUpdatableError";
29
+ kind = "Authorization";
30
+ status = 403;
31
+ constructor(path, component) {
32
+ super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
33
+ }
34
+ };
35
+ var LiveComponentNotFoundError = class extends LiveError {
36
+ name = "LiveComponentNotFoundError";
37
+ kind = "Http";
38
+ status = 404;
39
+ constructor(component) {
40
+ super(`Unable to find component: [${component}].`);
41
+ }
42
+ };
43
+ var InvalidLiveRequestError = class extends LiveError {
44
+ name = "InvalidLiveRequestError";
45
+ kind = "Http";
46
+ status = 400;
47
+ };
48
+
5
49
  // metadata.ts
6
50
  Symbol.metadata ??= /* @__PURE__ */ Symbol.for("Symbol.metadata");
7
51
  var LISTENERS = /* @__PURE__ */ Symbol.for("oriel:live.listeners");
@@ -75,6 +119,153 @@ function computedOf(cls) {
75
119
  return /* @__PURE__ */ new Set([...metadataOf(cls)[COMPUTED] ?? [], ...staticsOf(cls, "computed").flat()]);
76
120
  }
77
121
 
122
+ // security.ts
123
+ import { Collection as Collection2 } from "@orieljs/collections";
124
+
125
+ // synthesizers.ts
126
+ import { Collection } from "@orieljs/collections";
127
+ function isModel(value) {
128
+ if (typeof value !== "object" || value === null) return false;
129
+ const candidate = value;
130
+ return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
131
+ }
132
+ var isPlainObject = (value) => {
133
+ const proto = Object.getPrototypeOf(value);
134
+ return proto === Object.prototype || proto === null;
135
+ };
136
+ var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
137
+ function classNameOf(registry, cls) {
138
+ return registry.nameOf(cls) ?? registry.register(cls);
139
+ }
140
+ function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
141
+ switch (typeof value) {
142
+ case "undefined":
143
+ return [null, { s: "u" }];
144
+ case "string":
145
+ case "boolean":
146
+ return value;
147
+ case "number":
148
+ if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
149
+ return value;
150
+ case "bigint":
151
+ case "symbol":
152
+ case "function":
153
+ throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
154
+ }
155
+ if (value === null) return null;
156
+ const object = value;
157
+ if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
158
+ seen.add(object);
159
+ try {
160
+ if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
161
+ if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
162
+ if (isModel(object)) {
163
+ const name = classNameOf(registry, object.constructor);
164
+ if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
165
+ return [null, { s: "mdl", class: name, key: object.getKey() }];
166
+ }
167
+ if (object instanceof Collection) {
168
+ const items = object.all();
169
+ const first = items[0];
170
+ if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
171
+ for (const [i, item] of items.entries()) {
172
+ if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
173
+ }
174
+ const name = classNameOf(registry, first.constructor);
175
+ return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
176
+ }
177
+ return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
178
+ }
179
+ if (isPlainObject(object)) {
180
+ const out = {};
181
+ for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
182
+ return [out, { s: "obj" }];
183
+ }
184
+ const type = object.constructor?.name || "object";
185
+ throw new LiveError(
186
+ `Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
187
+ );
188
+ } finally {
189
+ seen.delete(object);
190
+ }
191
+ }
192
+ function modelClass(registry, name) {
193
+ if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
194
+ return registry.resolve(name);
195
+ }
196
+ async function hydrate(value, registry) {
197
+ if (!isTuple(value)) return value;
198
+ const [inner, meta] = value;
199
+ switch (meta.s) {
200
+ case "u":
201
+ return void 0;
202
+ case "dt":
203
+ return new Date(String(inner));
204
+ case "arr":
205
+ return Promise.all(inner.map((item) => hydrate(item, registry)));
206
+ case "clctn":
207
+ return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
208
+ case "obj": {
209
+ const out = {};
210
+ for (const [key, item] of Object.entries(inner)) {
211
+ Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
212
+ }
213
+ return out;
214
+ }
215
+ case "mdl":
216
+ return modelClass(registry, meta.class).findOrFail(meta.key);
217
+ case "elcln": {
218
+ const cls = modelClass(registry, meta.class);
219
+ const keys = meta.keys ?? [];
220
+ const keyName = new cls().getKeyName();
221
+ const found = [...await cls.query().whereIn(keyName, keys).get()];
222
+ const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
223
+ return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
224
+ }
225
+ }
226
+ throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
227
+ }
228
+
229
+ // security.ts
230
+ var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
231
+ var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
232
+ var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
233
+ var baseApi;
234
+ var isBaseApi = (name) => (baseApi ??= new Set(Object.getOwnPropertyNames(Component.prototype))).has(name);
235
+ var INDEX = /^(?:0|[1-9]\d*)$/;
236
+ function isArraySlot(array, segment) {
237
+ return INDEX.test(segment) && Number(segment) <= array.length;
238
+ }
239
+ function isCallable(instance, method) {
240
+ if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
241
+ if (HOOKS.has(method) || HOOK_PREFIX.test(method) || isBaseApi(method)) return false;
242
+ if (lockedOf(instance.constructor).has(method)) return false;
243
+ const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
244
+ for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
245
+ const descriptor = Object.getOwnPropertyDescriptor(proto, method);
246
+ if (descriptor !== void 0) return typeof descriptor.value === "function";
247
+ }
248
+ return false;
249
+ }
250
+ function isUpdatable(instance, path) {
251
+ const segments = path.split(".");
252
+ const root = segments[0];
253
+ if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
254
+ if (!Object.hasOwn(instance, root)) return false;
255
+ const descriptor = Object.getOwnPropertyDescriptor(instance, root);
256
+ if (!descriptor.enumerable || descriptor.get !== void 0) return false;
257
+ const cls = instance.constructor;
258
+ if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
259
+ let node = descriptor.value;
260
+ for (const segment of segments.slice(1)) {
261
+ if (isModel(node) || node instanceof Collection2) return false;
262
+ if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
263
+ if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
264
+ node = node[segment];
265
+ }
266
+ return !isModel(node) && !(node instanceof Collection2);
267
+ }
268
+
78
269
  // Component.ts
79
270
  var COMPONENT = /* @__PURE__ */ Symbol.for("oriel:live.component");
80
271
  var states = /* @__PURE__ */ new WeakMap();
@@ -206,6 +397,23 @@ var Component = class {
206
397
  skipRender() {
207
398
  stateOf(this).skipRender = true;
208
399
  }
400
+ /**
401
+ * An action for an `o:` directive, typed against its method: `o:click={this.$action(this.delete, tag.id)}` writes
402
+ * `delete(5)`, and a string where `delete` takes a number is a type error. The arguments are written as JSON, the
403
+ * trailing `undefined` ones left out so the method's defaults apply. The method must be one of this component's
404
+ * actions (a public method of its class, not a hook and not @Locked); anything else throws while rendering.
405
+ */
406
+ $action(method, ...params) {
407
+ const name = typeof method === "function" ? method.name : "";
408
+ if (name === "" || this[name] !== method || !isCallable(this, name)) {
409
+ throw new LiveError(
410
+ `[${name === "" ? "anonymous function" : name}] is not an action of [${this.getName() || this.constructor.name}]: pass one of its public methods, as in this.$action(this.save).`
411
+ );
412
+ }
413
+ let count = params.length;
414
+ while (count > 0 && params[count - 1] === void 0) count--;
415
+ return count === 0 ? name : `${name}(${params.slice(0, count).map((param) => JSON.stringify(param) ?? "null").join(", ")})`;
416
+ }
209
417
  /** A full-page component as a route action: Route.get('/counter', Counter). Route parameters go to mount(). */
210
418
  async __invoke() {
211
419
  return inject(LiveManager).page(this);
@@ -219,58 +427,33 @@ import { inject as inject3 } from "@orieljs/container";
219
427
  // Testable.ts
220
428
  import { AssertionError } from "assert";
221
429
  import { isDeepStrictEqual } from "util";
222
- import { LiveManager as LiveManager2, token } from "@orieljs/contracts";
430
+ import { LiveManager as LiveManager2, Session, token } from "@orieljs/contracts";
223
431
  import { Container, currentContainer, inject as inject2 } from "@orieljs/container";
224
432
 
225
- // errors.ts
226
- var LiveError = class extends Error {
227
- name = "LiveError";
228
- kind = "Server";
229
- status = 500;
230
- };
231
- var CorruptComponentPayloadError = class extends LiveError {
232
- name = "CorruptComponentPayloadError";
233
- kind = "Authorization";
234
- status = 403;
235
- constructor(component = "unknown") {
236
- super(`Live encountered corrupt data when trying to hydrate the [${component}] component.`);
237
- }
238
- };
239
- var MethodNotCallableError = class extends LiveError {
240
- name = "MethodNotCallableError";
241
- kind = "Authorization";
242
- status = 403;
243
- constructor(method, component) {
244
- super(`Unable to call component method. Public method [${method}] not found on component [${component}].`);
245
- }
246
- };
247
- var PropertyNotUpdatableError = class extends LiveError {
248
- name = "PropertyNotUpdatableError";
249
- kind = "Authorization";
250
- status = 403;
251
- constructor(path, component) {
252
- super(`Cannot update property [${path}] on component [${component}]: it is not a public, unlocked property.`);
253
- }
254
- };
255
- var LiveComponentNotFoundError = class extends LiveError {
256
- name = "LiveComponentNotFoundError";
257
- kind = "Http";
258
- status = 404;
259
- constructor(component) {
260
- super(`Unable to find component: [${component}].`);
261
- }
262
- };
263
- var InvalidLiveRequestError = class extends LiveError {
264
- name = "InvalidLiveRequestError";
265
- kind = "Http";
266
- status = 400;
267
- };
433
+ // flash.ts
434
+ function trackFlash(session, { saved = true } = {}) {
435
+ if (session === void 0) return () => void 0;
436
+ const before = new Set(session.get("_flash.new", []));
437
+ return (redirected) => {
438
+ if (redirected) return;
439
+ const flashed = session.get("_flash.new", []);
440
+ const fresh = flashed.filter((key) => !before.has(key));
441
+ if (fresh.length === 0) return;
442
+ session.put("_flash.new", flashed.filter((key) => before.has(key)));
443
+ if (!saved) return session.forget(...fresh);
444
+ const old = session.get("_flash.old", []);
445
+ session.put("_flash.old", [...old, ...fresh.filter((key) => !old.includes(key))]);
446
+ };
447
+ }
268
448
 
269
449
  // html.ts
270
450
  var ESCAPES = { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;" };
271
451
  function escapeAttribute(value) {
272
452
  return value.replace(/[&<>"']/g, (char) => ESCAPES[char]);
273
453
  }
454
+ function stripTags(html) {
455
+ return html.replace(/<[^>]*>/g, "");
456
+ }
274
457
  var VOID = /* @__PURE__ */ new Set(["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "source", "track", "wbr"]);
275
458
  var RAW_TEXT = /* @__PURE__ */ new Set(["script", "style", "textarea", "title"]);
276
459
  var TOKEN = /<\/([A-Za-z][\w:-]*)\s*>|<([A-Za-z][\w:-]*)(?:\s+[^\s"'<>/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s"'=<>`]+))?)*\s*(\/?)>/g;
@@ -327,6 +510,15 @@ function requestScope() {
327
510
  const container = currentContainer() ?? Container.getInstance();
328
511
  return container?.bound(TestRequestScope) === true ? container.make(TestRequestScope) : (run) => run();
329
512
  }
513
+ function currentSession() {
514
+ const container = currentContainer();
515
+ if (container?.bound(Session) !== true) return void 0;
516
+ try {
517
+ return container.make(Session);
518
+ } catch {
519
+ return void 0;
520
+ }
521
+ }
330
522
  var Testable = class _Testable {
331
523
  constructor(manager, scope, result) {
332
524
  this.manager = manager;
@@ -381,7 +573,12 @@ var Testable = class _Testable {
381
573
  return this.#update({}, [{ method: "$refresh", params: [] }]);
382
574
  }
383
575
  async #update(updates, calls) {
384
- const result = await this.scope(() => this.manager.updateComponent(this.#snapshot, updates, calls));
576
+ const result = await this.scope(async () => {
577
+ const flashed = trackFlash(currentSession(), { saved: false });
578
+ const updated = await this.manager.updateComponent(this.#snapshot, updates, calls);
579
+ flashed(updated.effects.redirect !== void 0);
580
+ return updated;
581
+ });
385
582
  this.#snapshot = JSON.stringify(result.snapshot);
386
583
  if (result.html !== void 0) this.#html = result.html;
387
584
  this.#effects = result.effects;
@@ -411,6 +608,17 @@ var Testable = class _Testable {
411
608
  for (const value of values) if (!this.html().includes(value)) fail(`Failed asserting that [${value}] is in the rendered HTML.`, this.html(), value);
412
609
  return this;
413
610
  }
611
+ /** Each value, HTML-escaped, appears in the rendered text: the HTML with its tags stripped (Livewire's assertSeeText). */
612
+ assertSeeText(...values) {
613
+ const text = stripTags(this.html());
614
+ for (const value of values) if (!text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is in the rendered text.`, text, value);
615
+ return this;
616
+ }
617
+ assertDontSeeText(...values) {
618
+ const text = stripTags(this.html());
619
+ for (const value of values) if (text.includes(escapeAttribute(value))) fail(`Failed asserting that [${value}] is not in the rendered text.`, text, value);
620
+ return this;
621
+ }
414
622
  assertDispatched(name, params) {
415
623
  const dispatched = (this.#effects.dispatches ?? []).filter((event) => event.name === name);
416
624
  if (dispatched.length === 0) fail(`Failed asserting that an event [${name}] was dispatched.`, this.#effects.dispatches ?? [], name);
@@ -477,16 +685,17 @@ var Live = Object.assign(LiveComponent, {
477
685
  });
478
686
 
479
687
  // LiveComponentManager.ts
480
- import { AsyncLocalStorage } from "async_hooks";
688
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
481
689
  import { randomBytes } from "crypto";
482
690
  import {
483
691
  ClassRegistry,
484
692
  ConfigRepository,
485
- CurrentRoute,
693
+ CurrentRoute as CurrentRoute2,
486
694
  Encrypter,
487
695
  HttpRequest,
488
696
  Redirector,
489
697
  Router,
698
+ Session as Session2,
490
699
  ViewFactory
491
700
  } from "@orieljs/contracts";
492
701
  import { currentContainer as currentContainer2 } from "@orieljs/container";
@@ -541,10 +750,12 @@ async function discover(directory) {
541
750
  }
542
751
 
543
752
  // persistent.ts
544
- var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest"];
753
+ import { CurrentRoute } from "@orieljs/contracts";
754
+ var PERSISTENT_MIDDLEWARE = ["auth", "verified", "guest", "can"];
755
+ var ROUTER_METHODS = ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups", "substituteBindings"];
545
756
  function canMatch(router) {
546
757
  const methods = router;
547
- return ["getRoutesByMatchOrder", "getPatterns", "getPatternVersion", "getMiddleware", "getMiddlewareGroups"].every((name) => typeof methods[name] === "function");
758
+ return ROUTER_METHODS.every((name) => typeof methods[name] === "function");
548
759
  }
549
760
  function isMatchingRoute(route) {
550
761
  const candidate = route;
@@ -573,7 +784,7 @@ function expand(references, aliases, groups, out = [], visiting = /* @__PURE__ *
573
784
  }
574
785
  return out;
575
786
  }
576
- function persistentMiddleware(router, persistent, method, path) {
787
+ function persistentPass(router, persistent, method, path) {
577
788
  if (!canMatch(router)) throw new LiveError("Live re-applies the page route's middleware to updates, which needs Oriel's router.");
578
789
  const routes = router.getRoutesByMatchOrder();
579
790
  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,187 +792,144 @@ function persistentMiddleware(router, persistent, method, path) {
581
792
  const target = routePath(path);
582
793
  const patterns = router.getPatterns();
583
794
  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 [];
795
+ let matched;
796
+ for (const candidate of routes) {
797
+ const values2 = candidate.methods.includes(verb) ? candidate.match(target, patterns, version) : void 0;
798
+ if (values2 !== void 0) {
799
+ matched = { route: candidate, values: values2 };
800
+ break;
801
+ }
802
+ }
803
+ if (matched === void 0) return void 0;
804
+ const { route, values } = matched;
586
805
  const aliases = router.getMiddleware();
587
806
  const groups = router.getMiddlewareGroups();
588
807
  const wanted = new Set(persistent.flatMap((name) => Object.hasOwn(aliases, name) ? [aliases[name]] : []));
589
808
  const excluded = new Set(expand(route.excludedMiddleware(), aliases, groups).map((entry) => entry.cls));
590
809
  const ids = /* @__PURE__ */ new Map();
591
810
  const seen = /* @__PURE__ */ new Set();
592
- return expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
811
+ const middleware = expand(route.gatherMiddleware(), aliases, groups).filter((entry) => {
593
812
  if (!ids.has(entry.cls)) ids.set(entry.cls, ids.size);
594
813
  const key = `${ids.get(entry.cls)}:${entry.parameters.join(",")}`;
595
814
  if (!wanted.has(entry.cls) || excluded.has(entry.cls) || seen.has(key)) return false;
596
815
  seen.add(key);
597
816
  return true;
598
817
  });
818
+ const early = new Set(expand(["web", "api"], aliases, groups).map((entry) => entry.cls));
819
+ for (const name of ["auth", "guest"]) if (Object.hasOwn(aliases, name)) early.add(aliases[name]);
820
+ const before = middleware.filter((entry) => early.has(entry.cls));
821
+ const after = middleware.filter((entry) => !early.has(entry.cls));
822
+ return { router, route, values, middleware: [...before, ...after], at: before.length };
599
823
  }
600
- async function throughMiddleware(container, request, middleware, destination) {
824
+ function persistentMiddleware(router, persistent, method, path) {
825
+ return [...persistentPass(router, persistent, method, path)?.middleware ?? []];
826
+ }
827
+ function currentRoute(container) {
828
+ if (!container.bound(CurrentRoute)) return void 0;
829
+ try {
830
+ return container.make(CurrentRoute);
831
+ } catch {
832
+ return void 0;
833
+ }
834
+ }
835
+ async function throughMiddleware(container, request, pass, destination) {
836
+ const original = currentRoute(container);
837
+ const page = { route: pass.route, parameters: { ...pass.values }, middleware: [] };
838
+ let swapped = false;
839
+ const show = () => {
840
+ container.instance(CurrentRoute, page);
841
+ swapped = true;
842
+ };
843
+ const restore = () => {
844
+ if (!swapped) return;
845
+ if (original === void 0) container.forgetInstance(CurrentRoute);
846
+ else container.instance(CurrentRoute, original);
847
+ swapped = false;
848
+ };
601
849
  let ran;
602
850
  const run = async (index) => {
603
- if (index === middleware.length) {
851
+ if (index === pass.at) Object.assign(page.parameters, await pass.router.substituteBindings(pass.route, pass.values, container));
852
+ if (index === pass.middleware.length) {
853
+ restore();
604
854
  ran = { result: await destination() };
605
855
  return new Response(null, { status: 204 });
606
856
  }
607
- const { cls, parameters } = middleware[index];
857
+ const { cls, parameters } = pass.middleware[index];
608
858
  const instance = container.make(cls);
609
859
  const next = () => run(index + 1);
610
860
  return instance.handle(request, next, ...parameters);
611
861
  };
612
- const response = await run(0);
613
- return ran ? ran.result : response;
862
+ show();
863
+ try {
864
+ const response = await run(0);
865
+ return ran ? ran.result : response;
866
+ } finally {
867
+ restore();
868
+ }
614
869
  }
615
870
 
616
- // security.ts
617
- import { Collection as Collection2 } from "@orieljs/collections";
618
-
619
- // synthesizers.ts
620
- import { Collection } from "@orieljs/collections";
621
- function isModel(value) {
622
- if (typeof value !== "object" || value === null) return false;
623
- const candidate = value;
624
- return typeof candidate.getKey === "function" && typeof candidate.getKeyName === "function" && typeof candidate.getTable === "function" && typeof candidate.exists === "boolean";
871
+ // WithPagination.ts
872
+ import { AsyncLocalStorage } from "async_hooks";
873
+ var PAGINATED = /* @__PURE__ */ Symbol.for("oriel:live.pagination");
874
+ function toPage(value) {
875
+ const page = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
876
+ return Number.isFinite(page) && page >= 1 ? Math.min(Math.floor(page), Number.MAX_SAFE_INTEGER) : 1;
625
877
  }
626
- var isPlainObject = (value) => {
627
- const proto = Object.getPrototypeOf(value);
628
- return proto === Object.prototype || proto === null;
629
- };
630
- var isTuple = (value) => Array.isArray(value) && value.length === 2 && typeof value[1] === "object" && value[1] !== null && !Array.isArray(value[1]) && typeof value[1].s === "string";
631
- function classNameOf(registry, cls) {
632
- return registry.nameOf(cls) ?? registry.register(cls);
878
+ async function hook(instance, name, ...args) {
879
+ const method = instance[name];
880
+ if (typeof method === "function") await method.apply(instance, args);
633
881
  }
634
- function dehydrate(value, registry, path, seen = /* @__PURE__ */ new Set()) {
635
- switch (typeof value) {
636
- case "undefined":
637
- return [null, { s: "u" }];
638
- case "string":
639
- case "boolean":
640
- return value;
641
- case "number":
642
- if (!Number.isFinite(value)) throw new LiveError(`Property [${path}] is ${value}: Live state must be JSON (finite numbers only).`);
643
- return value;
644
- case "bigint":
645
- case "symbol":
646
- case "function":
647
- throw new LiveError(`Property [${path}] holds a ${typeof value}: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`);
648
- }
649
- if (value === null) return null;
650
- const object = value;
651
- if (seen.has(object)) throw new LiveError(`Property [${path}] contains a circular reference.`);
652
- seen.add(object);
653
- try {
654
- if (object instanceof Date) return [object.toISOString(), { s: "dt" }];
655
- if (Array.isArray(object)) return [object.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "arr" }];
656
- if (isModel(object)) {
657
- const name = classNameOf(registry, object.constructor);
658
- if (!object.exists) throw new LiveError(`Property [${path}] holds an unsaved [${name}] model: only saved models can be kept between requests.`);
659
- return [null, { s: "mdl", class: name, key: object.getKey() }];
882
+ function WithPagination(Base) {
883
+ class WithPaginationMixin extends Base {
884
+ page = 1;
885
+ nextPage() {
886
+ return this.setPage(toPage(this.page) + 1);
660
887
  }
661
- if (object instanceof Collection) {
662
- const items = object.all();
663
- const first = items[0];
664
- if (items.length > 0 && isModel(first) && items.every((item) => isModel(item) && item.constructor === first.constructor)) {
665
- for (const [i, item] of items.entries()) {
666
- if (!item.exists) throw new LiveError(`Property [${path}.${i}] holds an unsaved model.`);
667
- }
668
- const name = classNameOf(registry, first.constructor);
669
- return [null, { s: "elcln", class: name, keys: items.map((item) => item.getKey()) }];
670
- }
671
- return [items.map((item, i) => dehydrate(item, registry, `${path}.${i}`, seen)), { s: "clctn" }];
888
+ previousPage() {
889
+ return this.setPage(Math.max(toPage(this.page) - 1, 1));
672
890
  }
673
- if (isPlainObject(object)) {
674
- const out = {};
675
- for (const [key, item] of Object.entries(object)) out[key] = dehydrate(item, registry, `${path}.${key}`, seen);
676
- return [out, { s: "obj" }];
891
+ gotoPage(page) {
892
+ return this.setPage(page);
677
893
  }
678
- const type = object.constructor?.name || "object";
679
- throw new LiveError(
680
- `Property [${path}] holds a [${type}] instance: Live state may only hold primitives, arrays, plain objects, Dates, models and collections.`
681
- );
682
- } finally {
683
- seen.delete(object);
684
- }
685
- }
686
- function modelClass(registry, name) {
687
- if (typeof name !== "string" || !registry.has(name)) throw new LiveError(`Unable to hydrate the [${String(name)}] model: it is not registered.`);
688
- return registry.resolve(name);
689
- }
690
- async function hydrate(value, registry) {
691
- if (!isTuple(value)) return value;
692
- const [inner, meta] = value;
693
- switch (meta.s) {
694
- case "u":
695
- return void 0;
696
- case "dt":
697
- return new Date(String(inner));
698
- case "arr":
699
- return Promise.all(inner.map((item) => hydrate(item, registry)));
700
- case "clctn":
701
- return new Collection(await Promise.all(inner.map((item) => hydrate(item, registry))));
702
- case "obj": {
703
- const out = {};
704
- for (const [key, item] of Object.entries(inner)) {
705
- Object.defineProperty(out, key, { value: await hydrate(item, registry), enumerable: true, writable: true, configurable: true });
706
- }
707
- return out;
894
+ resetPage() {
895
+ return this.setPage(1);
708
896
  }
709
- case "mdl":
710
- return modelClass(registry, meta.class).findOrFail(meta.key);
711
- case "elcln": {
712
- const cls = modelClass(registry, meta.class);
713
- const keys = meta.keys ?? [];
714
- const keyName = new cls().getKeyName();
715
- const found = [...await cls.query().whereIn(keyName, keys).get()];
716
- const byKey = new Map(found.map((model) => [String(model.getKey()), model]));
717
- return new Collection(keys.map((key) => byKey.get(String(key))).filter((model) => model !== void 0));
897
+ async setPage(page) {
898
+ const next = toPage(page);
899
+ await hook(this, "updatingPage", next);
900
+ this.page = next;
901
+ await hook(this, "updatedPage", next);
718
902
  }
719
903
  }
720
- throw new LiveError(`Unknown snapshot value type [${String(meta.s)}].`);
904
+ Object.defineProperty(WithPaginationMixin.prototype, PAGINATED, { value: true });
905
+ return WithPaginationMixin;
721
906
  }
722
-
723
- // security.ts
724
- var HOOKS = /* @__PURE__ */ new Set(["constructor", "render", "mount", "boot", "booted", "hydrate", "dehydrate", "updating", "updated", "rendering", "rendered", "exception"]);
725
- var HOOK_PREFIX = /^(updating|updated|hydrate|dehydrate)[A-Z_]/;
726
- var UNSAFE_SEGMENTS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
727
- var BASE_API = new Set(Object.getOwnPropertyNames(Component.prototype));
728
- var INDEX = /^(?:0|[1-9]\d*)$/;
729
- function isArraySlot(array, segment) {
730
- return INDEX.test(segment) && Number(segment) <= array.length;
907
+ function isPaginated(component) {
908
+ return component[PAGINATED] === true;
731
909
  }
732
- function isCallable(instance, method) {
733
- if (method === "" || method.startsWith("_") || method.startsWith("$") || method.startsWith("#")) return false;
734
- if (HOOKS.has(method) || HOOK_PREFIX.test(method) || BASE_API.has(method)) return false;
735
- if (lockedOf(instance.constructor).has(method)) return false;
736
- const isBase = (proto) => proto === Component.prototype || proto === Object.prototype;
737
- for (let proto = Object.getPrototypeOf(instance); proto !== null && !isBase(proto); proto = Object.getPrototypeOf(proto)) {
738
- const descriptor = Object.getOwnPropertyDescriptor(proto, method);
739
- if (descriptor !== void 0) return typeof descriptor.value === "function";
740
- }
741
- return false;
910
+ var rendering = new AsyncLocalStorage();
911
+ function renderPaginated(component, url, run) {
912
+ return rendering.run(isPaginated(component) ? { component, url } : void 0, run);
742
913
  }
743
- function isUpdatable(instance, path) {
744
- const segments = path.split(".");
745
- const root = segments[0];
746
- if (root === "" || root.startsWith("_") || segments.some((segment) => segment === "" || UNSAFE_SEGMENTS.has(segment))) return false;
747
- if (!Object.hasOwn(instance, root)) return false;
748
- const descriptor = Object.getOwnPropertyDescriptor(instance, root);
749
- if (!descriptor.enumerable || descriptor.get !== void 0) return false;
750
- const cls = instance.constructor;
751
- if (lockedOf(cls).has(root) || computedOf(cls).has(root)) return false;
752
- let node = descriptor.value;
753
- for (const segment of segments.slice(1)) {
754
- if (isModel(node) || node instanceof Collection2) return false;
755
- if (Array.isArray(node) && !isArraySlot(node, segment)) return false;
756
- if (typeof node !== "object" || node === null || !Object.hasOwn(node, segment)) return true;
757
- node = node[segment];
758
- }
759
- return !isModel(node) && !(node instanceof Collection2);
914
+ function outsidePagination(run) {
915
+ return rendering.run(void 0, run);
760
916
  }
917
+ var livePaginationState = {
918
+ currentPage(pageName) {
919
+ const current = rendering.getStore();
920
+ if (current === void 0 || pageName !== "page") return void 0;
921
+ const page = toPage(current.component.page);
922
+ current.component.page = page;
923
+ return page;
924
+ },
925
+ currentPath() {
926
+ return rendering.getStore()?.url;
927
+ }
928
+ };
761
929
 
762
930
  // LiveComponentManager.ts
763
- var renderContext = new AsyncLocalStorage();
764
- var pageRequest = new AsyncLocalStorage();
931
+ var renderContext = new AsyncLocalStorage2();
932
+ var pageRequest = new AsyncLocalStorage2();
765
933
  function optional(container, token2) {
766
934
  if (!container.bound(token2)) return void 0;
767
935
  try {
@@ -774,7 +942,7 @@ var isRecord = (value) => typeof value === "object" && value !== null && !Array.
774
942
  function setOwn(object, key, value) {
775
943
  Object.defineProperty(object, key, { value, enumerable: true, writable: true, configurable: true });
776
944
  }
777
- async function hook(instance, name, ...args) {
945
+ async function hook2(instance, name, ...args) {
778
946
  const method = instance[name];
779
947
  if (typeof method === "function") await method.apply(instance, args);
780
948
  }
@@ -825,7 +993,7 @@ var LiveComponentManager = class {
825
993
  setOwn(context.children, childKey, existing);
826
994
  return `<${existing[0]} o:id="${escapeAttribute(existing[1])}"></${existing[0]}>`;
827
995
  }
828
- const result = await this.mountComponent(component, params);
996
+ const result = await outsidePagination(() => this.mountComponent(component, params));
829
997
  setOwn(context.children, childKey, [result.tag, result.snapshot.memo.id]);
830
998
  return result.html;
831
999
  }
@@ -837,14 +1005,14 @@ var LiveComponentManager = class {
837
1005
  const state = stateOf(instance);
838
1006
  state.id = randomBytes(15).toString("base64url");
839
1007
  state.name = name;
840
- await hook(instance, "boot");
1008
+ await hook2(instance, "boot");
841
1009
  for (const [property, value] of Object.entries(params)) {
842
1010
  if (Object.hasOwn(instance, property)) instance[property] = value;
843
1011
  }
844
1012
  this.prepareComputed(instance);
845
- await hook(instance, "mount", params);
846
1013
  const request = optional(this.container(), HttpRequest);
847
1014
  const page = pageRequest.getStore() ?? { path: request?.path() ?? "", method: request?.method() ?? "GET" };
1015
+ await renderPaginated(instance, this.pageUrl(page.path), () => hook2(instance, "mount", params));
848
1016
  const memo = { id: state.id, name, path: page.path, method: page.method, children: {}, errors: {} };
849
1017
  const { html, tag } = await this.render(instance, memo, {});
850
1018
  const effects = this.effects(instance);
@@ -872,22 +1040,24 @@ var LiveComponentManager = class {
872
1040
  state.id = snapshot.memo.id;
873
1041
  state.name = snapshot.memo.name;
874
1042
  state.errors = { ...snapshot.memo.errors };
875
- await hook(instance, "boot");
1043
+ await hook2(instance, "boot");
876
1044
  const registry = this.registry();
877
1045
  for (const [property, value] of Object.entries(snapshot.data)) {
878
1046
  Object.defineProperty(instance, property, { value: await hydrate(value, registry), enumerable: true, writable: true, configurable: true });
879
1047
  }
880
1048
  this.prepareComputed(instance);
881
- await hook(instance, "hydrate");
882
1049
  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
- }
1050
+ await renderPaginated(instance, this.pageUrl(snapshot.memo.path), async () => {
1051
+ await hook2(instance, "hydrate");
1052
+ try {
1053
+ for (const [path, value] of Object.entries(updates)) await this.applyUpdate(instance, path, value);
1054
+ for (const call of calls) returns.push(await this.call(instance, call.method, call.params));
1055
+ } catch (error) {
1056
+ const errors = error.errors;
1057
+ if (error.kind !== "Validation" || !isRecord(errors)) throw error;
1058
+ state.errors = { ...errors };
1059
+ }
1060
+ });
891
1061
  const memo = { ...snapshot.memo, children: { ...snapshot.memo.children } };
892
1062
  const effects = this.effects(instance);
893
1063
  let html;
@@ -902,31 +1072,36 @@ var LiveComponentManager = class {
902
1072
  return { instance, snapshot: await this.snapshot(instance, memo), effects, html, tag };
903
1073
  }
904
1074
  /**
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.
1075
+ * The route action of POST /oriel/live/update. Each component's update runs behind the persistent middleware of the
1076
+ * route that rendered its page (Livewire's): `auth`, `verified`, `guest` and `can`, with the page route's parameters
1077
+ * bound again. A signed-out tab gets a 401, a revoked ability a 403 and a deleted model a 404, before the component
1078
+ * hydrates. When a middleware answers instead (a redirect), that is the response. A key first flashed during the
1079
+ * update shows in this response only, unless the update redirects.
908
1080
  */
909
1081
  async handle(request) {
910
1082
  if (!this.isLiveRequest(request)) throw new InvalidLiveRequestError("Live update requests need the X-Oriel-Live header.");
911
1083
  const container = this.container();
912
1084
  const persistent = optional(container, ConfigRepository)?.get("live.persistent_middleware", PERSISTENT_MIDDLEWARE) ?? PERSISTENT_MIDDLEWARE;
913
1085
  const components = [];
1086
+ const flashed = trackFlash(optional(container, Session2));
914
1087
  for (const component of this.payload(request).components) {
915
1088
  const { memo } = this.verify(component.snapshot);
916
- const middleware = persistentMiddleware(container.make(Router), persistent, memo.method, memo.path);
1089
+ const pass = persistentPass(container.make(Router), persistent, memo.method, memo.path);
1090
+ const update = () => this.updateComponent(component.snapshot, component.updates, component.calls);
917
1091
  const result = await pageRequest.run(
918
1092
  { path: memo.path, method: memo.method },
919
- () => throughMiddleware(container, request, middleware, () => this.updateComponent(component.snapshot, component.updates, component.calls))
1093
+ () => pass === void 0 || pass.middleware.length === 0 ? update() : throughMiddleware(container, request, pass, update)
920
1094
  );
921
1095
  if (result instanceof Response) return result;
922
1096
  components.push({ snapshot: JSON.stringify(result.snapshot), effects: result.effects });
923
1097
  }
1098
+ flashed(components.some((component) => component.effects.redirect !== void 0));
924
1099
  return { components };
925
1100
  }
926
1101
  /** A full-page component (Route.get('/counter', Counter)): mounted with the route parameters, inside the layout. */
927
1102
  async page(component) {
928
1103
  const container = this.container();
929
- const params = { ...optional(container, CurrentRoute)?.parameters ?? {} };
1104
+ const params = { ...optional(container, CurrentRoute2)?.parameters ?? {} };
930
1105
  const cls = component.constructor;
931
1106
  const result = await this.mountComponent(cls, params, component);
932
1107
  const redirect = result.effects.redirect;
@@ -995,8 +1170,8 @@ var LiveComponentManager = class {
995
1170
  const [root, ...rest] = path.split(".");
996
1171
  const key = rest.length > 0 ? rest.join(".") : void 0;
997
1172
  const studly = Str2.studly(root);
998
- await hook(instance, "updating", path, value);
999
- await hook(instance, `updating${studly}`, value, key);
1173
+ await hook2(instance, "updating", path, value);
1174
+ await hook2(instance, `updating${studly}`, value, key);
1000
1175
  const target = instance;
1001
1176
  if (rest.length === 0) target[root] = value;
1002
1177
  else {
@@ -1014,8 +1189,8 @@ var LiveComponentManager = class {
1014
1189
  assertSlot(node, last);
1015
1190
  Object.defineProperty(node, last, { value, enumerable: true, writable: true, configurable: true });
1016
1191
  }
1017
- await hook(instance, "updated", path, value);
1018
- await hook(instance, `updated${studly}`, value, key);
1192
+ await hook2(instance, "updated", path, value);
1193
+ await hook2(instance, `updated${studly}`, value, key);
1019
1194
  }
1020
1195
  /** Runs one call from the browser: a magic action, an event, or a public method. Returns its return value. */
1021
1196
  async call(instance, method, params) {
@@ -1043,18 +1218,35 @@ var LiveComponentManager = class {
1043
1218
  if (!isCallable(instance, method)) throw new MethodNotCallableError(method, name);
1044
1219
  return instance[method](...params);
1045
1220
  }
1046
- /** Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. */
1221
+ /**
1222
+ * Renders the component (rendering/rendered hooks, nested children tracked) and checks for a single root. A
1223
+ * WithPagination component's page and page URL are what paginate() reads while it renders, as in its mount() and in
1224
+ * its update's hooks and actions.
1225
+ */
1047
1226
  async render(instance, memo, previous) {
1048
- await hook(instance, "rendering");
1227
+ await hook2(instance, "rendering");
1049
1228
  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);
1229
+ const html = await renderContext.run(
1230
+ context,
1231
+ () => renderPaginated(instance, this.pageUrl(memo.path), async () => {
1232
+ const output = await instance.render();
1233
+ return typeof output === "string" ? output : this.container().make(ViewFactory).renderToString(output);
1234
+ })
1235
+ );
1236
+ await hook2(instance, "rendered", html);
1055
1237
  memo.children = context.children;
1056
1238
  return { html: html.trim(), tag: rootTag(html, memo.name) };
1057
1239
  }
1240
+ /** The absolute URL of the page a memo path belongs to, on the current request's origin ('/' + path without one). */
1241
+ pageUrl(path) {
1242
+ const relative3 = `/${path.replace(/^\/+/, "")}`;
1243
+ const url = optional(this.container(), HttpRequest)?.url();
1244
+ try {
1245
+ return url === void 0 ? relative3 : new URL(relative3, url).href;
1246
+ } catch {
1247
+ return relative3;
1248
+ }
1249
+ }
1058
1250
  /** Dispatches, redirects: the effects every response carries. */
1059
1251
  effects(instance) {
1060
1252
  const state = stateOf(instance);
@@ -1073,7 +1265,7 @@ var LiveComponentManager = class {
1073
1265
  }
1074
1266
  /** dehydrate() hook, then the state as snapshot data, the error bag, and the checksum over both. */
1075
1267
  async snapshot(instance, memo) {
1076
- await hook(instance, "dehydrate");
1268
+ await hook2(instance, "dehydrate");
1077
1269
  const registry = this.registry();
1078
1270
  const data = {};
1079
1271
  for (const [property, value] of Object.entries(instance)) data[property] = dehydrate(value, registry, `${memo.name}.${property}`);
@@ -1083,7 +1275,7 @@ var LiveComponentManager = class {
1083
1275
  };
1084
1276
 
1085
1277
  // LiveServiceProvider.ts
1086
- import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, Router as Router2 } from "@orieljs/contracts";
1278
+ import { ConfigRepository as ConfigRepository3, ConsoleKernel, HttpRequest as HttpRequest2, LiveManager as LiveManager4, PaginationState, Router as Router2 } from "@orieljs/contracts";
1087
1279
  import { inject as inject4 } from "@orieljs/container";
1088
1280
  import { ServiceProvider } from "@orieljs/support";
1089
1281
 
@@ -1160,6 +1352,7 @@ var LiveServiceProvider = class extends ServiceProvider {
1160
1352
  this.mergeConfigFrom({ class_path: this.app.appPath("Live"), layout: "components.layouts.app", persistent_middleware: [...PERSISTENT_MIDDLEWARE] }, "live");
1161
1353
  this.app.singleton(LiveComponentManager, () => new LiveComponentManager(this.app));
1162
1354
  this.app.singleton(LiveManager4, (container) => container.make(LiveComponentManager));
1355
+ this.app.singleton(PaginationState, () => livePaginationState);
1163
1356
  }
1164
1357
  async boot() {
1165
1358
  const manager = this.app.make(LiveComponentManager);
@@ -1173,6 +1366,42 @@ var LiveServiceProvider = class extends ServiceProvider {
1173
1366
  }
1174
1367
  }
1175
1368
  };
1369
+
1370
+ // Pagination.ts
1371
+ function urlWindow(current, last, onEachSide) {
1372
+ const range = (from, to) => Array.from({ length: Math.max(to - from + 1, 0) }, (_, index) => from + index);
1373
+ if (last < onEachSide * 2 + 8) return range(1, last);
1374
+ const size = onEachSide + 4;
1375
+ if (current <= size) return [...range(1, size + onEachSide), "...", ...range(last - 1, last)];
1376
+ if (current > last - size) return [...range(1, 2), "...", ...range(last - (size + (onEachSide - 1)), last)];
1377
+ return [...range(1, 2), "...", ...range(current - onEachSide, current + onEachSide), "...", ...range(last - 1, last)];
1378
+ }
1379
+ function button(label, action, attributes = "") {
1380
+ const live2 = action === void 0 ? " disabled" : ` o:click="${escapeAttribute(action)}"`;
1381
+ return `<button type="button" class="btn btn-secondary"${attributes}${live2}>${escapeAttribute(label)}</button>`;
1382
+ }
1383
+ function Pagination({ paginator, class: className }) {
1384
+ if (typeof paginator.nextCursor === "function") {
1385
+ throw new LiveError("<Pagination> needs paginate() or simplePaginate(): a cursor paginator has no page numbers.");
1386
+ }
1387
+ const current = paginator.currentPage();
1388
+ const onFirst = paginator.onFirstPage();
1389
+ const hasMore = paginator.hasMorePages();
1390
+ if (onFirst && !hasMore) return { toHtml: () => "" };
1391
+ const parts = [button("\xAB Previous", onFirst ? void 0 : "previousPage")];
1392
+ if (typeof paginator.lastPage === "function") {
1393
+ const onEachSide = Number.isInteger(paginator.onEachSideCount) && paginator.onEachSideCount >= 0 ? paginator.onEachSideCount : 3;
1394
+ for (const page of urlWindow(current, paginator.lastPage(), onEachSide)) {
1395
+ if (page === "...") parts.push('<span aria-hidden="true">...</span>');
1396
+ else if (page === current) parts.push(`<button type="button" class="btn" aria-current="page" disabled>${page}</button>`);
1397
+ else parts.push(button(String(page), `gotoPage(${page})`, ` aria-label="Go to page ${page}"`));
1398
+ }
1399
+ }
1400
+ parts.push(button("Next \xBB", hasMore ? "nextPage" : void 0));
1401
+ const classes = className === void 0 || className === "" ? "" : ` class="${escapeAttribute(className)}"`;
1402
+ const html = `<nav aria-label="Pagination"${classes}>${parts.join("")}</nav>`;
1403
+ return { toHtml: () => html };
1404
+ }
1176
1405
  export {
1177
1406
  Component,
1178
1407
  Computed,
@@ -1188,11 +1417,13 @@ export {
1188
1417
  MethodNotCallableError,
1189
1418
  On,
1190
1419
  PERSISTENT_MIDDLEWARE,
1420
+ Pagination,
1191
1421
  PendingEvent,
1192
1422
  PropertyNotUpdatableError,
1193
1423
  TestRequestScope,
1194
1424
  Testable,
1195
1425
  UPDATE_URI,
1426
+ WithPagination,
1196
1427
  componentName,
1197
1428
  live,
1198
1429
  persistentMiddleware