riftt 1.0.0 → 1.0.1

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/src/router.js DELETED
@@ -1,535 +0,0 @@
1
- import { gsap } from "gsap";
2
- import { processModules } from "./modules.js";
3
-
4
- function pathToRegex(path) {
5
- const keys = [];
6
- const regex = path.replace(/:([^/]+)/g, (_, key) => {
7
- keys.push(key);
8
- return "([^/]+)";
9
- });
10
- return { regex: new RegExp(`^${regex}$`), keys };
11
- }
12
-
13
- function matchRoute(pathname, routes) {
14
- for (const r of routes) {
15
- const { regex, keys } = pathToRegex(r.path);
16
- const match = pathname.match(regex);
17
- if (match) {
18
- const params = {};
19
- keys.forEach((k, i) => (params[k] = match[i + 1]));
20
- return { route: r, params };
21
- }
22
- }
23
- return null;
24
- }
25
-
26
- function findParentRoutes(pathname, routes) {
27
- const parents = [];
28
- const segments = pathname.split("/").filter(Boolean);
29
-
30
- for (let i = 1; i < segments.length; i++) {
31
- const testPath = `/${segments.slice(0, i).join("/")}`;
32
-
33
- for (const route of routes) {
34
- const { regex, keys } = pathToRegex(route.path);
35
- const match = testPath.match(regex);
36
- if (match) {
37
- const params = {};
38
- keys.forEach((k, idx) => (params[k] = match[idx + 1]));
39
- parents.push({ route, params });
40
- break;
41
- }
42
- }
43
- }
44
-
45
- return parents;
46
- }
47
-
48
- async function renderServerTemplate(
49
- name,
50
- props,
51
- target,
52
- dataset,
53
- isInitialLoad = false,
54
- ) {
55
- let body = typeof props === "function" ? props({}) : props;
56
-
57
- if (dataset) {
58
- body = { ...body, dataset };
59
- }
60
-
61
- const html = await fetch(`/templates/${name}`, {
62
- method: "POST",
63
- headers: { "Content-Type": "application/json" },
64
- body: JSON.stringify(body),
65
- }).then((r) => r.text());
66
-
67
- target.innerHTML = html;
68
-
69
- await processModules(target);
70
-
71
- const scripts = target.querySelectorAll("script");
72
- scripts.forEach((script) => {
73
- const newScript = document.createElement("script");
74
- if (script.src) {
75
- newScript.src = script.src;
76
- } else {
77
- newScript.textContent = script.textContent;
78
- }
79
- script.parentNode.replaceChild(newScript, script);
80
- });
81
-
82
- await hydrateNestedTemplates(target, isInitialLoad);
83
- await processElementTransitions(target, isInitialLoad);
84
- }
85
-
86
- async function playTransition(elements, transition) {
87
- if (!transition) return;
88
-
89
- return new Promise((resolve) => {
90
- requestAnimationFrame(() => {
91
- // `from` is not a valid property, so remove it
92
- const gsapVars = { ...transition, from: undefined };
93
- gsapVars.onComplete = resolve;
94
-
95
- if (transition.from) {
96
- gsap.fromTo(elements, transition.from, gsapVars);
97
- } else {
98
- gsap.to(elements, gsapVars);
99
- }
100
- });
101
- });
102
- }
103
-
104
- function parseJSON(jsonStr) {
105
- try {
106
- return JSON.parse(jsonStr);
107
- } catch (e) {
108
- try {
109
- return Function(`"use strict"; return (${jsonStr})`)();
110
- } catch (e2) {
111
- throw new Error("invalid JSON syntax");
112
- }
113
- }
114
- }
115
-
116
- async function processElementTransitions(container, isInitialLoad = false) {
117
- if (isInitialLoad) return;
118
-
119
- const transitionPromises = [];
120
-
121
- if (container.hasAttribute("r-transition-stagger-in")) {
122
- try {
123
- const transition = parseJSON(
124
- container.getAttribute("r-transition-stagger-in"),
125
- );
126
- const children = Array.from(container.children);
127
- if (children.length > 0) {
128
- gsap.set(children, transition.from || {});
129
- transitionPromises.push(playTransition(children, transition));
130
- }
131
- } catch (e) {
132
- console.warn("invalid transition data on container stagger element:", e);
133
- }
134
- }
135
-
136
- // descendants have stagger transitions?
137
- const staggerElements = container.querySelectorAll(
138
- "[r-transition-stagger-in]",
139
- );
140
- for (const el of staggerElements) {
141
- try {
142
- const transition = parseJSON(el.getAttribute("r-transition-stagger-in"));
143
- const children = Array.from(el.children);
144
- if (children.length > 0) {
145
- gsap.set(children, transition.from || {});
146
- transitionPromises.push(playTransition(children, transition));
147
- }
148
- } catch (e) {
149
- console.warn("invalid transition data on stagger element:", e);
150
- }
151
- }
152
-
153
- if (container.hasAttribute("r-transition-in")) {
154
- try {
155
- const transition = parseJSON(container.getAttribute("r-transition-in"));
156
- gsap.set(container, transition.from || {});
157
- transitionPromises.push(playTransition(container, transition));
158
- } catch (e) {
159
- console.warn("invalid transition data on container element:", e);
160
- }
161
- }
162
-
163
- // descendants have individual transitions?
164
- const individualElements = container.querySelectorAll("[r-transition-in]");
165
- for (const el of individualElements) {
166
- const parent = el.closest("[r-transition-stagger-in]");
167
- if (parent && parent !== el) continue;
168
-
169
- try {
170
- const transition = parseJSON(el.getAttribute("r-transition-in"));
171
- gsap.set(el, transition.from || {});
172
- transitionPromises.push(playTransition(el, transition));
173
- } catch (e) {
174
- console.warn("invalid transition data on element:", e);
175
- }
176
- }
177
-
178
- Promise.all(transitionPromises);
179
- }
180
-
181
- function captureGlobalTransitions(container) {
182
- const globalTransitions = [];
183
-
184
- if (container.hasAttribute("r-transition-stagger-in")) {
185
- const transition = parseJSON(
186
- container.getAttribute("r-transition-stagger-in"),
187
- );
188
- const children = Array.from(container.children);
189
- if (children.length > 0) {
190
- globalTransitions.push({
191
- type: "stagger",
192
- elements: children,
193
- transition,
194
- });
195
- gsap.set(children, transition.from || {});
196
- }
197
- container.removeAttribute("r-transition-stagger-in");
198
- }
199
-
200
- if (container.hasAttribute("r-transition-in")) {
201
- const transition = parseJSON(container.getAttribute("r-transition-in"));
202
- globalTransitions.push({
203
- type: "individual",
204
- elements: container,
205
- transition,
206
- });
207
- gsap.set(container, transition.from || {});
208
- container.removeAttribute("r-transition-in");
209
- }
210
-
211
- return globalTransitions;
212
- }
213
-
214
- function runGlobalTransitions(globalTransitions) {
215
- for (const { elements, transition } of globalTransitions) {
216
- playTransition(elements, transition);
217
- }
218
- }
219
-
220
- async function hydrateNestedTemplates(root, isInitialLoad = false) {
221
- const els = root.querySelectorAll("[r-tpl]");
222
- for (const el of els) {
223
- const name = el.getAttribute("r-tpl");
224
- const payload = el.getAttribute("r-tpl-props");
225
- const dataset = el.getAttribute("r-dataset");
226
-
227
- let data = payload ? parseJSON(payload) : {};
228
-
229
- if (dataset) {
230
- data = { ...data, dataset };
231
- }
232
-
233
- const html = await fetch(`/templates/${name}`, {
234
- method: "POST",
235
- headers: { "Content-Type": "application/json" },
236
- body: JSON.stringify(data),
237
- }).then((r) => r.text());
238
-
239
- el.innerHTML = html;
240
-
241
- await processModules(el);
242
- await hydrateNestedTemplates(el, isInitialLoad);
243
- await processElementTransitions(el, isInitialLoad);
244
- }
245
- }
246
-
247
- function parseQuery(search) {
248
- const params = {};
249
- const urlParams = new URLSearchParams(search);
250
- for (const [key, value] of urlParams) {
251
- params[key] = value;
252
- }
253
- return params;
254
- }
255
-
256
- export function createRouter(routes) {
257
- let currentPath = null;
258
-
259
- async function navigate({
260
- path,
261
- initial = false,
262
- linkProps = null,
263
- linkTarget = null,
264
- linkDataset = null,
265
- }) {
266
- const [pathname, search = ""] = path.split("?");
267
- const matched = matchRoute(pathname, routes);
268
- if (!matched) return console.error("rift: route not found:", pathname);
269
-
270
- const { route, params } = matched;
271
- const query = parseQuery(search);
272
-
273
- if (initial) document.body.classList.add("loading");
274
-
275
- let renders;
276
-
277
- if (route.renders) {
278
- renders = route.renders;
279
- } else {
280
- const parentRoutes = findParentRoutes(path, routes);
281
- console.log("parentRoutes for", path, ":", parentRoutes);
282
- const currentRender = {
283
- template: route.template,
284
- props: route.props,
285
- target: route.target,
286
- };
287
-
288
- const filteredParents = parentRoutes.filter(
289
- ({ route: parentRoute }) =>
290
- !currentPath ||
291
- (!currentPath.startsWith(`${parentRoute.path}/`) &&
292
- currentPath !== parentRoute.path),
293
- );
294
-
295
- renders = [
296
- ...filteredParents.map(
297
- ({ route: parentRoute, params: parentParams }) => ({
298
- template: parentRoute.template,
299
- props: parentRoute.props,
300
- target: parentRoute.target,
301
- dataset: parentRoute.dataset,
302
- params: parentParams,
303
- skipTransition: false,
304
- }),
305
- ),
306
- ...parentRoutes
307
- .filter(({ route: parentRoute }) =>
308
- currentPath?.startsWith(`${parentRoute.path}/`),
309
- )
310
- .map(({ route: parentRoute, params: parentParams }) => ({
311
- template: parentRoute.template,
312
- props: parentRoute.props,
313
- target: parentRoute.target,
314
- dataset: parentRoute.dataset,
315
- params: parentParams,
316
- skipTransition: true,
317
- })),
318
- { ...currentRender, skipTransition: false },
319
- ];
320
- console.log("renders:", renders);
321
- }
322
-
323
- for (const render of renders) {
324
- const renderParams = render.params || params;
325
- let props =
326
- typeof render.props === "function"
327
- ? render.props(renderParams, query)
328
- : render.props;
329
-
330
- if (linkProps) props = { ...props, ...linkProps };
331
-
332
- const target =
333
- (linkTarget && document.querySelector(linkTarget)) ||
334
- (render.target && document.querySelector(render.target)) ||
335
- document.getElementById("main");
336
-
337
- const dataset = linkDataset || render.dataset || route.dataset;
338
-
339
- if (target.id !== "main") {
340
- // const mainContainer = document.getElementById("main");
341
- // gsap.killTweensOf(mainContainer);
342
- // gsap.set(mainContainer, { clearProps: "all" });
343
- }
344
-
345
- gsap.killTweensOf(target);
346
- gsap.set(target, { clearProps: "all" });
347
-
348
- target.classList.add("loading");
349
-
350
- await renderServerTemplate(
351
- render.template,
352
- props,
353
- target,
354
- dataset,
355
- initial || render.skipTransition,
356
- );
357
-
358
- target.classList.remove("loading");
359
- }
360
-
361
- if (initial) document.body.classList.remove("loading");
362
-
363
- currentPath = path;
364
- initial = false;
365
- window.dispatchEvent(new CustomEvent("navigate"));
366
- }
367
-
368
- function init() {
369
- document.addEventListener("click", (e) => {
370
- const link = e.target.closest("[r-link]");
371
- if (!link) {
372
- return;
373
- }
374
- e.preventDefault();
375
- const path = link.getAttribute("href");
376
- const linkDataset = link.getAttribute("r-dataset");
377
- const linkDatasetProps = link.getAttribute("r-dataset-props");
378
- const linkTarget = link.getAttribute("r-target");
379
-
380
- history.pushState({}, "", path);
381
- window.dispatchEvent(new CustomEvent("navigate"));
382
- navigate({
383
- path,
384
- linkProps: linkDatasetProps ? parseJSON(linkDatasetProps) : null,
385
- linkDataset,
386
- linkTarget,
387
- });
388
- });
389
-
390
- window.addEventListener("popstate", () =>
391
- navigate({ path: location.pathname + location.search }),
392
- );
393
- }
394
-
395
- return {
396
- navigate,
397
- init,
398
- hydrateNestedTemplates,
399
- processElementTransitions,
400
- captureGlobalTransitions,
401
- runGlobalTransitions,
402
- refresh: async (options = {}) => {
403
- const doRefresh = async () => {
404
- const { path = location.pathname + location.search, parents = false } =
405
- options;
406
- const [pathname, search = ""] = path.split("?");
407
- const matched = matchRoute(pathname, routes);
408
- if (!matched) return console.error("rift: route not found:", pathname);
409
-
410
- const { route, params } = matched;
411
- const query = parseQuery(search);
412
-
413
- if (parents) {
414
- const parentRoutes = findParentRoutes(path, routes);
415
- const allRoutes = [...parentRoutes, { route, params }];
416
-
417
- const usedTargets = new Set();
418
- for (let i = allRoutes.length - 1; i >= 0; i--) {
419
- const currentRoute = allRoutes[i].route;
420
- const target = currentRoute.target || "#main";
421
-
422
- if (!usedTargets.has(target)) {
423
- usedTargets.add(target);
424
- if (i < allRoutes.length - 1) {
425
- const { route: parentRoute, params: parentParams } =
426
- allRoutes[i];
427
- const targetEl =
428
- document.querySelector(target) ||
429
- document.getElementById("main");
430
- const props =
431
- typeof parentRoute.props === "function"
432
- ? parentRoute.props(parentParams, query)
433
- : parentRoute.props;
434
- await renderServerTemplate(
435
- parentRoute.template,
436
- props,
437
- targetEl,
438
- parentRoute.dataset,
439
- true,
440
- );
441
- }
442
- }
443
- }
444
- }
445
-
446
- const target =
447
- document.querySelector(route.target) ||
448
- document.getElementById("main");
449
- const props =
450
- typeof route.props === "function"
451
- ? route.props(params, query)
452
- : route.props;
453
- await renderServerTemplate(
454
- route.template,
455
- props,
456
- target,
457
- route.dataset,
458
- true,
459
- );
460
- };
461
-
462
- if (document.startViewTransition) {
463
- document.startViewTransition(doRefresh);
464
- } else {
465
- await doRefresh();
466
- }
467
- },
468
- refreshTemplate: async (templateName, props = {}) => {
469
- const refreshTasks = [];
470
-
471
- // find all r-tpl elements with matching name
472
- const elements = document.querySelectorAll(`[r-tpl="${templateName}"]`);
473
- elements.forEach((element) => {
474
- refreshTasks.push(async () => {
475
- const response = await fetch(`/templates/${templateName}`, {
476
- method: "POST",
477
- headers: { "Content-Type": "application/json" },
478
- body: JSON.stringify(props),
479
- });
480
- if (!response.ok)
481
- return console.error("failed to fetch template:", templateName);
482
- const html = await response.text();
483
- element.innerHTML = html;
484
- await processModules(element);
485
- });
486
- });
487
-
488
- // find any routes with matching template name
489
- const matchingRoutes = routes.filter(
490
- (route) => route.template === templateName,
491
- );
492
- matchingRoutes.forEach((route) => {
493
- const target =
494
- document.querySelector(route.target) ||
495
- document.getElementById("main");
496
- if (target) {
497
- refreshTasks.push(async () => {
498
- const currentPath = location.pathname + location.search;
499
- const matched = matchRoute(currentPath, [route]);
500
- if (matched) {
501
- const { params } = matched;
502
- const [, search = ""] = currentPath.split("?");
503
- const query = parseQuery(search);
504
- const props =
505
- typeof route.props === "function"
506
- ? route.props(params, query)
507
- : route.props;
508
- await renderServerTemplate(
509
- route.template,
510
- props,
511
- target,
512
- route.dataset,
513
- true,
514
- );
515
- }
516
- });
517
- }
518
- });
519
-
520
- if (refreshTasks.length === 0) {
521
- return console.error("no templates found with name:", templateName);
522
- }
523
-
524
- const doRefresh = async () => {
525
- await Promise.all(refreshTasks.map((task) => task()));
526
- };
527
-
528
- if (document.startViewTransition) {
529
- document.startViewTransition(doRefresh);
530
- } else {
531
- await doRefresh();
532
- }
533
- },
534
- };
535
- }