@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/README.md +45 -1
- package/dist/index.d.ts +66 -10
- package/dist/index.js +249 -50
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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(() =>
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
585
|
-
|
|
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
|
-
|
|
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,
|
|
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 ===
|
|
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
|
-
|
|
613
|
-
|
|
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
|
|
764
|
-
var pageRequest = new
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
|
906
|
-
* that rendered its page (Livewire's
|
|
907
|
-
*
|
|
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
|
|
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
|
-
() =>
|
|
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,
|
|
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
|
|
999
|
-
await
|
|
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
|
|
1018
|
-
await
|
|
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
|
-
/**
|
|
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
|
|
1195
|
+
await hook2(instance, "rendering");
|
|
1049
1196
|
const context = { previous, children: {}, index: 0 };
|
|
1050
|
-
const html = await renderContext.run(
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
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
|
|
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
|