turbo-rails 2.0.0.pre.beta.2 → 2.0.0.pre.beta.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,6 +1,6 @@
1
1
  /*!
2
- Turbo 8.0.0-beta.2
3
- Copyright © 2023 37signals LLC
2
+ Turbo 8.0.0-beta.4
3
+ Copyright © 2024 37signals LLC
4
4
  */
5
5
  (function(prototype) {
6
6
  if (typeof prototype.requestSubmit == "function") return;
@@ -485,6 +485,32 @@ async function around(callback, reader) {
485
485
  return [ before, after ];
486
486
  }
487
487
 
488
+ function doesNotTargetIFrame(anchor) {
489
+ if (anchor.hasAttribute("target")) {
490
+ for (const element of document.getElementsByName(anchor.target)) {
491
+ if (element instanceof HTMLIFrameElement) return false;
492
+ }
493
+ }
494
+ return true;
495
+ }
496
+
497
+ function findLinkFromClickTarget(target) {
498
+ return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
499
+ }
500
+
501
+ function getLocationForLink(link) {
502
+ return expandURL(link.getAttribute("href") || "");
503
+ }
504
+
505
+ function debounce(fn, delay) {
506
+ let timeoutId = null;
507
+ return (...args) => {
508
+ const callback = () => fn.apply(this, args);
509
+ clearTimeout(timeoutId);
510
+ timeoutId = setTimeout(callback, delay);
511
+ };
512
+ }
513
+
488
514
  class LimitedSet extends Set {
489
515
  constructor(maxSize) {
490
516
  super();
@@ -625,10 +651,15 @@ class FetchRequest {
625
651
  async perform() {
626
652
  const {fetchOptions: fetchOptions} = this;
627
653
  this.delegate.prepareRequest(this);
628
- await this.#allowRequestToBeIntercepted(fetchOptions);
654
+ const event = await this.#allowRequestToBeIntercepted(fetchOptions);
629
655
  try {
630
656
  this.delegate.requestStarted(this);
631
- const response = await fetchWithTurboHeaders(this.url.href, fetchOptions);
657
+ if (event.detail.fetchRequest) {
658
+ this.response = event.detail.fetchRequest.response;
659
+ } else {
660
+ this.response = fetchWithTurboHeaders(this.url.href, fetchOptions);
661
+ }
662
+ const response = await this.response;
632
663
  return await this.receive(response);
633
664
  } catch (error) {
634
665
  if (error.name !== "AbortError") {
@@ -686,6 +717,7 @@ class FetchRequest {
686
717
  });
687
718
  this.url = event.detail.url;
688
719
  if (event.defaultPrevented) await requestInterception;
720
+ return event;
689
721
  }
690
722
  #willDelegateErrorHandling(error) {
691
723
  const event = dispatch("turbo:fetch-request-error", {
@@ -781,6 +813,41 @@ function importStreamElements(fragment) {
781
813
  return fragment;
782
814
  }
783
815
 
816
+ const PREFETCH_DELAY = 100;
817
+
818
+ class PrefetchCache {
819
+ #prefetchTimeout=null;
820
+ #prefetched=null;
821
+ get(url) {
822
+ if (this.#prefetched && this.#prefetched.url === url && this.#prefetched.expire > Date.now()) {
823
+ return this.#prefetched.request;
824
+ }
825
+ }
826
+ setLater(url, request, ttl) {
827
+ this.clear();
828
+ this.#prefetchTimeout = setTimeout((() => {
829
+ request.perform();
830
+ this.set(url, request, ttl);
831
+ this.#prefetchTimeout = null;
832
+ }), PREFETCH_DELAY);
833
+ }
834
+ set(url, request, ttl) {
835
+ this.#prefetched = {
836
+ url: url,
837
+ request: request,
838
+ expire: new Date((new Date).getTime() + ttl)
839
+ };
840
+ }
841
+ clear() {
842
+ if (this.#prefetchTimeout) clearTimeout(this.#prefetchTimeout);
843
+ this.#prefetched = null;
844
+ }
845
+ }
846
+
847
+ const cacheTtl = 10 * 1e3;
848
+
849
+ const prefetchCache = new PrefetchCache;
850
+
784
851
  const FormSubmissionState = {
785
852
  initialized: "initialized",
786
853
  requesting: "requesting",
@@ -877,6 +944,7 @@ class FormSubmission {
877
944
  this.delegate.formSubmissionStarted(this);
878
945
  }
879
946
  requestPreventedHandlingResponse(request, response) {
947
+ prefetchCache.clear();
880
948
  this.result = {
881
949
  success: response.succeeded,
882
950
  fetchResponse: response
@@ -885,7 +953,10 @@ class FormSubmission {
885
953
  requestSucceededWithResponse(request, response) {
886
954
  if (response.clientError || response.serverError) {
887
955
  this.delegate.formSubmissionFailedWithResponse(this, response);
888
- } else if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
956
+ return;
957
+ }
958
+ prefetchCache.clear();
959
+ if (this.requestMustRedirect(request) && responseSucceededWithoutRedirect(response)) {
889
960
  const error = new Error("Form responses must redirect to another location");
890
961
  this.delegate.formSubmissionErrored(this, error);
891
962
  } else {
@@ -1168,7 +1239,7 @@ class View {
1168
1239
  resume: this.#resolveInterceptionPromise,
1169
1240
  render: this.renderer.renderElement
1170
1241
  };
1171
- const immediateRender = this.delegate.allowsImmediateRender(snapshot, isPreview, options);
1242
+ const immediateRender = this.delegate.allowsImmediateRender(snapshot, options);
1172
1243
  if (!immediateRender) await renderInterception;
1173
1244
  await this.renderSnapshot(renderer);
1174
1245
  this.delegate.viewRenderedSnapshot(snapshot, isPreview, this.renderer.renderMethod);
@@ -1286,9 +1357,9 @@ class LinkClickObserver {
1286
1357
  clickBubbled=event => {
1287
1358
  if (event instanceof MouseEvent && this.clickEventIsSignificant(event)) {
1288
1359
  const target = event.composedPath && event.composedPath()[0] || event.target;
1289
- const link = this.findLinkFromClickTarget(target);
1360
+ const link = findLinkFromClickTarget(target);
1290
1361
  if (link && doesNotTargetIFrame(link)) {
1291
- const location = this.getLocationForLink(link);
1362
+ const location = getLocationForLink(link);
1292
1363
  if (this.delegate.willFollowLinkToLocation(link, location, event)) {
1293
1364
  event.preventDefault();
1294
1365
  this.delegate.followedLinkToLocation(link, location);
@@ -1299,23 +1370,6 @@ class LinkClickObserver {
1299
1370
  clickEventIsSignificant(event) {
1300
1371
  return !(event.target && event.target.isContentEditable || event.defaultPrevented || event.which > 1 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey);
1301
1372
  }
1302
- findLinkFromClickTarget(target) {
1303
- return findClosestRecursively(target, "a[href]:not([target^=_]):not([download])");
1304
- }
1305
- getLocationForLink(link) {
1306
- return expandURL(link.getAttribute("href") || "");
1307
- }
1308
- }
1309
-
1310
- function doesNotTargetIFrame(anchor) {
1311
- if (anchor.hasAttribute("target")) {
1312
- for (const element of document.getElementsByName(anchor.target)) {
1313
- if (element instanceof HTMLIFrameElement) return false;
1314
- }
1315
- return true;
1316
- } else {
1317
- return true;
1318
- }
1319
1373
  }
1320
1374
 
1321
1375
  class FormLinkClickObserver {
@@ -1329,6 +1383,12 @@ class FormLinkClickObserver {
1329
1383
  stop() {
1330
1384
  this.linkInterceptor.stop();
1331
1385
  }
1386
+ canPrefetchRequestToLocation(link, location) {
1387
+ return false;
1388
+ }
1389
+ prefetchAndCacheRequestToLocation(link, location) {
1390
+ return;
1391
+ }
1332
1392
  willFollowLinkToLocation(link, location, originalEvent) {
1333
1393
  return this.delegate.willSubmitFormLinkToLocation(link, location, originalEvent) && (link.hasAttribute("data-turbo-method") || link.hasAttribute("data-turbo-stream"));
1334
1394
  }
@@ -2504,6 +2564,128 @@ class History {
2504
2564
  }
2505
2565
  }
2506
2566
 
2567
+ class LinkPrefetchObserver {
2568
+ started=false;
2569
+ hoverTriggerEvent="mouseenter";
2570
+ touchTriggerEvent="touchstart";
2571
+ constructor(delegate, eventTarget) {
2572
+ this.delegate = delegate;
2573
+ this.eventTarget = eventTarget;
2574
+ }
2575
+ start() {
2576
+ if (this.started) return;
2577
+ if (this.eventTarget.readyState === "loading") {
2578
+ this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
2579
+ once: true
2580
+ });
2581
+ } else {
2582
+ this.#enable();
2583
+ }
2584
+ }
2585
+ stop() {
2586
+ if (!this.started) return;
2587
+ this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
2588
+ capture: true,
2589
+ passive: true
2590
+ });
2591
+ this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
2592
+ capture: true,
2593
+ passive: true
2594
+ });
2595
+ this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
2596
+ this.started = false;
2597
+ }
2598
+ #enable=() => {
2599
+ this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
2600
+ capture: true,
2601
+ passive: true
2602
+ });
2603
+ this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
2604
+ capture: true,
2605
+ passive: true
2606
+ });
2607
+ this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
2608
+ this.started = true;
2609
+ };
2610
+ #tryToPrefetchRequest=event => {
2611
+ if (getMetaContent("turbo-prefetch") !== "true") return;
2612
+ const target = event.target;
2613
+ const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
2614
+ if (isLink && this.#isPrefetchable(target)) {
2615
+ const link = target;
2616
+ const location = getLocationForLink(link);
2617
+ if (this.delegate.canPrefetchRequestToLocation(link, location)) {
2618
+ const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
2619
+ prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
2620
+ link.addEventListener("mouseleave", (() => prefetchCache.clear()), {
2621
+ once: true
2622
+ });
2623
+ }
2624
+ }
2625
+ };
2626
+ #tryToUsePrefetchedRequest=event => {
2627
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
2628
+ const cached = prefetchCache.get(event.detail.url.toString());
2629
+ if (cached) {
2630
+ event.detail.fetchRequest = cached;
2631
+ }
2632
+ prefetchCache.clear();
2633
+ }
2634
+ };
2635
+ prepareRequest(request) {
2636
+ const link = request.target;
2637
+ request.headers["Sec-Purpose"] = "prefetch";
2638
+ const turboFrame = link.closest("turbo-frame");
2639
+ const turboFrameTarget = link.getAttribute("data-turbo-frame") || turboFrame?.getAttribute("target") || turboFrame?.id;
2640
+ if (turboFrameTarget && turboFrameTarget !== "_top") {
2641
+ request.headers["Turbo-Frame"] = turboFrameTarget;
2642
+ }
2643
+ if (link.hasAttribute("data-turbo-stream")) {
2644
+ request.acceptResponseType("text/vnd.turbo-stream.html");
2645
+ }
2646
+ }
2647
+ requestSucceededWithResponse() {}
2648
+ requestStarted(fetchRequest) {}
2649
+ requestErrored(fetchRequest) {}
2650
+ requestFinished(fetchRequest) {}
2651
+ requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
2652
+ requestFailedWithResponse(fetchRequest, fetchResponse) {}
2653
+ get #cacheTtl() {
2654
+ return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
2655
+ }
2656
+ #isPrefetchable(link) {
2657
+ const href = link.getAttribute("href");
2658
+ if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
2659
+ return false;
2660
+ }
2661
+ if (link.origin !== document.location.origin) {
2662
+ return false;
2663
+ }
2664
+ if (![ "http:", "https:" ].includes(link.protocol)) {
2665
+ return false;
2666
+ }
2667
+ if (link.pathname + link.search === document.location.pathname + document.location.search) {
2668
+ return false;
2669
+ }
2670
+ if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
2671
+ return false;
2672
+ }
2673
+ if (targetsIframe(link)) {
2674
+ return false;
2675
+ }
2676
+ if (link.pathname + link.search === document.location.pathname + document.location.search) {
2677
+ return false;
2678
+ }
2679
+ const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
2680
+ if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
2681
+ return false;
2682
+ }
2683
+ return true;
2684
+ }
2685
+ }
2686
+
2687
+ const targetsIframe = link => !doesNotTargetIFrame(link);
2688
+
2507
2689
  class Navigator {
2508
2690
  constructor(delegate) {
2509
2691
  this.delegate = delegate;
@@ -2894,504 +3076,545 @@ class ErrorRenderer extends Renderer {
2894
3076
  }
2895
3077
  }
2896
3078
 
2897
- let EMPTY_SET = new Set;
2898
-
2899
- function morph(oldNode, newContent, config = {}) {
2900
- if (oldNode instanceof Document) {
2901
- oldNode = oldNode.documentElement;
2902
- }
2903
- if (typeof newContent === "string") {
2904
- newContent = parseContent(newContent);
2905
- }
2906
- let normalizedContent = normalizeContent(newContent);
2907
- let ctx = createMorphContext(oldNode, normalizedContent, config);
2908
- return morphNormalizedContent(oldNode, normalizedContent, ctx);
2909
- }
2910
-
2911
- function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
2912
- if (ctx.head.block) {
2913
- let oldHead = oldNode.querySelector("head");
2914
- let newHead = normalizedNewContent.querySelector("head");
2915
- if (oldHead && newHead) {
2916
- let promises = handleHeadElement(newHead, oldHead, ctx);
2917
- Promise.all(promises).then((function() {
2918
- morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
2919
- head: {
2920
- block: false,
2921
- ignore: true
2922
- }
3079
+ var Idiomorph = function() {
3080
+ let EMPTY_SET = new Set;
3081
+ let defaults = {
3082
+ morphStyle: "outerHTML",
3083
+ callbacks: {
3084
+ beforeNodeAdded: noOp,
3085
+ afterNodeAdded: noOp,
3086
+ beforeNodeMorphed: noOp,
3087
+ afterNodeMorphed: noOp,
3088
+ beforeNodeRemoved: noOp,
3089
+ afterNodeRemoved: noOp,
3090
+ beforeAttributeUpdated: noOp
3091
+ },
3092
+ head: {
3093
+ style: "merge",
3094
+ shouldPreserve: function(elt) {
3095
+ return elt.getAttribute("im-preserve") === "true";
3096
+ },
3097
+ shouldReAppend: function(elt) {
3098
+ return elt.getAttribute("im-re-append") === "true";
3099
+ },
3100
+ shouldRemove: noOp,
3101
+ afterHeadMorphed: noOp
3102
+ }
3103
+ };
3104
+ function morph(oldNode, newContent, config = {}) {
3105
+ if (oldNode instanceof Document) {
3106
+ oldNode = oldNode.documentElement;
3107
+ }
3108
+ if (typeof newContent === "string") {
3109
+ newContent = parseContent(newContent);
3110
+ }
3111
+ let normalizedContent = normalizeContent(newContent);
3112
+ let ctx = createMorphContext(oldNode, normalizedContent, config);
3113
+ return morphNormalizedContent(oldNode, normalizedContent, ctx);
3114
+ }
3115
+ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
3116
+ if (ctx.head.block) {
3117
+ let oldHead = oldNode.querySelector("head");
3118
+ let newHead = normalizedNewContent.querySelector("head");
3119
+ if (oldHead && newHead) {
3120
+ let promises = handleHeadElement(newHead, oldHead, ctx);
3121
+ Promise.all(promises).then((function() {
3122
+ morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
3123
+ head: {
3124
+ block: false,
3125
+ ignore: true
3126
+ }
3127
+ }));
2923
3128
  }));
2924
- }));
2925
- return;
3129
+ return;
3130
+ }
2926
3131
  }
2927
- }
2928
- if (ctx.morphStyle === "innerHTML") {
2929
- morphChildren(normalizedNewContent, oldNode, ctx);
2930
- return oldNode.children;
2931
- } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
2932
- let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
2933
- let previousSibling = bestMatch?.previousSibling;
2934
- let nextSibling = bestMatch?.nextSibling;
2935
- let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
2936
- if (bestMatch) {
2937
- return insertSiblings(previousSibling, morphedNode, nextSibling);
3132
+ if (ctx.morphStyle === "innerHTML") {
3133
+ morphChildren(normalizedNewContent, oldNode, ctx);
3134
+ return oldNode.children;
3135
+ } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
3136
+ let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
3137
+ let previousSibling = bestMatch?.previousSibling;
3138
+ let nextSibling = bestMatch?.nextSibling;
3139
+ let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
3140
+ if (bestMatch) {
3141
+ return insertSiblings(previousSibling, morphedNode, nextSibling);
3142
+ } else {
3143
+ return [];
3144
+ }
2938
3145
  } else {
2939
- return [];
3146
+ throw "Do not understand how to morph style " + ctx.morphStyle;
2940
3147
  }
2941
- } else {
2942
- throw "Do not understand how to morph style " + ctx.morphStyle;
2943
3148
  }
2944
- }
2945
-
2946
- function morphOldNodeTo(oldNode, newContent, ctx) {
2947
- if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
2948
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
2949
- oldNode.remove();
2950
- ctx.callbacks.afterNodeRemoved(oldNode);
2951
- return null;
2952
- } else if (!isSoftMatch(oldNode, newContent)) {
2953
- if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return;
2954
- if (ctx.callbacks.beforeNodeAdded(newContent) === false) return;
2955
- oldNode.parentElement.replaceChild(newContent, oldNode);
2956
- ctx.callbacks.afterNodeAdded(newContent);
2957
- ctx.callbacks.afterNodeRemoved(oldNode);
2958
- return newContent;
2959
- } else {
2960
- if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return;
2961
- if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
2962
- handleHeadElement(newContent, oldNode, ctx);
2963
- } else {
2964
- syncNodeFrom(newContent, oldNode);
2965
- morphChildren(newContent, oldNode, ctx);
2966
- }
2967
- ctx.callbacks.afterNodeMorphed(oldNode, newContent);
2968
- return oldNode;
3149
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3150
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
2969
3151
  }
2970
- }
2971
-
2972
- function morphChildren(newParent, oldParent, ctx) {
2973
- let nextNewChild = newParent.firstChild;
2974
- let insertionPoint = oldParent.firstChild;
2975
- let newChild;
2976
- while (nextNewChild) {
2977
- newChild = nextNewChild;
2978
- nextNewChild = newChild.nextSibling;
2979
- if (insertionPoint == null) {
3152
+ function morphOldNodeTo(oldNode, newContent, ctx) {
3153
+ if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
3154
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3155
+ oldNode.remove();
3156
+ ctx.callbacks.afterNodeRemoved(oldNode);
3157
+ return null;
3158
+ } else if (!isSoftMatch(oldNode, newContent)) {
3159
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3160
+ if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
3161
+ oldNode.parentElement.replaceChild(newContent, oldNode);
3162
+ ctx.callbacks.afterNodeAdded(newContent);
3163
+ ctx.callbacks.afterNodeRemoved(oldNode);
3164
+ return newContent;
3165
+ } else {
3166
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
3167
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
3168
+ handleHeadElement(newContent, oldNode, ctx);
3169
+ } else {
3170
+ syncNodeFrom(newContent, oldNode, ctx);
3171
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
3172
+ morphChildren(newContent, oldNode, ctx);
3173
+ }
3174
+ }
3175
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
3176
+ return oldNode;
3177
+ }
3178
+ }
3179
+ function morphChildren(newParent, oldParent, ctx) {
3180
+ let nextNewChild = newParent.firstChild;
3181
+ let insertionPoint = oldParent.firstChild;
3182
+ let newChild;
3183
+ while (nextNewChild) {
3184
+ newChild = nextNewChild;
3185
+ nextNewChild = newChild.nextSibling;
3186
+ if (insertionPoint == null) {
3187
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3188
+ oldParent.appendChild(newChild);
3189
+ ctx.callbacks.afterNodeAdded(newChild);
3190
+ removeIdsFromConsideration(ctx, newChild);
3191
+ continue;
3192
+ }
3193
+ if (isIdSetMatch(newChild, insertionPoint, ctx)) {
3194
+ morphOldNodeTo(insertionPoint, newChild, ctx);
3195
+ insertionPoint = insertionPoint.nextSibling;
3196
+ removeIdsFromConsideration(ctx, newChild);
3197
+ continue;
3198
+ }
3199
+ let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3200
+ if (idSetMatch) {
3201
+ insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
3202
+ morphOldNodeTo(idSetMatch, newChild, ctx);
3203
+ removeIdsFromConsideration(ctx, newChild);
3204
+ continue;
3205
+ }
3206
+ let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3207
+ if (softMatch) {
3208
+ insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
3209
+ morphOldNodeTo(softMatch, newChild, ctx);
3210
+ removeIdsFromConsideration(ctx, newChild);
3211
+ continue;
3212
+ }
2980
3213
  if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
2981
- oldParent.appendChild(newChild);
3214
+ oldParent.insertBefore(newChild, insertionPoint);
2982
3215
  ctx.callbacks.afterNodeAdded(newChild);
2983
3216
  removeIdsFromConsideration(ctx, newChild);
2984
- continue;
2985
3217
  }
2986
- if (isIdSetMatch(newChild, insertionPoint, ctx)) {
2987
- morphOldNodeTo(insertionPoint, newChild, ctx);
3218
+ while (insertionPoint !== null) {
3219
+ let tempNode = insertionPoint;
2988
3220
  insertionPoint = insertionPoint.nextSibling;
2989
- removeIdsFromConsideration(ctx, newChild);
2990
- continue;
2991
- }
2992
- let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
2993
- if (idSetMatch) {
2994
- insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
2995
- morphOldNodeTo(idSetMatch, newChild, ctx);
2996
- removeIdsFromConsideration(ctx, newChild);
2997
- continue;
2998
- }
2999
- let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3000
- if (softMatch) {
3001
- insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
3002
- morphOldNodeTo(softMatch, newChild, ctx);
3003
- removeIdsFromConsideration(ctx, newChild);
3004
- continue;
3221
+ removeNode(tempNode, ctx);
3005
3222
  }
3006
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3007
- oldParent.insertBefore(newChild, insertionPoint);
3008
- ctx.callbacks.afterNodeAdded(newChild);
3009
- removeIdsFromConsideration(ctx, newChild);
3010
3223
  }
3011
- while (insertionPoint !== null) {
3012
- let tempNode = insertionPoint;
3013
- insertionPoint = insertionPoint.nextSibling;
3014
- removeNode(tempNode, ctx);
3224
+ function ignoreAttribute(attr, to, updateType, ctx) {
3225
+ if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
3226
+ return true;
3227
+ }
3228
+ return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
3015
3229
  }
3016
- }
3017
-
3018
- function syncNodeFrom(from, to) {
3019
- let type = from.nodeType;
3020
- if (type === 1) {
3021
- const fromAttributes = from.attributes;
3022
- const toAttributes = to.attributes;
3023
- for (const fromAttribute of fromAttributes) {
3024
- if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
3025
- to.setAttribute(fromAttribute.name, fromAttribute.value);
3230
+ function syncNodeFrom(from, to, ctx) {
3231
+ let type = from.nodeType;
3232
+ if (type === 1) {
3233
+ const fromAttributes = from.attributes;
3234
+ const toAttributes = to.attributes;
3235
+ for (const fromAttribute of fromAttributes) {
3236
+ if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
3237
+ continue;
3238
+ }
3239
+ if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
3240
+ to.setAttribute(fromAttribute.name, fromAttribute.value);
3241
+ }
3242
+ }
3243
+ for (let i = toAttributes.length - 1; 0 <= i; i--) {
3244
+ const toAttribute = toAttributes[i];
3245
+ if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
3246
+ continue;
3247
+ }
3248
+ if (!from.hasAttribute(toAttribute.name)) {
3249
+ to.removeAttribute(toAttribute.name);
3250
+ }
3026
3251
  }
3027
3252
  }
3028
- for (const toAttribute of toAttributes) {
3029
- if (!from.hasAttribute(toAttribute.name)) {
3030
- to.removeAttribute(toAttribute.name);
3253
+ if (type === 8 || type === 3) {
3254
+ if (to.nodeValue !== from.nodeValue) {
3255
+ to.nodeValue = from.nodeValue;
3031
3256
  }
3032
3257
  }
3033
- }
3034
- if (type === 8 || type === 3) {
3035
- if (to.nodeValue !== from.nodeValue) {
3036
- to.nodeValue = from.nodeValue;
3258
+ if (!ignoreValueOfActiveElement(to, ctx)) {
3259
+ syncInputValue(from, to, ctx);
3037
3260
  }
3038
3261
  }
3039
- if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
3040
- to.value = from.value || "";
3041
- syncAttribute(from, to, "value");
3042
- syncAttribute(from, to, "checked");
3043
- syncAttribute(from, to, "disabled");
3044
- } else if (from instanceof HTMLOptionElement) {
3045
- syncAttribute(from, to, "selected");
3046
- } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
3047
- let fromValue = from.value;
3048
- let toValue = to.value;
3049
- if (fromValue !== toValue) {
3050
- to.value = fromValue;
3051
- }
3052
- if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
3053
- to.firstChild.nodeValue = fromValue;
3262
+ function syncBooleanAttribute(from, to, attributeName, ctx) {
3263
+ if (from[attributeName] !== to[attributeName]) {
3264
+ let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
3265
+ if (!ignoreUpdate) {
3266
+ to[attributeName] = from[attributeName];
3267
+ }
3268
+ if (from[attributeName]) {
3269
+ if (!ignoreUpdate) {
3270
+ to.setAttribute(attributeName, from[attributeName]);
3271
+ }
3272
+ } else {
3273
+ if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
3274
+ to.removeAttribute(attributeName);
3275
+ }
3276
+ }
3054
3277
  }
3055
3278
  }
3056
- }
3057
-
3058
- function syncAttribute(from, to, attributeName) {
3059
- if (from[attributeName] !== to[attributeName]) {
3060
- if (from[attributeName]) {
3061
- to.setAttribute(attributeName, from[attributeName]);
3062
- } else {
3063
- to.removeAttribute(attributeName);
3279
+ function syncInputValue(from, to, ctx) {
3280
+ if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
3281
+ let fromValue = from.value;
3282
+ let toValue = to.value;
3283
+ syncBooleanAttribute(from, to, "checked", ctx);
3284
+ syncBooleanAttribute(from, to, "disabled", ctx);
3285
+ if (!from.hasAttribute("value")) {
3286
+ if (!ignoreAttribute("value", to, "remove", ctx)) {
3287
+ to.value = "";
3288
+ to.removeAttribute("value");
3289
+ }
3290
+ } else if (fromValue !== toValue) {
3291
+ if (!ignoreAttribute("value", to, "update", ctx)) {
3292
+ to.setAttribute("value", fromValue);
3293
+ to.value = fromValue;
3294
+ }
3295
+ }
3296
+ } else if (from instanceof HTMLOptionElement) {
3297
+ syncBooleanAttribute(from, to, "selected", ctx);
3298
+ } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
3299
+ let fromValue = from.value;
3300
+ let toValue = to.value;
3301
+ if (ignoreAttribute("value", to, "update", ctx)) {
3302
+ return;
3303
+ }
3304
+ if (fromValue !== toValue) {
3305
+ to.value = fromValue;
3306
+ }
3307
+ if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
3308
+ to.firstChild.nodeValue = fromValue;
3309
+ }
3064
3310
  }
3065
3311
  }
3066
- }
3067
-
3068
- function handleHeadElement(newHeadTag, currentHead, ctx) {
3069
- let added = [];
3070
- let removed = [];
3071
- let preserved = [];
3072
- let nodesToAppend = [];
3073
- let headMergeStyle = ctx.head.style;
3074
- let srcToNewHeadNodes = new Map;
3075
- for (const newHeadChild of newHeadTag.children) {
3076
- srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
3077
- }
3078
- for (const currentHeadElt of currentHead.children) {
3079
- let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
3080
- let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
3081
- let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
3082
- if (inNewContent || isPreserved) {
3083
- if (isReAppended) {
3084
- removed.push(currentHeadElt);
3085
- } else {
3086
- srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
3087
- preserved.push(currentHeadElt);
3088
- }
3089
- } else {
3090
- if (headMergeStyle === "append") {
3312
+ function handleHeadElement(newHeadTag, currentHead, ctx) {
3313
+ let added = [];
3314
+ let removed = [];
3315
+ let preserved = [];
3316
+ let nodesToAppend = [];
3317
+ let headMergeStyle = ctx.head.style;
3318
+ let srcToNewHeadNodes = new Map;
3319
+ for (const newHeadChild of newHeadTag.children) {
3320
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
3321
+ }
3322
+ for (const currentHeadElt of currentHead.children) {
3323
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
3324
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
3325
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
3326
+ if (inNewContent || isPreserved) {
3091
3327
  if (isReAppended) {
3092
3328
  removed.push(currentHeadElt);
3093
- nodesToAppend.push(currentHeadElt);
3329
+ } else {
3330
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
3331
+ preserved.push(currentHeadElt);
3094
3332
  }
3095
3333
  } else {
3096
- if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3097
- removed.push(currentHeadElt);
3334
+ if (headMergeStyle === "append") {
3335
+ if (isReAppended) {
3336
+ removed.push(currentHeadElt);
3337
+ nodesToAppend.push(currentHeadElt);
3338
+ }
3339
+ } else {
3340
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3341
+ removed.push(currentHeadElt);
3342
+ }
3098
3343
  }
3099
3344
  }
3100
3345
  }
3101
- }
3102
- nodesToAppend.push(...srcToNewHeadNodes.values());
3103
- let promises = [];
3104
- for (const newNode of nodesToAppend) {
3105
- let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
3106
- if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
3107
- if (newElt.href || newElt.src) {
3108
- let resolve = null;
3109
- let promise = new Promise((function(_resolve) {
3110
- resolve = _resolve;
3111
- }));
3112
- newElt.addEventListener("load", (function() {
3113
- resolve();
3114
- }));
3115
- promises.push(promise);
3346
+ nodesToAppend.push(...srcToNewHeadNodes.values());
3347
+ let promises = [];
3348
+ for (const newNode of nodesToAppend) {
3349
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
3350
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
3351
+ if (newElt.href || newElt.src) {
3352
+ let resolve = null;
3353
+ let promise = new Promise((function(_resolve) {
3354
+ resolve = _resolve;
3355
+ }));
3356
+ newElt.addEventListener("load", (function() {
3357
+ resolve();
3358
+ }));
3359
+ promises.push(promise);
3360
+ }
3361
+ currentHead.appendChild(newElt);
3362
+ ctx.callbacks.afterNodeAdded(newElt);
3363
+ added.push(newElt);
3116
3364
  }
3117
- currentHead.appendChild(newElt);
3118
- ctx.callbacks.afterNodeAdded(newElt);
3119
- added.push(newElt);
3120
3365
  }
3121
- }
3122
- for (const removedElement of removed) {
3123
- if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3124
- currentHead.removeChild(removedElement);
3125
- ctx.callbacks.afterNodeRemoved(removedElement);
3366
+ for (const removedElement of removed) {
3367
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3368
+ currentHead.removeChild(removedElement);
3369
+ ctx.callbacks.afterNodeRemoved(removedElement);
3370
+ }
3126
3371
  }
3372
+ ctx.head.afterHeadMorphed(currentHead, {
3373
+ added: added,
3374
+ kept: preserved,
3375
+ removed: removed
3376
+ });
3377
+ return promises;
3378
+ }
3379
+ function noOp() {}
3380
+ function mergeDefaults(config) {
3381
+ let finalConfig = {};
3382
+ Object.assign(finalConfig, defaults);
3383
+ Object.assign(finalConfig, config);
3384
+ finalConfig.callbacks = {};
3385
+ Object.assign(finalConfig.callbacks, defaults.callbacks);
3386
+ Object.assign(finalConfig.callbacks, config.callbacks);
3387
+ finalConfig.head = {};
3388
+ Object.assign(finalConfig.head, defaults.head);
3389
+ Object.assign(finalConfig.head, config.head);
3390
+ return finalConfig;
3391
+ }
3392
+ function createMorphContext(oldNode, newContent, config) {
3393
+ config = mergeDefaults(config);
3394
+ return {
3395
+ target: oldNode,
3396
+ newContent: newContent,
3397
+ config: config,
3398
+ morphStyle: config.morphStyle,
3399
+ ignoreActive: config.ignoreActive,
3400
+ ignoreActiveValue: config.ignoreActiveValue,
3401
+ idMap: createIdMap(oldNode, newContent),
3402
+ deadIds: new Set,
3403
+ callbacks: config.callbacks,
3404
+ head: config.head
3405
+ };
3127
3406
  }
3128
- ctx.head.afterHeadMorphed(currentHead, {
3129
- added: added,
3130
- kept: preserved,
3131
- removed: removed
3132
- });
3133
- return promises;
3134
- }
3135
-
3136
- function noOp() {}
3137
-
3138
- function createMorphContext(oldNode, newContent, config) {
3139
- return {
3140
- target: oldNode,
3141
- newContent: newContent,
3142
- config: config,
3143
- morphStyle: config.morphStyle,
3144
- ignoreActive: config.ignoreActive,
3145
- idMap: createIdMap(oldNode, newContent),
3146
- deadIds: new Set,
3147
- callbacks: Object.assign({
3148
- beforeNodeAdded: noOp,
3149
- afterNodeAdded: noOp,
3150
- beforeNodeMorphed: noOp,
3151
- afterNodeMorphed: noOp,
3152
- beforeNodeRemoved: noOp,
3153
- afterNodeRemoved: noOp
3154
- }, config.callbacks),
3155
- head: Object.assign({
3156
- style: "merge",
3157
- shouldPreserve: function(elt) {
3158
- return elt.getAttribute("im-preserve") === "true";
3159
- },
3160
- shouldReAppend: function(elt) {
3161
- return elt.getAttribute("im-re-append") === "true";
3162
- },
3163
- shouldRemove: noOp,
3164
- afterHeadMorphed: noOp
3165
- }, config.head)
3166
- };
3167
- }
3168
-
3169
- function isIdSetMatch(node1, node2, ctx) {
3170
- if (node1 == null || node2 == null) {
3171
- return false;
3172
- }
3173
- if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
3174
- if (node1.id !== "" && node1.id === node2.id) {
3175
- return true;
3176
- } else {
3177
- return getIdIntersectionCount(ctx, node1, node2) > 0;
3407
+ function isIdSetMatch(node1, node2, ctx) {
3408
+ if (node1 == null || node2 == null) {
3409
+ return false;
3178
3410
  }
3179
- }
3180
- return false;
3181
- }
3182
-
3183
- function isSoftMatch(node1, node2) {
3184
- if (node1 == null || node2 == null) {
3185
- return false;
3186
- }
3187
- return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
3188
- }
3189
-
3190
- function removeNodesBetween(startInclusive, endExclusive, ctx) {
3191
- while (startInclusive !== endExclusive) {
3192
- let tempNode = startInclusive;
3193
- startInclusive = startInclusive.nextSibling;
3194
- removeNode(tempNode, ctx);
3195
- }
3196
- removeIdsFromConsideration(ctx, endExclusive);
3197
- return endExclusive.nextSibling;
3198
- }
3199
-
3200
- function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3201
- let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
3202
- let potentialMatch = null;
3203
- if (newChildPotentialIdCount > 0) {
3204
- let potentialMatch = insertionPoint;
3205
- let otherMatchCount = 0;
3206
- while (potentialMatch != null) {
3207
- if (isIdSetMatch(newChild, potentialMatch, ctx)) {
3208
- return potentialMatch;
3209
- }
3210
- otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
3211
- if (otherMatchCount > newChildPotentialIdCount) {
3212
- return null;
3411
+ if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
3412
+ if (node1.id !== "" && node1.id === node2.id) {
3413
+ return true;
3414
+ } else {
3415
+ return getIdIntersectionCount(ctx, node1, node2) > 0;
3213
3416
  }
3214
- potentialMatch = potentialMatch.nextSibling;
3215
3417
  }
3418
+ return false;
3216
3419
  }
3217
- return potentialMatch;
3218
- }
3219
-
3220
- function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3221
- let potentialSoftMatch = insertionPoint;
3222
- let nextSibling = newChild.nextSibling;
3223
- let siblingSoftMatchCount = 0;
3224
- while (potentialSoftMatch != null) {
3225
- if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
3226
- return null;
3227
- }
3228
- if (isSoftMatch(newChild, potentialSoftMatch)) {
3229
- return potentialSoftMatch;
3420
+ function isSoftMatch(node1, node2) {
3421
+ if (node1 == null || node2 == null) {
3422
+ return false;
3230
3423
  }
3231
- if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3232
- siblingSoftMatchCount++;
3233
- nextSibling = nextSibling.nextSibling;
3234
- if (siblingSoftMatchCount >= 2) {
3235
- return null;
3424
+ return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
3425
+ }
3426
+ function removeNodesBetween(startInclusive, endExclusive, ctx) {
3427
+ while (startInclusive !== endExclusive) {
3428
+ let tempNode = startInclusive;
3429
+ startInclusive = startInclusive.nextSibling;
3430
+ removeNode(tempNode, ctx);
3431
+ }
3432
+ removeIdsFromConsideration(ctx, endExclusive);
3433
+ return endExclusive.nextSibling;
3434
+ }
3435
+ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3436
+ let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
3437
+ let potentialMatch = null;
3438
+ if (newChildPotentialIdCount > 0) {
3439
+ let potentialMatch = insertionPoint;
3440
+ let otherMatchCount = 0;
3441
+ while (potentialMatch != null) {
3442
+ if (isIdSetMatch(newChild, potentialMatch, ctx)) {
3443
+ return potentialMatch;
3444
+ }
3445
+ otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
3446
+ if (otherMatchCount > newChildPotentialIdCount) {
3447
+ return null;
3448
+ }
3449
+ potentialMatch = potentialMatch.nextSibling;
3236
3450
  }
3237
3451
  }
3238
- potentialSoftMatch = potentialSoftMatch.nextSibling;
3452
+ return potentialMatch;
3239
3453
  }
3240
- return potentialSoftMatch;
3241
- }
3242
-
3243
- function parseContent(newContent) {
3244
- let parser = new DOMParser;
3245
- let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
3246
- if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
3247
- let content = parser.parseFromString(newContent, "text/html");
3248
- if (contentWithSvgsRemoved.match(/<\/html>/)) {
3454
+ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3455
+ let potentialSoftMatch = insertionPoint;
3456
+ let nextSibling = newChild.nextSibling;
3457
+ let siblingSoftMatchCount = 0;
3458
+ while (potentialSoftMatch != null) {
3459
+ if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
3460
+ return null;
3461
+ }
3462
+ if (isSoftMatch(newChild, potentialSoftMatch)) {
3463
+ return potentialSoftMatch;
3464
+ }
3465
+ if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3466
+ siblingSoftMatchCount++;
3467
+ nextSibling = nextSibling.nextSibling;
3468
+ if (siblingSoftMatchCount >= 2) {
3469
+ return null;
3470
+ }
3471
+ }
3472
+ potentialSoftMatch = potentialSoftMatch.nextSibling;
3473
+ }
3474
+ return potentialSoftMatch;
3475
+ }
3476
+ function parseContent(newContent) {
3477
+ let parser = new DOMParser;
3478
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
3479
+ if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
3480
+ let content = parser.parseFromString(newContent, "text/html");
3481
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
3482
+ content.generatedByIdiomorph = true;
3483
+ return content;
3484
+ } else {
3485
+ let htmlElement = content.firstChild;
3486
+ if (htmlElement) {
3487
+ htmlElement.generatedByIdiomorph = true;
3488
+ return htmlElement;
3489
+ } else {
3490
+ return null;
3491
+ }
3492
+ }
3493
+ } else {
3494
+ let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
3495
+ let content = responseDoc.body.querySelector("template").content;
3249
3496
  content.generatedByIdiomorph = true;
3250
3497
  return content;
3498
+ }
3499
+ }
3500
+ function normalizeContent(newContent) {
3501
+ if (newContent == null) {
3502
+ const dummyParent = document.createElement("div");
3503
+ return dummyParent;
3504
+ } else if (newContent.generatedByIdiomorph) {
3505
+ return newContent;
3506
+ } else if (newContent instanceof Node) {
3507
+ const dummyParent = document.createElement("div");
3508
+ dummyParent.append(newContent);
3509
+ return dummyParent;
3251
3510
  } else {
3252
- let htmlElement = content.firstChild;
3253
- if (htmlElement) {
3254
- htmlElement.generatedByIdiomorph = true;
3255
- return htmlElement;
3256
- } else {
3257
- return null;
3511
+ const dummyParent = document.createElement("div");
3512
+ for (const elt of [ ...newContent ]) {
3513
+ dummyParent.append(elt);
3258
3514
  }
3515
+ return dummyParent;
3259
3516
  }
3260
- } else {
3261
- let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
3262
- let content = responseDoc.body.querySelector("template").content;
3263
- content.generatedByIdiomorph = true;
3264
- return content;
3265
- }
3266
- }
3267
-
3268
- function normalizeContent(newContent) {
3269
- if (newContent == null) {
3270
- const dummyParent = document.createElement("div");
3271
- return dummyParent;
3272
- } else if (newContent.generatedByIdiomorph) {
3273
- return newContent;
3274
- } else if (newContent instanceof Node) {
3275
- const dummyParent = document.createElement("div");
3276
- dummyParent.append(newContent);
3277
- return dummyParent;
3278
- } else {
3279
- const dummyParent = document.createElement("div");
3280
- for (const elt of [ ...newContent ]) {
3281
- dummyParent.append(elt);
3517
+ }
3518
+ function insertSiblings(previousSibling, morphedNode, nextSibling) {
3519
+ let stack = [];
3520
+ let added = [];
3521
+ while (previousSibling != null) {
3522
+ stack.push(previousSibling);
3523
+ previousSibling = previousSibling.previousSibling;
3524
+ }
3525
+ while (stack.length > 0) {
3526
+ let node = stack.pop();
3527
+ added.push(node);
3528
+ morphedNode.parentElement.insertBefore(node, morphedNode);
3529
+ }
3530
+ added.push(morphedNode);
3531
+ while (nextSibling != null) {
3532
+ stack.push(nextSibling);
3533
+ added.push(nextSibling);
3534
+ nextSibling = nextSibling.nextSibling;
3282
3535
  }
3283
- return dummyParent;
3536
+ while (stack.length > 0) {
3537
+ morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3538
+ }
3539
+ return added;
3540
+ }
3541
+ function findBestNodeMatch(newContent, oldNode, ctx) {
3542
+ let currentElement;
3543
+ currentElement = newContent.firstChild;
3544
+ let bestElement = currentElement;
3545
+ let score = 0;
3546
+ while (currentElement) {
3547
+ let newScore = scoreElement(currentElement, oldNode, ctx);
3548
+ if (newScore > score) {
3549
+ bestElement = currentElement;
3550
+ score = newScore;
3551
+ }
3552
+ currentElement = currentElement.nextSibling;
3553
+ }
3554
+ return bestElement;
3284
3555
  }
3285
- }
3286
-
3287
- function insertSiblings(previousSibling, morphedNode, nextSibling) {
3288
- let stack = [];
3289
- let added = [];
3290
- while (previousSibling != null) {
3291
- stack.push(previousSibling);
3292
- previousSibling = previousSibling.previousSibling;
3556
+ function scoreElement(node1, node2, ctx) {
3557
+ if (isSoftMatch(node1, node2)) {
3558
+ return .5 + getIdIntersectionCount(ctx, node1, node2);
3559
+ }
3560
+ return 0;
3293
3561
  }
3294
- while (stack.length > 0) {
3295
- let node = stack.pop();
3296
- added.push(node);
3297
- morphedNode.parentElement.insertBefore(node, morphedNode);
3562
+ function removeNode(tempNode, ctx) {
3563
+ removeIdsFromConsideration(ctx, tempNode);
3564
+ if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
3565
+ tempNode.remove();
3566
+ ctx.callbacks.afterNodeRemoved(tempNode);
3298
3567
  }
3299
- added.push(morphedNode);
3300
- while (nextSibling != null) {
3301
- stack.push(nextSibling);
3302
- added.push(nextSibling);
3303
- nextSibling = nextSibling.nextSibling;
3568
+ function isIdInConsideration(ctx, id) {
3569
+ return !ctx.deadIds.has(id);
3304
3570
  }
3305
- while (stack.length > 0) {
3306
- morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3571
+ function idIsWithinNode(ctx, id, targetNode) {
3572
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
3573
+ return idSet.has(id);
3307
3574
  }
3308
- return added;
3309
- }
3310
-
3311
- function findBestNodeMatch(newContent, oldNode, ctx) {
3312
- let currentElement;
3313
- currentElement = newContent.firstChild;
3314
- let bestElement = currentElement;
3315
- let score = 0;
3316
- while (currentElement) {
3317
- let newScore = scoreElement(currentElement, oldNode, ctx);
3318
- if (newScore > score) {
3319
- bestElement = currentElement;
3320
- score = newScore;
3575
+ function removeIdsFromConsideration(ctx, node) {
3576
+ let idSet = ctx.idMap.get(node) || EMPTY_SET;
3577
+ for (const id of idSet) {
3578
+ ctx.deadIds.add(id);
3321
3579
  }
3322
- currentElement = currentElement.nextSibling;
3323
- }
3324
- return bestElement;
3325
- }
3326
-
3327
- function scoreElement(node1, node2, ctx) {
3328
- if (isSoftMatch(node1, node2)) {
3329
- return .5 + getIdIntersectionCount(ctx, node1, node2);
3330
- }
3331
- return 0;
3332
- }
3333
-
3334
- function removeNode(tempNode, ctx) {
3335
- removeIdsFromConsideration(ctx, tempNode);
3336
- if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
3337
- tempNode.remove();
3338
- ctx.callbacks.afterNodeRemoved(tempNode);
3339
- }
3340
-
3341
- function isIdInConsideration(ctx, id) {
3342
- return !ctx.deadIds.has(id);
3343
- }
3344
-
3345
- function idIsWithinNode(ctx, id, targetNode) {
3346
- let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
3347
- return idSet.has(id);
3348
- }
3349
-
3350
- function removeIdsFromConsideration(ctx, node) {
3351
- let idSet = ctx.idMap.get(node) || EMPTY_SET;
3352
- for (const id of idSet) {
3353
- ctx.deadIds.add(id);
3354
3580
  }
3355
- }
3356
-
3357
- function getIdIntersectionCount(ctx, node1, node2) {
3358
- let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
3359
- let matchCount = 0;
3360
- for (const id of sourceSet) {
3361
- if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
3362
- ++matchCount;
3581
+ function getIdIntersectionCount(ctx, node1, node2) {
3582
+ let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
3583
+ let matchCount = 0;
3584
+ for (const id of sourceSet) {
3585
+ if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
3586
+ ++matchCount;
3587
+ }
3363
3588
  }
3364
- }
3365
- return matchCount;
3366
- }
3367
-
3368
- function populateIdMapForNode(node, idMap) {
3369
- let nodeParent = node.parentElement;
3370
- let idElements = node.querySelectorAll("[id]");
3371
- for (const elt of idElements) {
3372
- let current = elt;
3373
- while (current !== nodeParent && current != null) {
3374
- let idSet = idMap.get(current);
3375
- if (idSet == null) {
3376
- idSet = new Set;
3377
- idMap.set(current, idSet);
3589
+ return matchCount;
3590
+ }
3591
+ function populateIdMapForNode(node, idMap) {
3592
+ let nodeParent = node.parentElement;
3593
+ let idElements = node.querySelectorAll("[id]");
3594
+ for (const elt of idElements) {
3595
+ let current = elt;
3596
+ while (current !== nodeParent && current != null) {
3597
+ let idSet = idMap.get(current);
3598
+ if (idSet == null) {
3599
+ idSet = new Set;
3600
+ idMap.set(current, idSet);
3601
+ }
3602
+ idSet.add(elt.id);
3603
+ current = current.parentElement;
3378
3604
  }
3379
- idSet.add(elt.id);
3380
- current = current.parentElement;
3381
3605
  }
3382
3606
  }
3383
- }
3384
-
3385
- function createIdMap(oldContent, newContent) {
3386
- let idMap = new Map;
3387
- populateIdMapForNode(oldContent, idMap);
3388
- populateIdMapForNode(newContent, idMap);
3389
- return idMap;
3390
- }
3391
-
3392
- var idiomorph = {
3393
- morph: morph
3394
- };
3607
+ function createIdMap(oldContent, newContent) {
3608
+ let idMap = new Map;
3609
+ populateIdMapForNode(oldContent, idMap);
3610
+ populateIdMapForNode(newContent, idMap);
3611
+ return idMap;
3612
+ }
3613
+ return {
3614
+ morph: morph,
3615
+ defaults: defaults
3616
+ };
3617
+ }();
3395
3618
 
3396
3619
  class MorphRenderer extends Renderer {
3397
3620
  async render() {
@@ -3412,7 +3635,7 @@ class MorphRenderer extends Renderer {
3412
3635
  }
3413
3636
  #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
3414
3637
  this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
3415
- idiomorph.morph(currentElement, newElement, {
3638
+ Idiomorph.morph(currentElement, newElement, {
3416
3639
  morphStyle: morphStyle,
3417
3640
  callbacks: {
3418
3641
  beforeNodeAdded: this.#shouldAddElement,
@@ -3525,6 +3748,9 @@ class PageRenderer extends Renderer {
3525
3748
  this.copyNewHeadScriptElements();
3526
3749
  await mergedHeadElements;
3527
3750
  await newStylesheetElements;
3751
+ if (this.willRender) {
3752
+ this.removeUnusedDynamicStylesheetElements();
3753
+ }
3528
3754
  }
3529
3755
  async replaceBody() {
3530
3756
  await this.preservingPermanentElements((async () => {
@@ -3548,6 +3774,11 @@ class PageRenderer extends Renderer {
3548
3774
  document.head.appendChild(activateScriptElement(element));
3549
3775
  }
3550
3776
  }
3777
+ removeUnusedDynamicStylesheetElements() {
3778
+ for (const element of this.unusedDynamicStylesheetElements) {
3779
+ document.head.removeChild(element);
3780
+ }
3781
+ }
3551
3782
  async mergeProvisionalElements() {
3552
3783
  const newHeadElements = [ ...this.newHeadProvisionalElements ];
3553
3784
  for (const element of this.currentHeadProvisionalElements) {
@@ -3600,6 +3831,12 @@ class PageRenderer extends Renderer {
3600
3831
  async assignNewBody() {
3601
3832
  await this.renderElement(this.currentElement, this.newElement);
3602
3833
  }
3834
+ get unusedDynamicStylesheetElements() {
3835
+ return this.oldHeadStylesheetElements.filter((element => element.getAttribute("data-turbo-track") === "dynamic"));
3836
+ }
3837
+ get oldHeadStylesheetElements() {
3838
+ return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot);
3839
+ }
3603
3840
  get newHeadStylesheetElements() {
3604
3841
  return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
3605
3842
  }
@@ -3790,6 +4027,7 @@ class Session {
3790
4027
  adapter=new BrowserAdapter(this);
3791
4028
  pageObserver=new PageObserver(this);
3792
4029
  cacheObserver=new CacheObserver;
4030
+ linkPrefetchObserver=new LinkPrefetchObserver(this, document);
3793
4031
  linkClickObserver=new LinkClickObserver(this, window);
3794
4032
  formSubmitObserver=new FormSubmitObserver(this, document);
3795
4033
  scrollObserver=new ScrollObserver(this);
@@ -3803,14 +4041,18 @@ class Session {
3803
4041
  progressBarDelay=500;
3804
4042
  started=false;
3805
4043
  formMode="on";
4044
+ #pageRefreshDebouncePeriod=150;
3806
4045
  constructor(recentRequests) {
3807
4046
  this.recentRequests = recentRequests;
3808
4047
  this.preloader = new Preloader(this, this.view.snapshotCache);
4048
+ this.debouncedRefresh = this.refresh;
4049
+ this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
3809
4050
  }
3810
4051
  start() {
3811
4052
  if (!this.started) {
3812
4053
  this.pageObserver.start();
3813
4054
  this.cacheObserver.start();
4055
+ this.linkPrefetchObserver.start();
3814
4056
  this.formLinkClickObserver.start();
3815
4057
  this.linkClickObserver.start();
3816
4058
  this.formSubmitObserver.start();
@@ -3830,6 +4072,7 @@ class Session {
3830
4072
  if (this.started) {
3831
4073
  this.pageObserver.stop();
3832
4074
  this.cacheObserver.stop();
4075
+ this.linkPrefetchObserver.stop();
3833
4076
  this.formLinkClickObserver.stop();
3834
4077
  this.linkClickObserver.stop();
3835
4078
  this.formSubmitObserver.stop();
@@ -3886,6 +4129,13 @@ class Session {
3886
4129
  get restorationIdentifier() {
3887
4130
  return this.history.restorationIdentifier;
3888
4131
  }
4132
+ get pageRefreshDebouncePeriod() {
4133
+ return this.#pageRefreshDebouncePeriod;
4134
+ }
4135
+ set pageRefreshDebouncePeriod(value) {
4136
+ this.refresh = debounce(this.debouncedRefresh.bind(this), value);
4137
+ this.#pageRefreshDebouncePeriod = value;
4138
+ }
3889
4139
  shouldPreloadLink(element) {
3890
4140
  const isUnsafe = element.hasAttribute("data-turbo-method");
3891
4141
  const isStream = element.hasAttribute("data-turbo-stream");
@@ -3920,6 +4170,9 @@ class Session {
3920
4170
  return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
3921
4171
  }
3922
4172
  submittedFormLinkToLocation() {}
4173
+ canPrefetchRequestToLocation(link, location) {
4174
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
4175
+ }
3923
4176
  willFollowLinkToLocation(link, location, event) {
3924
4177
  return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
3925
4178
  }
@@ -3984,17 +4237,17 @@ class Session {
3984
4237
  this.notifyApplicationBeforeCachingSnapshot();
3985
4238
  }
3986
4239
  }
3987
- allowsImmediateRender({element: element}, isPreview, options) {
3988
- const event = this.notifyApplicationBeforeRender(element, isPreview, options);
4240
+ allowsImmediateRender({element: element}, options) {
4241
+ const event = this.notifyApplicationBeforeRender(element, options);
3989
4242
  const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
3990
4243
  if (this.view.renderer && render) {
3991
4244
  this.view.renderer.renderElement = render;
3992
4245
  }
3993
4246
  return !defaultPrevented;
3994
4247
  }
3995
- viewRenderedSnapshot(_snapshot, isPreview, renderMethod) {
4248
+ viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
3996
4249
  this.view.lastRenderedLocation = this.history.location;
3997
- this.notifyApplicationAfterRender(isPreview, renderMethod);
4250
+ this.notifyApplicationAfterRender(renderMethod);
3998
4251
  }
3999
4252
  preloadOnLoadLinksForView(element) {
4000
4253
  this.preloader.preloadOnLoadLinksForView(element);
@@ -4045,20 +4298,18 @@ class Session {
4045
4298
  notifyApplicationBeforeCachingSnapshot() {
4046
4299
  return dispatch("turbo:before-cache");
4047
4300
  }
4048
- notifyApplicationBeforeRender(newBody, isPreview, options) {
4301
+ notifyApplicationBeforeRender(newBody, options) {
4049
4302
  return dispatch("turbo:before-render", {
4050
4303
  detail: {
4051
4304
  newBody: newBody,
4052
- isPreview: isPreview,
4053
4305
  ...options
4054
4306
  },
4055
4307
  cancelable: true
4056
4308
  });
4057
4309
  }
4058
- notifyApplicationAfterRender(isPreview, renderMethod) {
4310
+ notifyApplicationAfterRender(renderMethod) {
4059
4311
  return dispatch("turbo:render", {
4060
4312
  detail: {
4061
- isPreview: isPreview,
4062
4313
  renderMethod: renderMethod
4063
4314
  }
4064
4315
  });
@@ -4386,7 +4637,7 @@ class FrameController {
4386
4637
  formSubmissionFinished({formElement: formElement}) {
4387
4638
  clearBusyState(formElement, this.#findFrameElement(formElement));
4388
4639
  }
4389
- allowsImmediateRender({element: newFrame}, _isPreview, options) {
4640
+ allowsImmediateRender({element: newFrame}, options) {
4390
4641
  const event = dispatch("turbo:before-frame-render", {
4391
4642
  target: this.element,
4392
4643
  detail: {