@thoughtbot/superglue 0.53.4 → 1.0.0

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.
@@ -0,0 +1,1178 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // lib/index.tsx
31
+ var lib_exports = {};
32
+ __export(lib_exports, {
33
+ Application: () => Application,
34
+ GRAFTING_ERROR: () => GRAFTING_ERROR,
35
+ GRAFTING_SUCCESS: () => GRAFTING_SUCCESS,
36
+ NavigationContext: () => NavigationContext,
37
+ NavigationProvider: () => NavigationProvider,
38
+ beforeFetch: () => beforeFetch,
39
+ beforeRemote: () => beforeRemote,
40
+ beforeVisit: () => beforeVisit,
41
+ copyPage: () => copyPage,
42
+ getIn: () => getIn,
43
+ pageReducer: () => pageReducer,
44
+ prepareStore: () => prepareStore,
45
+ removePage: () => removePage,
46
+ rootReducer: () => rootReducer,
47
+ saveAndProcessPage: () => saveAndProcessPage,
48
+ saveResponse: () => saveResponse,
49
+ setup: () => setup,
50
+ superglueReducer: () => superglueReducer,
51
+ updateFragments: () => updateFragments,
52
+ urlToPageKey: () => urlToPageKey,
53
+ useContent: () => useContent,
54
+ useSuperglue: () => useSuperglue
55
+ });
56
+ module.exports = __toCommonJS(lib_exports);
57
+ var import_react2 = __toESM(require("react"));
58
+ var import_url_parse4 = __toESM(require("url-parse"));
59
+
60
+ // lib/config.ts
61
+ var config = {
62
+ baseUrl: "",
63
+ maxPages: 20
64
+ };
65
+
66
+ // lib/utils/url.ts
67
+ var import_url_parse = __toESM(require("url-parse"));
68
+ function pathQuery(url) {
69
+ const { pathname, query } = new import_url_parse.default(url, {});
70
+ return pathname + query;
71
+ }
72
+ function pathQueryHash(url) {
73
+ const { pathname, query, hash } = new import_url_parse.default(url, {});
74
+ return pathname + query + hash;
75
+ }
76
+ function propsAtParam(url) {
77
+ const parsed = new import_url_parse.default(url, {}, true);
78
+ const query = parsed.query;
79
+ return query["props_at"];
80
+ }
81
+ function withFormatJson(url) {
82
+ const parsed = new import_url_parse.default(url, {}, true);
83
+ parsed.query["format"] = "json";
84
+ return parsed.toString();
85
+ }
86
+ function pathWithoutBZParams(url) {
87
+ const parsed = new import_url_parse.default(url, {}, true);
88
+ const query = parsed.query;
89
+ delete query["props_at"];
90
+ delete query["format"];
91
+ parsed.set("query", query);
92
+ return pathQueryHash(parsed.toString());
93
+ }
94
+ function urlToPageKey(url) {
95
+ const parsed = new import_url_parse.default(url, {}, true);
96
+ const query = parsed.query;
97
+ delete query["props_at"];
98
+ delete query["format"];
99
+ parsed.set("query", query);
100
+ return pathQuery(parsed.toString());
101
+ }
102
+ function withoutHash(url) {
103
+ const parsed = new import_url_parse.default(url, {}, true);
104
+ parsed.set("hash", "");
105
+ return parsed.toString();
106
+ }
107
+ function withoutBusters(url) {
108
+ const parsed = new import_url_parse.default(url, {}, true);
109
+ const query = parsed.query;
110
+ delete query["format"];
111
+ parsed.set("query", query);
112
+ return pathQuery(parsed.toString());
113
+ }
114
+ function formatForXHR(url) {
115
+ const formats = [withoutHash, withFormatJson];
116
+ return formats.reduce((memo, f) => f(memo), url);
117
+ }
118
+ function parsePageKey(pageKey) {
119
+ const { pathname, query } = new import_url_parse.default(pageKey, {}, true);
120
+ return {
121
+ pathname,
122
+ search: query
123
+ };
124
+ }
125
+
126
+ // lib/utils/helpers.ts
127
+ function argsForHistory(path) {
128
+ const pageKey = urlToPageKey(path);
129
+ return [
130
+ path,
131
+ {
132
+ superglue: true,
133
+ pageKey,
134
+ posX: 0,
135
+ posY: 0
136
+ }
137
+ ];
138
+ }
139
+
140
+ // lib/utils/immutability.ts
141
+ var canLookAhead = /^[\da-zA-Z\-_]+=[\da-zA-Z\-_]+$/;
142
+ var KeyPathError = class extends Error {
143
+ constructor(message) {
144
+ super(message);
145
+ this.name = "KeyPathError";
146
+ }
147
+ };
148
+ function getIn(node, path) {
149
+ const keyPath = normalizeKeyPath(path);
150
+ let result = node;
151
+ let i;
152
+ for (i = 0; i < keyPath.length; i++) {
153
+ const key = keyPath[i];
154
+ if (typeof result === "object" && result !== null) {
155
+ if (!Array.isArray(result) && canLookAhead.test(key)) {
156
+ throw new KeyPathError(
157
+ `Expected to find an Array when using the key: ${key}`
158
+ );
159
+ }
160
+ result = atKey(result, key);
161
+ } else {
162
+ throw new KeyPathError(
163
+ `Expected to traverse an Array or Obj, got ${JSON.stringify(result)}`
164
+ );
165
+ }
166
+ }
167
+ if (i === keyPath.length) {
168
+ return result;
169
+ } else {
170
+ return void 0;
171
+ }
172
+ }
173
+ function clone(node) {
174
+ return Array.isArray(node) ? [].slice.call(node) : { ...node };
175
+ }
176
+ function getKey(node, key) {
177
+ if (Array.isArray(node) && Number.isNaN(Number(key))) {
178
+ const key_parts = Array.from(key.split("="));
179
+ const attr = key_parts[0];
180
+ const id = key_parts[1];
181
+ if (!id || !attr) {
182
+ return key;
183
+ }
184
+ let i;
185
+ let child;
186
+ for (i = 0; i < node.length; i++) {
187
+ child = node[i];
188
+ if (typeof child === "object" && !Array.isArray(child) && child !== null) {
189
+ const val = child[attr];
190
+ if (val && val.toString() === id) {
191
+ break;
192
+ }
193
+ } else {
194
+ throw new KeyPathError(`Could not look ahead ${key} at ${child}`);
195
+ }
196
+ }
197
+ if (i === node.length) {
198
+ throw new KeyPathError(`Could not find ${key} while looking ahead`);
199
+ }
200
+ return i;
201
+ } else {
202
+ return key;
203
+ }
204
+ }
205
+ function atKey(node, key) {
206
+ const actualKey = getKey(node, key);
207
+ if (Array.isArray(node)) {
208
+ return node[actualKey];
209
+ } else {
210
+ return node[actualKey];
211
+ }
212
+ }
213
+ function normalizeKeyPath(path) {
214
+ if (typeof path === "string") {
215
+ path = path.replace(/ /g, "");
216
+ if (path === "") {
217
+ return [];
218
+ }
219
+ return path.split(".");
220
+ } else {
221
+ return path;
222
+ }
223
+ }
224
+ function setIn(object, path, value) {
225
+ const keypath = normalizeKeyPath(path);
226
+ const results = { 0: object };
227
+ const parents = { 0: object };
228
+ let i;
229
+ for (i = 0; i < keypath.length; i++) {
230
+ const parent = parents[i];
231
+ if (!(typeof parent === "object" && parent !== null)) {
232
+ throw new KeyPathError(
233
+ `Expected to traverse an Array or Obj, got ${JSON.stringify(parent)}`
234
+ );
235
+ }
236
+ const child = atKey(parent, keypath[i]);
237
+ parents[i + 1] = child;
238
+ }
239
+ results[keypath.length] = value;
240
+ for (i = keypath.length - 1; i >= 0; i--) {
241
+ const target = clone(parents[i]);
242
+ results[i] = target;
243
+ const key = getKey(results[i], keypath[i]);
244
+ if (Array.isArray(target)) {
245
+ target[key] = results[i + 1];
246
+ } else {
247
+ target[key] = results[i + 1];
248
+ }
249
+ }
250
+ return results[0];
251
+ }
252
+
253
+ // lib/utils/request.ts
254
+ var import_url_parse2 = __toESM(require("url-parse"));
255
+ function isValidResponse(xhr) {
256
+ return isValidContent(xhr) && !downloadingFile(xhr);
257
+ }
258
+ function isValidContent(rsp) {
259
+ const contentType = rsp.headers.get("content-type");
260
+ const jsContent = /^(?:application\/json)(?:;|$)/;
261
+ return !!(contentType && contentType.match(jsContent));
262
+ }
263
+ function downloadingFile(xhr) {
264
+ const disposition = xhr.headers.get("content-disposition");
265
+ return !!(disposition && disposition.match(/^attachment/) !== null);
266
+ }
267
+ var SuperglueResponseError = class extends Error {
268
+ constructor(message) {
269
+ super(message);
270
+ this.name = "SuperglueResponseError";
271
+ }
272
+ };
273
+ function validateResponse(args) {
274
+ const { rsp } = args;
275
+ if (isValidResponse(rsp)) {
276
+ return args;
277
+ } else {
278
+ const error = new SuperglueResponseError("Invalid Superglue Response");
279
+ error.response = rsp;
280
+ throw error;
281
+ }
282
+ }
283
+ function handleServerErrors(args) {
284
+ const { rsp } = args;
285
+ if (!rsp.ok && rsp.status !== 422) {
286
+ if (rsp.status === 406) {
287
+ console.error(
288
+ "Superglue encountered a 406 Not Acceptable response. This can happen if you used respond_to and didn't specify format.json in the block. Try adding it to your respond_to. For example:\n\nrespond_to do |format|\n format.html\n format.json\n format.csv\nend"
289
+ );
290
+ }
291
+ const error = new SuperglueResponseError(rsp.statusText);
292
+ error.response = rsp;
293
+ throw error;
294
+ }
295
+ return args;
296
+ }
297
+ function argsForFetch(getState, pathQuery2, {
298
+ method = "GET",
299
+ headers = {},
300
+ body = "",
301
+ signal,
302
+ ...rest
303
+ } = {}) {
304
+ method = method.toUpperCase();
305
+ const currentState = getState().superglue;
306
+ const nextHeaders = { ...headers };
307
+ nextHeaders["x-requested-with"] = "XMLHttpRequest";
308
+ nextHeaders["accept"] = "application/json";
309
+ nextHeaders["x-superglue-request"] = "true";
310
+ if (method != "GET" && method != "HEAD") {
311
+ nextHeaders["content-type"] = "application/json";
312
+ }
313
+ if (body instanceof FormData) {
314
+ delete nextHeaders["content-type"];
315
+ }
316
+ if (currentState.csrfToken) {
317
+ nextHeaders["x-csrf-token"] = currentState.csrfToken;
318
+ }
319
+ const fetchPath = new import_url_parse2.default(
320
+ formatForXHR(pathQuery2),
321
+ config.baseUrl || {},
322
+ true
323
+ );
324
+ const credentials = "same-origin";
325
+ if (!(method == "GET" || method == "HEAD")) {
326
+ nextHeaders["x-http-method-override"] = method;
327
+ method = "POST";
328
+ }
329
+ const options = {
330
+ method,
331
+ headers: nextHeaders,
332
+ body,
333
+ credentials,
334
+ signal
335
+ };
336
+ if (currentState.currentPageKey) {
337
+ const referrer = new import_url_parse2.default(
338
+ currentState.currentPageKey,
339
+ config.baseUrl || {},
340
+ false
341
+ ).href;
342
+ options.referrer = referrer;
343
+ }
344
+ if (method == "GET" || method == "HEAD") {
345
+ if (options.body instanceof FormData) {
346
+ const allData = new URLSearchParams(
347
+ options.body
348
+ );
349
+ const nextQuery = { ...fetchPath.query, ...Object.fromEntries(allData) };
350
+ fetchPath.set("query", nextQuery);
351
+ }
352
+ delete options.body;
353
+ }
354
+ return [fetchPath.toString(), { ...options, ...rest }];
355
+ }
356
+ function extractJSON(rsp) {
357
+ return rsp.json().then((json) => {
358
+ return { rsp, json };
359
+ }).catch((e) => {
360
+ e.response = rsp;
361
+ throw e;
362
+ });
363
+ }
364
+ function parseResponse(prm) {
365
+ return Promise.resolve(prm).then(extractJSON).then(handleServerErrors).then(validateResponse);
366
+ }
367
+
368
+ // lib/utils/ujs.ts
369
+ var HandlerBuilder = class {
370
+ constructor({
371
+ ujsAttributePrefix,
372
+ visit,
373
+ remote: remote2,
374
+ store
375
+ }) {
376
+ this.attributePrefix = ujsAttributePrefix;
377
+ this.isUJS = this.isUJS.bind(this);
378
+ this.store = store;
379
+ this.handleSubmit = this.handleSubmit.bind(this);
380
+ this.handleClick = this.handleClick.bind(this);
381
+ this.visit = visit;
382
+ this.remote = remote2;
383
+ this.visitOrRemote = this.visitOrRemote.bind(this);
384
+ }
385
+ retrieveLink(target) {
386
+ const link = target.closest("a");
387
+ if (link && link.href.length !== 0) {
388
+ return link;
389
+ }
390
+ }
391
+ isNonStandardClick(event) {
392
+ return event.button > 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey;
393
+ }
394
+ isUJS(node) {
395
+ const hasVisit = !!node.getAttribute(this.attributePrefix + "-visit");
396
+ const hasRemote = !!node.getAttribute(this.attributePrefix + "-remote");
397
+ return hasVisit || hasRemote;
398
+ }
399
+ handleSubmit(event) {
400
+ const form = event.target;
401
+ if (!(form instanceof HTMLFormElement)) {
402
+ return;
403
+ }
404
+ if (!this.isUJS(form)) {
405
+ return;
406
+ }
407
+ event.preventDefault();
408
+ let url = form.getAttribute("action");
409
+ if (!url) {
410
+ return;
411
+ }
412
+ const method = (form.getAttribute("method") || "POST").toUpperCase();
413
+ url = withoutBusters(url);
414
+ this.visitOrRemote(form, url, {
415
+ method,
416
+ body: new FormData(form)
417
+ });
418
+ }
419
+ handleClick(event) {
420
+ if (!(event.target instanceof Element)) {
421
+ return;
422
+ }
423
+ const link = this.retrieveLink(event.target);
424
+ const isNonStandard = this.isNonStandardClick(event);
425
+ if (!link || isNonStandard || !this.isUJS(link)) {
426
+ return;
427
+ }
428
+ event.preventDefault();
429
+ let url = link.getAttribute("href");
430
+ if (!url) {
431
+ return;
432
+ }
433
+ url = withoutBusters(url);
434
+ this.visitOrRemote(link, url, { method: "GET" });
435
+ }
436
+ visitOrRemote(linkOrForm, url, opts) {
437
+ const dataset = { ...linkOrForm.dataset };
438
+ if (linkOrForm.getAttribute(this.attributePrefix + "-visit")) {
439
+ this.visit(url, { ...opts, dataset });
440
+ }
441
+ if (linkOrForm.getAttribute(this.attributePrefix + "-remote")) {
442
+ const { currentPageKey } = this.store.getState().superglue;
443
+ this.remote(url, {
444
+ ...opts,
445
+ pageKey: currentPageKey,
446
+ dataset
447
+ });
448
+ }
449
+ }
450
+ handlers() {
451
+ return {
452
+ onClick: this.handleClick,
453
+ onSubmit: this.handleSubmit
454
+ };
455
+ }
456
+ };
457
+ var ujsHandlers = ({
458
+ ujsAttributePrefix,
459
+ visit,
460
+ remote: remote2,
461
+ store
462
+ }) => {
463
+ const builder = new HandlerBuilder({
464
+ visit,
465
+ remote: remote2,
466
+ ujsAttributePrefix,
467
+ store
468
+ });
469
+ return builder.handlers();
470
+ };
471
+
472
+ // lib/utils/window.ts
473
+ function needsRefresh(prevAssets, newAssets) {
474
+ if (prevAssets && newAssets) {
475
+ const hasNewAssets = !newAssets.every((asset) => prevAssets.includes(asset));
476
+ return hasNewAssets;
477
+ } else {
478
+ return false;
479
+ }
480
+ }
481
+
482
+ // lib/action_creators/index.ts
483
+ var import_url_parse3 = __toESM(require("url-parse"));
484
+
485
+ // lib/actions.ts
486
+ var import_toolkit = require("@reduxjs/toolkit");
487
+ var GRAFTING_ERROR = "@@superglue/GRAFTING_ERROR";
488
+ var GRAFTING_SUCCESS = "@@superglue/GRAFTING_SUCCESS";
489
+ var saveResponse = (0, import_toolkit.createAction)(
490
+ "@@superglue/SAVE_RESPONSE",
491
+ ({ pageKey, page }) => {
492
+ pageKey = urlToPageKey(pageKey);
493
+ return {
494
+ payload: {
495
+ pageKey,
496
+ page
497
+ }
498
+ };
499
+ }
500
+ );
501
+ var handleGraft = (0, import_toolkit.createAction)(
502
+ "@@superglue/HANDLE_GRAFT",
503
+ ({ pageKey, page }) => {
504
+ pageKey = urlToPageKey(pageKey);
505
+ return {
506
+ payload: {
507
+ page,
508
+ pageKey
509
+ }
510
+ };
511
+ }
512
+ );
513
+ var superglueError = (0, import_toolkit.createAction)(
514
+ "@@superglue/ERROR"
515
+ );
516
+ var updateFragments = (0, import_toolkit.createAction)("@@superglue/UPDATE_FRAGMENTS");
517
+ var copyPage = (0, import_toolkit.createAction)(
518
+ "@@superglue/COPY_PAGE"
519
+ );
520
+ var removePage = (0, import_toolkit.createAction)(
521
+ "@@superglue/REMOVE_PAGE"
522
+ );
523
+ var beforeFetch = (0, import_toolkit.createAction)(
524
+ "@@superglue/BEFORE_FETCH"
525
+ );
526
+ var beforeVisit = (0, import_toolkit.createAction)("@@superglue/BEFORE_VISIT");
527
+ var beforeRemote = (0, import_toolkit.createAction)("@@superglue/BEFORE_REMOTE");
528
+ var setCSRFToken = (0, import_toolkit.createAction)("@@superglue/SET_CSRF_TOKEN");
529
+ var historyChange = (0, import_toolkit.createAction)("@@superglue/HISTORY_CHANGE");
530
+ var setActivePage = (0, import_toolkit.createAction)("@@superglue/SET_ACTIVE_PAGE");
531
+
532
+ // lib/action_creators/requests.ts
533
+ function handleFetchErr(err, fetchArgs, dispatch) {
534
+ dispatch(superglueError({ message: err.message }));
535
+ throw err;
536
+ }
537
+ function buildMeta(pageKey, page, state, rsp, fetchArgs) {
538
+ const { assets: prevAssets } = state;
539
+ const { assets: nextAssets } = page;
540
+ return {
541
+ pageKey,
542
+ page,
543
+ redirected: rsp.redirected,
544
+ rsp,
545
+ fetchArgs,
546
+ componentIdentifier: page.componentIdentifier,
547
+ needsRefresh: needsRefresh(prevAssets, nextAssets)
548
+ };
549
+ }
550
+ var MismatchedComponentError = class extends Error {
551
+ constructor(message) {
552
+ super(message);
553
+ this.name = "MismatchedComponentError";
554
+ }
555
+ };
556
+ var remote = (path, {
557
+ pageKey: targetPageKey,
558
+ force = false,
559
+ beforeSave = (prevPage, receivedPage) => receivedPage,
560
+ ...rest
561
+ } = {}) => {
562
+ path = withoutBusters(path);
563
+ targetPageKey = targetPageKey && urlToPageKey(targetPageKey);
564
+ return (dispatch, getState) => {
565
+ const fetchArgs = argsForFetch(getState, path, rest);
566
+ const currentPageKey = getState().superglue.currentPageKey;
567
+ dispatch(beforeRemote({ currentPageKey, fetchArgs }));
568
+ dispatch(beforeFetch({ fetchArgs }));
569
+ return fetch(...fetchArgs).then(parseResponse).then(({ rsp, json }) => {
570
+ const { superglue, pages = {} } = getState();
571
+ let pageKey;
572
+ if (targetPageKey === void 0) {
573
+ const isGet = fetchArgs[1].method === "GET";
574
+ pageKey = calculatePageKey(rsp, isGet, currentPageKey);
575
+ } else {
576
+ pageKey = targetPageKey;
577
+ }
578
+ const meta = buildMeta(pageKey, json, superglue, rsp, fetchArgs);
579
+ const existingId = pages[pageKey]?.componentIdentifier;
580
+ const receivedId = json.componentIdentifier;
581
+ if (!!existingId && existingId != receivedId && !force) {
582
+ const message = `You cannot replace or update an existing page
583
+ located at pages["${currentPageKey}"] that has a componentIdentifier
584
+ of "${existingId}" with the contents of a page response that has a
585
+ componentIdentifier of "${receivedId}".
586
+
587
+ This can happen if you're using data-sg-remote or remote but your
588
+ response redirected to a page with a different componentIdentifier
589
+ than the target page.
590
+
591
+ This limitation exists because the resulting page shape from grafting
592
+ "${receivedId}"'s "${propsAtParam(path)}" into "${existingId}" may not be
593
+ compatible with the page component associated with "${existingId}".
594
+
595
+ Consider using data-sg-visit, the visit function, or redirect_back to
596
+ the same page. Or if you're sure you want to proceed, use force: true.
597
+ `;
598
+ throw new MismatchedComponentError(message);
599
+ }
600
+ const page = beforeSave(pages[pageKey], json);
601
+ return dispatch(saveAndProcessPage(pageKey, page)).then(() => meta);
602
+ }).catch((e) => handleFetchErr(e, fetchArgs, dispatch));
603
+ };
604
+ };
605
+ function calculatePageKey(rsp, isGet, currentPageKey) {
606
+ let pageKey = urlToPageKey(rsp.url);
607
+ if (!isGet && !rsp.redirected) {
608
+ pageKey = currentPageKey;
609
+ }
610
+ const contentLocation = rsp.headers.get("content-location");
611
+ if (contentLocation) {
612
+ pageKey = urlToPageKey(contentLocation);
613
+ }
614
+ return pageKey;
615
+ }
616
+
617
+ // lib/action_creators/index.ts
618
+ function fetchDeferments(pageKey, defers = []) {
619
+ pageKey = urlToPageKey(pageKey);
620
+ return (dispatch) => {
621
+ const fetches = defers.filter(({ type }) => type === "auto").map(function({
622
+ url,
623
+ successAction = GRAFTING_SUCCESS,
624
+ failAction = GRAFTING_ERROR
625
+ }) {
626
+ const parsedUrl = new import_url_parse3.default(url, true);
627
+ const keyPath = parsedUrl.query.props_at;
628
+ return dispatch(remote(url, { pageKey })).then(() => {
629
+ dispatch({
630
+ type: successAction,
631
+ payload: {
632
+ pageKey,
633
+ keyPath
634
+ }
635
+ });
636
+ }).catch((err) => {
637
+ dispatch({
638
+ type: failAction,
639
+ payload: {
640
+ url,
641
+ err,
642
+ pageKey,
643
+ keyPath
644
+ }
645
+ });
646
+ });
647
+ });
648
+ return Promise.all(fetches);
649
+ };
650
+ }
651
+ function saveAndProcessPage(pageKey, page) {
652
+ return (dispatch, getState) => {
653
+ pageKey = urlToPageKey(pageKey);
654
+ const { defers = [] } = page;
655
+ if ("action" in page) {
656
+ const prevPage = getState().pages[pageKey];
657
+ dispatch(handleGraft({ pageKey, page }));
658
+ const currentPage = getState().pages[pageKey];
659
+ currentPage.fragments.forEach((fragment) => {
660
+ const { type, path } = fragment;
661
+ const currentFragment = getIn(currentPage, path);
662
+ const prevFragment = getIn(prevPage, path);
663
+ if (!prevFragment) {
664
+ dispatch(
665
+ updateFragments({
666
+ name: type,
667
+ pageKey,
668
+ value: currentFragment,
669
+ path
670
+ })
671
+ );
672
+ } else if (currentFragment !== prevFragment) {
673
+ dispatch(
674
+ updateFragments({
675
+ name: type,
676
+ pageKey,
677
+ value: currentFragment,
678
+ previousValue: prevFragment,
679
+ path
680
+ })
681
+ );
682
+ }
683
+ });
684
+ } else {
685
+ dispatch(saveResponse({ pageKey, page }));
686
+ const currentPage = getState().pages[pageKey];
687
+ currentPage.fragments.forEach((fragment) => {
688
+ const { type, path } = fragment;
689
+ const currentFragment = getIn(currentPage, path);
690
+ dispatch(
691
+ updateFragments({
692
+ name: type,
693
+ pageKey,
694
+ value: currentFragment,
695
+ path
696
+ })
697
+ );
698
+ });
699
+ }
700
+ const hasFetch = typeof fetch != "undefined";
701
+ if (hasFetch) {
702
+ return dispatch(fetchDeferments(pageKey, defers)).then(
703
+ () => Promise.resolve()
704
+ );
705
+ } else {
706
+ return Promise.resolve();
707
+ }
708
+ };
709
+ }
710
+
711
+ // lib/index.tsx
712
+ var import_react_redux3 = require("react-redux");
713
+ var import_history = require("history");
714
+
715
+ // lib/components/Navigation.tsx
716
+ var import_react = __toESM(require("react"));
717
+ var import_react_redux = require("react-redux");
718
+ var NavigationContext = (0, import_react.createContext)(
719
+ {}
720
+ );
721
+ var hasWindow = typeof window !== "undefined";
722
+ var setWindowScroll = (posX, posY) => {
723
+ hasWindow && window.scrollTo(posX, posY);
724
+ };
725
+ var notFound = (identifier) => {
726
+ let reminder = "";
727
+ if (!identifier) {
728
+ reminder = "Did you forget to add `json.componentIdentifier` in your application.json.props layout?";
729
+ }
730
+ const error = new Error(
731
+ `Superglue Nav component was looking for ${identifier} but could not find it in your mapping. ${reminder}`
732
+ );
733
+ throw error;
734
+ };
735
+ var NavigationProvider = (0, import_react.forwardRef)(function NavigationProvider2({ history, visit, remote: remote2, mapping }, ref) {
736
+ const dispatch = (0, import_react_redux.useDispatch)();
737
+ const pages = (0, import_react_redux.useSelector)((state) => state.pages);
738
+ const superglue = (0, import_react_redux.useSelector)(
739
+ (state) => state.superglue
740
+ );
741
+ const store = (0, import_react_redux.useStore)();
742
+ (0, import_react.useEffect)(() => {
743
+ return history.listen(onHistoryChange);
744
+ }, []);
745
+ (0, import_react.useImperativeHandle)(
746
+ ref,
747
+ () => {
748
+ return {
749
+ navigateTo
750
+ };
751
+ },
752
+ []
753
+ );
754
+ const visitAndRestore = (pageKey, posX, posY) => {
755
+ return visit(pageKey, { revisit: true }).then((meta) => {
756
+ if (meta) {
757
+ if (meta.navigationAction === "none") {
758
+ dispatch(setActivePage({ pageKey }));
759
+ setWindowScroll(posX, posY);
760
+ }
761
+ } else {
762
+ console.warn(
763
+ `scoll restoration was skipped. Your visit's then funtion
764
+ should return the meta object it recieved if you want your
765
+ application to restore the page's previous scroll.`
766
+ );
767
+ }
768
+ });
769
+ };
770
+ const onHistoryChange = ({ location, action }) => {
771
+ const state = location.state;
772
+ if (!state && location.hash !== "" && action === "POP") {
773
+ const nextPageKey = urlToPageKey(location.pathname + location.search);
774
+ const containsKey = !!pages[nextPageKey];
775
+ if (containsKey) {
776
+ history.replace(
777
+ {
778
+ pathname: location.pathname,
779
+ search: location.search,
780
+ hash: location.hash
781
+ },
782
+ {
783
+ pageKey: nextPageKey,
784
+ superglue: true,
785
+ posY: window.pageYOffset,
786
+ posX: window.pageXOffset
787
+ }
788
+ );
789
+ }
790
+ }
791
+ if (state && "superglue" in state) {
792
+ dispatch(
793
+ historyChange({
794
+ pageKey: state.pageKey
795
+ })
796
+ );
797
+ if (action !== "POP") {
798
+ return;
799
+ }
800
+ const { pageKey, posX, posY } = state;
801
+ const containsKey = !!pages[pageKey];
802
+ if (containsKey) {
803
+ const { restoreStrategy } = pages[pageKey];
804
+ switch (restoreStrategy) {
805
+ case "fromCacheOnly":
806
+ dispatch(setActivePage({ pageKey }));
807
+ setWindowScroll(posX, posY);
808
+ break;
809
+ case "fromCacheAndRevisitInBackground":
810
+ dispatch(setActivePage({ pageKey }));
811
+ setWindowScroll(posX, posY);
812
+ visit(pageKey, { revisit: true });
813
+ break;
814
+ case "revisitOnly":
815
+ default:
816
+ visitAndRestore(pageKey, posX, posY);
817
+ }
818
+ } else {
819
+ visitAndRestore(pageKey, posX, posY);
820
+ }
821
+ }
822
+ };
823
+ const navigateTo = (path, { action } = {
824
+ action: "push"
825
+ }) => {
826
+ if (action === "none") {
827
+ return false;
828
+ }
829
+ path = pathWithoutBZParams(path);
830
+ const nextPageKey = urlToPageKey(path);
831
+ const hasPage = Object.prototype.hasOwnProperty.call(
832
+ store.getState().pages,
833
+ nextPageKey
834
+ );
835
+ if (hasPage) {
836
+ const location = history.location;
837
+ const state = location.state;
838
+ const prevPageKey = state.pageKey;
839
+ const historyArgs = [
840
+ path,
841
+ {
842
+ pageKey: nextPageKey,
843
+ superglue: true,
844
+ posY: 0,
845
+ posX: 0
846
+ }
847
+ ];
848
+ if (action === "push") {
849
+ if (hasWindow) {
850
+ history.replace(
851
+ {
852
+ pathname: location.pathname,
853
+ search: location.search,
854
+ hash: location.hash
855
+ },
856
+ {
857
+ ...state,
858
+ posY: window.pageYOffset,
859
+ posX: window.pageXOffset
860
+ }
861
+ );
862
+ }
863
+ history.push(...historyArgs);
864
+ }
865
+ if (action === "replace") {
866
+ history.replace(...historyArgs);
867
+ }
868
+ setActivePage({ pageKey: nextPageKey });
869
+ setWindowScroll(0, 0);
870
+ if (action === "replace" && prevPageKey && prevPageKey !== nextPageKey) {
871
+ dispatch(removePage({ pageKey: prevPageKey }));
872
+ }
873
+ return true;
874
+ } else {
875
+ console.warn(
876
+ `\`navigateTo\` was called , but could not find
877
+ the pageKey in the store. This may happen when the wrong
878
+ content_location was set in your non-get controller action.
879
+ No navigation will take place`
880
+ );
881
+ return false;
882
+ }
883
+ };
884
+ const { currentPageKey, search } = superglue;
885
+ const { componentIdentifier } = pages[currentPageKey];
886
+ const Component = mapping[componentIdentifier];
887
+ if (Component) {
888
+ return /* @__PURE__ */ import_react.default.createElement(
889
+ NavigationContext.Provider,
890
+ {
891
+ value: { pageKey: currentPageKey, search, navigateTo, visit, remote: remote2 }
892
+ },
893
+ /* @__PURE__ */ import_react.default.createElement(Component, null)
894
+ );
895
+ } else {
896
+ notFound(componentIdentifier);
897
+ }
898
+ });
899
+
900
+ // lib/reducers/index.ts
901
+ function addPlaceholdersToDeferredNodes(existingPage, page) {
902
+ const { defers = [] } = existingPage;
903
+ const prevDefers = defers.map(({ path }) => {
904
+ const node = getIn(existingPage, path);
905
+ const copy = JSON.stringify(node);
906
+ return [path, JSON.parse(copy)];
907
+ });
908
+ return prevDefers.reduce((memo, [path, node]) => {
909
+ return setIn(page, path, node);
910
+ }, page);
911
+ }
912
+ function constrainPagesSize(state) {
913
+ const { maxPages } = config;
914
+ const allPageKeys = Object.keys(state);
915
+ const cacheTimesRecentFirst = allPageKeys.map((key) => state[key].savedAt).sort((a, b) => b - a);
916
+ for (const key of Array.from(allPageKeys)) {
917
+ if (state[key].savedAt <= cacheTimesRecentFirst[maxPages - 1]) {
918
+ delete state[key];
919
+ }
920
+ }
921
+ }
922
+ function handleSaveResponse(state, pageKey, page) {
923
+ state = { ...state };
924
+ let nextPage = {
925
+ ...page,
926
+ savedAt: Date.now()
927
+ };
928
+ const existingPage = state[pageKey];
929
+ if (existingPage) {
930
+ nextPage = addPlaceholdersToDeferredNodes(existingPage, nextPage);
931
+ }
932
+ constrainPagesSize(state);
933
+ state[pageKey] = nextPage;
934
+ return state;
935
+ }
936
+ function appendReceivedFragmentsOntoPage(state, pageKey, receivedFragments) {
937
+ if (!pageKey) {
938
+ return state;
939
+ }
940
+ if (receivedFragments.length === 0) {
941
+ return state;
942
+ }
943
+ const currentPage = state[pageKey];
944
+ const { fragments: prevFragments = [] } = currentPage;
945
+ const nextFragments = [...prevFragments];
946
+ const existingKeys = {};
947
+ prevFragments.forEach((frag) => existingKeys[frag.path] = true);
948
+ receivedFragments.forEach((frag) => {
949
+ if (!existingKeys[frag.path]) {
950
+ nextFragments.push(frag);
951
+ }
952
+ });
953
+ const nextPage = {
954
+ ...currentPage,
955
+ fragments: nextFragments
956
+ };
957
+ const nextState = { ...state };
958
+ nextState[pageKey] = nextPage;
959
+ return nextState;
960
+ }
961
+ function graftNodeOntoPage(state, pageKey, node, pathToNode) {
962
+ if (!node) {
963
+ console.warn(
964
+ "There was no node returned in the response. Do you have the correct key path in your props_at?"
965
+ );
966
+ return state;
967
+ }
968
+ if (!pathToNode || !pageKey) {
969
+ return state;
970
+ }
971
+ const fullPathToNode = [pageKey, pathToNode].join(".");
972
+ return setIn(state, fullPathToNode, node);
973
+ }
974
+ function handleGraftResponse(state, pageKey, page) {
975
+ const currentPage = state[pageKey];
976
+ if (!currentPage) {
977
+ const error = new Error(
978
+ `Superglue was looking for ${pageKey} in your state, but could not find it in your mapping. Did you forget to pass in a valid pageKey to this.props.remote or this.props.visit?`
979
+ );
980
+ throw error;
981
+ }
982
+ const {
983
+ data: receivedNode,
984
+ path: pathToNode,
985
+ fragments: receivedFragments = []
986
+ } = page;
987
+ return [
988
+ (nextState) => graftNodeOntoPage(nextState, pageKey, receivedNode, pathToNode),
989
+ (nextState) => appendReceivedFragmentsOntoPage(nextState, pageKey, receivedFragments)
990
+ ].reduce((memo, fn) => fn(memo), state);
991
+ }
992
+ function pageReducer(state = {}, action) {
993
+ if (removePage.match(action)) {
994
+ const { pageKey } = action.payload;
995
+ const nextState = { ...state };
996
+ delete nextState[pageKey];
997
+ return nextState;
998
+ }
999
+ if (copyPage.match(action)) {
1000
+ const nextState = { ...state };
1001
+ const { from, to } = action.payload;
1002
+ nextState[urlToPageKey(to)] = JSON.parse(JSON.stringify(nextState[from]));
1003
+ return nextState;
1004
+ }
1005
+ if (handleGraft.match(action)) {
1006
+ const { pageKey, page } = action.payload;
1007
+ return handleGraftResponse(state, pageKey, page);
1008
+ }
1009
+ if (saveResponse.match(action)) {
1010
+ const { pageKey, page } = action.payload;
1011
+ const nextState = handleSaveResponse(state, pageKey, page);
1012
+ return nextState;
1013
+ }
1014
+ return state;
1015
+ }
1016
+ function superglueReducer(state = {
1017
+ currentPageKey: "",
1018
+ search: {},
1019
+ assets: []
1020
+ }, action) {
1021
+ if (setCSRFToken.match(action)) {
1022
+ const { csrfToken } = action.payload;
1023
+ return { ...state, csrfToken };
1024
+ }
1025
+ if (setActivePage.match(action)) {
1026
+ const { pageKey } = action.payload;
1027
+ const { search } = parsePageKey(pageKey);
1028
+ return {
1029
+ ...state,
1030
+ search,
1031
+ currentPageKey: pageKey
1032
+ };
1033
+ }
1034
+ if (historyChange.match(action)) {
1035
+ const { pageKey } = action.payload;
1036
+ const { search } = parsePageKey(pageKey);
1037
+ return {
1038
+ ...state,
1039
+ currentPageKey: pageKey,
1040
+ search
1041
+ };
1042
+ }
1043
+ if (saveResponse.match(action)) {
1044
+ const {
1045
+ page: { csrfToken, assets }
1046
+ } = action.payload;
1047
+ return { ...state, csrfToken, assets };
1048
+ }
1049
+ return state;
1050
+ }
1051
+ var rootReducer = {
1052
+ superglue: superglueReducer,
1053
+ pages: pageReducer
1054
+ };
1055
+
1056
+ // lib/hooks/index.ts
1057
+ var import_react_redux2 = require("react-redux");
1058
+ function useSuperglue() {
1059
+ return (0, import_react_redux2.useSelector)((state) => state.superglue);
1060
+ }
1061
+ function useContent() {
1062
+ const superglueState = useSuperglue();
1063
+ const currentPageKey = superglueState.currentPageKey;
1064
+ return (0, import_react_redux2.useSelector)(
1065
+ (state) => state.pages[currentPageKey]
1066
+ ).data;
1067
+ }
1068
+
1069
+ // lib/index.tsx
1070
+ var hasWindow2 = typeof window !== "undefined";
1071
+ var createHistory = () => {
1072
+ if (hasWindow2) {
1073
+ return (0, import_history.createBrowserHistory)({});
1074
+ } else {
1075
+ return (0, import_history.createMemoryHistory)({});
1076
+ }
1077
+ };
1078
+ var prepareStore = (store, initialPage, path) => {
1079
+ const location = (0, import_url_parse4.default)(path);
1080
+ const initialPageKey = urlToPageKey(location.href);
1081
+ const { csrfToken } = initialPage;
1082
+ store.dispatch(
1083
+ historyChange({
1084
+ pageKey: initialPageKey
1085
+ })
1086
+ );
1087
+ store.dispatch(saveAndProcessPage(initialPageKey, initialPage));
1088
+ store.dispatch(setCSRFToken({ csrfToken }));
1089
+ };
1090
+ var setup = ({
1091
+ initialPage,
1092
+ baseUrl,
1093
+ path,
1094
+ store,
1095
+ buildVisitAndRemote,
1096
+ history,
1097
+ navigatorRef
1098
+ }) => {
1099
+ config.baseUrl = baseUrl;
1100
+ const { visit, remote: remote2 } = buildVisitAndRemote(navigatorRef, store);
1101
+ const initialPageKey = urlToPageKey((0, import_url_parse4.default)(path).href);
1102
+ const nextHistory = history || createHistory();
1103
+ nextHistory.replace(...argsForHistory(path));
1104
+ prepareStore(store, initialPage, path);
1105
+ const handlers = ujsHandlers({
1106
+ visit,
1107
+ remote: remote2,
1108
+ ujsAttributePrefix: "data-sg",
1109
+ store
1110
+ });
1111
+ return {
1112
+ visit,
1113
+ remote: remote2,
1114
+ nextHistory,
1115
+ initialPageKey,
1116
+ ujs: handlers
1117
+ };
1118
+ };
1119
+ function Application({
1120
+ initialPage,
1121
+ baseUrl,
1122
+ path,
1123
+ store,
1124
+ buildVisitAndRemote,
1125
+ history,
1126
+ mapping,
1127
+ ...rest
1128
+ }) {
1129
+ const navigatorRef = (0, import_react2.useRef)(null);
1130
+ const { visit, remote: remote2, nextHistory, initialPageKey, ujs } = (0, import_react2.useMemo)(() => {
1131
+ return setup({
1132
+ initialPage,
1133
+ baseUrl,
1134
+ path,
1135
+ store,
1136
+ buildVisitAndRemote,
1137
+ history,
1138
+ navigatorRef
1139
+ });
1140
+ }, []);
1141
+ return /* @__PURE__ */ import_react2.default.createElement("div", { onClick: ujs.onClick, onSubmit: ujs.onSubmit, ...rest }, /* @__PURE__ */ import_react2.default.createElement(import_react_redux3.Provider, { store }, /* @__PURE__ */ import_react2.default.createElement(
1142
+ NavigationProvider,
1143
+ {
1144
+ ref: navigatorRef,
1145
+ visit,
1146
+ remote: remote2,
1147
+ mapping,
1148
+ history: nextHistory,
1149
+ initialPageKey
1150
+ }
1151
+ )));
1152
+ }
1153
+ // Annotate the CommonJS export names for ESM import in node:
1154
+ 0 && (module.exports = {
1155
+ Application,
1156
+ GRAFTING_ERROR,
1157
+ GRAFTING_SUCCESS,
1158
+ NavigationContext,
1159
+ NavigationProvider,
1160
+ beforeFetch,
1161
+ beforeRemote,
1162
+ beforeVisit,
1163
+ copyPage,
1164
+ getIn,
1165
+ pageReducer,
1166
+ prepareStore,
1167
+ removePage,
1168
+ rootReducer,
1169
+ saveAndProcessPage,
1170
+ saveResponse,
1171
+ setup,
1172
+ superglueReducer,
1173
+ updateFragments,
1174
+ urlToPageKey,
1175
+ useContent,
1176
+ useSuperglue
1177
+ });
1178
+ //# sourceMappingURL=superglue.cjs.map