@very-simple/ajax 1.0.0-beta.5 → 1.0.0-beta.7

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 CHANGED
@@ -87,6 +87,19 @@ start({
87
87
  })
88
88
  ```
89
89
 
90
+ `morphHeads`, `executeScripts` and `viewTransitions` also accept a callback
91
+ receiving `{ from, to, isBackForward }` and returning a boolean. Combined with
92
+ `data-ajax-state` this allows deciding per link, e.g. to skip morphing the
93
+ `<head>` when loading a partial:
94
+
95
+ ```html
96
+ <a href="/partial" data-ajax-state='{"isPartial": true}'>Partial</a>
97
+ ```
98
+
99
+ ```ts
100
+ start({ morphHeads: ({ to }) => !to.state?.isPartial })
101
+ ```
102
+
90
103
  ### `visit(url, options?)`
91
104
 
92
105
  A lower-level API for triggering a visit programmatically, e.g. in
package/dist/index.d.ts CHANGED
@@ -20,12 +20,12 @@ type MergeStrategy = 'replace' | 'morph' | 'before' | 'after' | 'prepend' | 'app
20
20
  interface Config {
21
21
  /** Strategy used to merge the swapped content into the current DOM. */
22
22
  merge?: MergeStrategy;
23
- /** Morph the `<head>` instead of leaving it untouched. */
24
- morphHeads?: boolean;
25
- /** Re-execute `<script>` tags found in the swapped content. */
26
- executeScripts?: boolean;
27
- /** Wrap navigations in the View Transitions API, if supported. */
28
- viewTransitions?: boolean;
23
+ /** Morph the `<head>` instead of leaving it untouched. A callback decides per visit. */
24
+ morphHeads?: Resolvable<boolean>;
25
+ /** Re-execute `<script>` tags found in the swapped content. A callback decides per visit. */
26
+ executeScripts?: Resolvable<boolean>;
27
+ /** Wrap navigations in the View Transitions API, if supported. A callback decides per visit. */
28
+ viewTransitions?: Resolvable<boolean>;
29
29
  /** Called instead of the default merge to render the new document. */
30
30
  render?: (newDocument: Document) => Promise<void> | void;
31
31
  /** Custom scroll handling, e.g. restoring position on back/forward visits. */
@@ -43,6 +43,12 @@ interface Config {
43
43
  /** Called before an element is removed from the DOM by a merge. */
44
44
  unmount?: (el: Element) => void;
45
45
  }
46
+ /** A value, or a callback resolving it right before the swap. */
47
+ type Resolvable<T> = T | ((info: {
48
+ from: PageState;
49
+ to: PageState;
50
+ isBackForward: boolean;
51
+ }) => T);
46
52
  type ScrollPosition = {
47
53
  top: number;
48
54
  };
@@ -95,6 +101,6 @@ declare const scrollPositions: Map<string, ScrollPosition>;
95
101
  * Load a new page, merge the regions/bodies and add a new history entry
96
102
  * according to the history mode.
97
103
  */
98
- declare const visit: (url: string, { history: historyMode, isBackForward, autoFocus, request, state, regions, morphHeads, merge: mergeStrategy, executeScripts: shouldExecuteScripts, viewTransitions, loadingDelay, progressHideDelay, }?: VisitOptions) => Promise<void>;
104
+ declare const visit: (url: string, { history: historyMode, isBackForward, autoFocus, request, state, regions, morphHeads, merge: mergeStrategy, executeScripts: executeScriptsOption, viewTransitions, loadingDelay, progressHideDelay, }?: VisitOptions) => Promise<void>;
99
105
 
100
- export { type AjaxState, type Config, type EventMap, type LoadingOptions, type MergeStrategy, type PageState, type ScrollBehavior, type ScrollPosition, type StartOptions, type VisitOptions, scrollPositions, start, visit };
106
+ export { type AjaxState, type Config, type EventMap, type LoadingOptions, type MergeStrategy, type PageState, type Resolvable, type ScrollBehavior, type ScrollPosition, type StartOptions, type VisitOptions, scrollPositions, start, visit };
package/dist/index.js CHANGED
@@ -13,14 +13,16 @@ var config = {
13
13
  // src/load.ts
14
14
  var parser = new DOMParser();
15
15
  var currentLoadController;
16
+ var progressDelayTimeout;
16
17
  var progress = 0;
17
18
  var trickleInterval;
18
19
  var emit = (type, payload) => document.dispatchEvent(new CustomEvent(`ajax:${type}`, { detail: payload }));
19
20
  var cache = /* @__PURE__ */ new Map();
20
21
  var load = async (url, regions, options) => {
21
- let progressDelayTimeout;
22
+ let aborted = false;
22
23
  try {
23
24
  currentLoadController?.abort();
25
+ clearTimeout(progressDelayTimeout);
24
26
  currentLoadController = new AbortController();
25
27
  setProgress(0);
26
28
  progressDelayTimeout = window.setTimeout(() => {
@@ -41,16 +43,18 @@ var load = async (url, regions, options) => {
41
43
  const document2 = parseHtml(html);
42
44
  return { document: document2, response };
43
45
  } catch (e) {
44
- setProgress(0);
45
- const isAbortError = e instanceof DOMException && e.name === "AbortError";
46
- if (!isAbortError) {
46
+ aborted = e instanceof DOMException && e.name === "AbortError";
47
+ if (!aborted) {
48
+ setProgress(0);
47
49
  window.location.reload();
48
50
  }
49
51
  } finally {
50
- clearTimeout(progressDelayTimeout);
51
- stopTrickle();
52
- setProgress(1);
53
- setTimeout(() => emit("progress-end", {}), options.progressHideDelay);
52
+ if (!aborted) {
53
+ clearTimeout(progressDelayTimeout);
54
+ stopTrickle();
55
+ setProgress(1);
56
+ setTimeout(() => emit("progress-end", {}), options.progressHideDelay);
57
+ }
54
58
  }
55
59
  };
56
60
  var parseHtml = (html) => parser.parseFromString(html, "text/html");
@@ -58,8 +62,14 @@ var trickle = () => {
58
62
  const amount = -0.095 * progress + 0.1;
59
63
  setProgress(progress + Math.random() * amount);
60
64
  };
61
- var startTrickle = () => trickleInterval = window.setInterval(trickle, 300);
62
- var stopTrickle = () => clearInterval(trickleInterval);
65
+ var startTrickle = () => {
66
+ stopTrickle();
67
+ trickleInterval = window.setInterval(trickle, 300);
68
+ };
69
+ var stopTrickle = () => {
70
+ clearInterval(trickleInterval);
71
+ trickleInterval = void 0;
72
+ };
63
73
  var setProgress = (value) => {
64
74
  progress = value;
65
75
  emit("progress", { progress: value });
@@ -76,7 +86,7 @@ var merge = (region, newRegion, strategy) => {
76
86
  Array.from(
77
87
  newRegion.querySelectorAll(`[${escapedPrefix}permanent][id]`)
78
88
  ).forEach((el) => {
79
- const original = region.querySelector(`#${el.id}`);
89
+ const original = region.querySelector(`#${CSS.escape(el.id)}`);
80
90
  if (original) {
81
91
  permanents.set(el.id, original);
82
92
  original.remove();
@@ -85,13 +95,12 @@ var merge = (region, newRegion, strategy) => {
85
95
  unmount?.(region);
86
96
  if (strategy === "replace") region.replaceWith(newRegion);
87
97
  else region.replaceChildren(...Array.from(newRegion.children));
88
- Array.from(
89
- newRegion.querySelectorAll(`[${escapedPrefix}permanent][id]`)
90
- ).forEach((el) => {
98
+ const target = strategy === "replace" ? newRegion : region;
99
+ target.querySelectorAll(`[${escapedPrefix}permanent][id]`).forEach((el) => {
91
100
  const original = permanents.get(el.id);
92
101
  if (original) el.replaceWith(original);
93
102
  });
94
- mount?.(strategy === "replace" ? newRegion : region);
103
+ mount?.(target);
95
104
  } else if (strategy === "before") {
96
105
  region.insertAdjacentElement("beforebegin", newRegion);
97
106
  mount?.(newRegion);
@@ -108,6 +117,7 @@ var merge = (region, newRegion, strategy) => {
108
117
  const result = morph(region, newRegion);
109
118
  autoFocusEl = result.autoFocusEl;
110
119
  }
120
+ if (strategy === "morph") return { autoFocusEl };
111
121
  if (newRegion.hasAttribute(`${prefix}autofocus`) || newRegion.autofocus) {
112
122
  autoFocusEl ??= newRegion;
113
123
  }
@@ -117,7 +127,7 @@ var merge = (region, newRegion, strategy) => {
117
127
  var morph = (container, newContainer) => {
118
128
  const { prefix, mount, unmount } = config;
119
129
  const manualReplacements = [];
120
- let autoFocusEls = [];
130
+ const autoFocusEls = [];
121
131
  let currentNodeKeepAttributes = [];
122
132
  Idiomorph.morph(container, newContainer, {
123
133
  callbacks: {
@@ -132,6 +142,7 @@ var morph = (container, newContainer) => {
132
142
  const isSameComponent = oldIsComponent && newIsComponent && oldNode.getAttribute(`${prefix}component`) === newNode.getAttribute(`${prefix}component`);
133
143
  if (!oldIsComponent || isSameComponent) return true;
134
144
  manualReplacements.push([oldNode, newNode]);
145
+ return false;
135
146
  },
136
147
  beforeAttributeUpdated: (name) => {
137
148
  return !currentNodeKeepAttributes.includes(name);
@@ -147,9 +158,9 @@ var morph = (container, newContainer) => {
147
158
  mount?.(node);
148
159
  }
149
160
  },
150
- afterNodeMorphed(_, newNode) {
151
- if (newNode instanceof HTMLElement && (newNode.autofocus || newNode.hasAttribute(`${prefix}autofocus`))) {
152
- autoFocusEls.push(newNode);
161
+ afterNodeMorphed(oldNode) {
162
+ if (oldNode instanceof HTMLElement && (oldNode.autofocus || oldNode.hasAttribute(`${prefix}autofocus`))) {
163
+ autoFocusEls.push(oldNode);
153
164
  }
154
165
  }
155
166
  }
@@ -207,7 +218,7 @@ var emit2 = (type, payload) => {
207
218
  document.dispatchEvent(event);
208
219
  if (waiting.length) return Promise.allSettled(waiting);
209
220
  };
210
- var currentUrl = window.location.pathname;
221
+ var currentUrl = window.location.pathname + window.location.search;
211
222
  var currentState = history.state;
212
223
  var currentVisitController;
213
224
  var scrollPositions = /* @__PURE__ */ new Map();
@@ -220,7 +231,7 @@ var visit = async (url, {
220
231
  regions = config.regions,
221
232
  morphHeads = config.morphHeads,
222
233
  merge: mergeStrategy = config.merge,
223
- executeScripts: shouldExecuteScripts = config.executeScripts,
234
+ executeScripts: executeScriptsOption = config.executeScripts,
224
235
  viewTransitions = config.viewTransitions,
225
236
  loadingDelay = config.loadingDelay,
226
237
  progressHideDelay = config.progressHideDelay
@@ -254,6 +265,7 @@ var visit = async (url, {
254
265
  }
255
266
  }
256
267
  if (!newDocument) return;
268
+ if (signal.aborted) return;
257
269
  currentUrl = url;
258
270
  currentState = state;
259
271
  if (historyMode === "replace") history.replaceState(state, "", url);
@@ -262,6 +274,10 @@ var visit = async (url, {
262
274
  const to = { url, state, document: newDocument };
263
275
  await emit2("before-swap", { from, to, isBackForward, signal });
264
276
  if (signal.aborted) return;
277
+ const resolve = (option) => typeof option === "function" ? option({ from, to, isBackForward }) : option;
278
+ const shouldMorphHeads = resolve(morphHeads);
279
+ const shouldExecuteScripts = resolve(executeScriptsOption);
280
+ const useViewTransitions = resolve(viewTransitions);
265
281
  const getMergeStrategy = (oldEl, newEl) => newEl.getAttribute(`${config.prefix}merge`) || oldEl.getAttribute(`${config.prefix}merge`) || mergeStrategy;
266
282
  let autoFocusEl;
267
283
  const hasMatchingRegions = regions?.some(
@@ -272,8 +288,9 @@ var visit = async (url, {
272
288
  if (hasMatchingRegions) {
273
289
  for (const id of regions) {
274
290
  const region = document.querySelector(id);
275
- const newRegion = newDocument.querySelector(id);
276
- if (!region || !newRegion) continue;
291
+ const parsedRegion = newDocument.querySelector(id);
292
+ if (!region || !parsedRegion) continue;
293
+ const newRegion = document.importNode(parsedRegion, true);
277
294
  const strategy = getMergeStrategy(region, newRegion);
278
295
  const result = merge(region, newRegion, strategy);
279
296
  autoFocusEl ??= result.autoFocusEl;
@@ -284,7 +301,7 @@ var visit = async (url, {
284
301
  }
285
302
  } else {
286
303
  const region = document.body;
287
- const newRegion = newDocument.body;
304
+ const newRegion = document.importNode(newDocument.body, true);
288
305
  const strategy = getMergeStrategy(region, newRegion);
289
306
  const result = merge(region, newRegion, strategy);
290
307
  autoFocusEl = result.autoFocusEl;
@@ -307,17 +324,17 @@ var visit = async (url, {
307
324
  if (position) window.scrollTo(position);
308
325
  };
309
326
  if (config.render) {
310
- if (morphHeads) Idiomorph2.morph(document.head, newDocument.head);
327
+ if (shouldMorphHeads) Idiomorph2.morph(document.head, newDocument.head);
311
328
  await config.render(newDocument);
312
- } else if (viewTransitions && document.startViewTransition) {
329
+ } else if (useViewTransitions && document.startViewTransition) {
313
330
  await document.startViewTransition(async () => {
314
- if (morphHeads) Idiomorph2.morph(document.head, newDocument.head);
331
+ if (shouldMorphHeads) Idiomorph2.morph(document.head, newDocument.head);
315
332
  await mergeRegions();
316
333
  await emit2("after-swap", { from, to, isBackForward, signal });
317
334
  applyScrollBehavior();
318
- }).ready;
335
+ }).updateCallbackDone;
319
336
  } else {
320
- if (morphHeads) Idiomorph2.morph(document.head, newDocument.head);
337
+ if (shouldMorphHeads) Idiomorph2.morph(document.head, newDocument.head);
321
338
  await mergeRegions();
322
339
  await emit2("after-swap", { from, to, isBackForward, signal });
323
340
  applyScrollBehavior();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@very-simple/ajax",
3
- "version": "1.0.0-beta.5",
3
+ "version": "1.0.0-beta.7",
4
4
  "description": "A very simple turbolinks inspired ajax library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",