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

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.3
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
  }
@@ -1563,6 +1623,8 @@ function readScrollBehavior(value, defaultValue) {
1563
1623
  }
1564
1624
  }
1565
1625
 
1626
+ const ProgressBarID = "turbo-progress-bar";
1627
+
1566
1628
  class ProgressBar {
1567
1629
  static animationDuration=300;
1568
1630
  static get defaultCSS() {
@@ -1650,6 +1712,8 @@ class ProgressBar {
1650
1712
  }
1651
1713
  createStylesheetElement() {
1652
1714
  const element = document.createElement("style");
1715
+ element.id = ProgressBarID;
1716
+ element.setAttribute("data-turbo-permanent", "");
1653
1717
  element.type = "text/css";
1654
1718
  element.textContent = ProgressBar.defaultCSS;
1655
1719
  if (this.cspNonce) {
@@ -2504,6 +2568,131 @@ class History {
2504
2568
  }
2505
2569
  }
2506
2570
 
2571
+ class LinkPrefetchObserver {
2572
+ started=false;
2573
+ hoverTriggerEvent="mouseenter";
2574
+ touchTriggerEvent="touchstart";
2575
+ constructor(delegate, eventTarget) {
2576
+ this.delegate = delegate;
2577
+ this.eventTarget = eventTarget;
2578
+ }
2579
+ start() {
2580
+ if (this.started) return;
2581
+ if (this.eventTarget.readyState === "loading") {
2582
+ this.eventTarget.addEventListener("DOMContentLoaded", this.#enable, {
2583
+ once: true
2584
+ });
2585
+ } else {
2586
+ this.#enable();
2587
+ }
2588
+ }
2589
+ stop() {
2590
+ if (!this.started) return;
2591
+ this.eventTarget.removeEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
2592
+ capture: true,
2593
+ passive: true
2594
+ });
2595
+ this.eventTarget.removeEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
2596
+ capture: true,
2597
+ passive: true
2598
+ });
2599
+ this.eventTarget.removeEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
2600
+ this.started = false;
2601
+ }
2602
+ #enable=() => {
2603
+ this.eventTarget.addEventListener(this.hoverTriggerEvent, this.#tryToPrefetchRequest, {
2604
+ capture: true,
2605
+ passive: true
2606
+ });
2607
+ this.eventTarget.addEventListener(this.touchTriggerEvent, this.#tryToPrefetchRequest, {
2608
+ capture: true,
2609
+ passive: true
2610
+ });
2611
+ this.eventTarget.addEventListener("turbo:before-fetch-request", this.#tryToUsePrefetchedRequest, true);
2612
+ this.started = true;
2613
+ };
2614
+ #tryToPrefetchRequest=event => {
2615
+ if (getMetaContent("turbo-prefetch") !== "true") return;
2616
+ const target = event.target;
2617
+ const isLink = target.matches && target.matches("a[href]:not([target^=_]):not([download])");
2618
+ if (isLink && this.#isPrefetchable(target)) {
2619
+ const link = target;
2620
+ const location = getLocationForLink(link);
2621
+ if (this.delegate.canPrefetchRequestToLocation(link, location)) {
2622
+ const fetchRequest = new FetchRequest(this, FetchMethod.get, location, new URLSearchParams, target);
2623
+ prefetchCache.setLater(location.toString(), fetchRequest, this.#cacheTtl);
2624
+ link.addEventListener("mouseleave", (() => prefetchCache.clear()), {
2625
+ once: true
2626
+ });
2627
+ }
2628
+ }
2629
+ };
2630
+ #tryToUsePrefetchedRequest=event => {
2631
+ if (event.target.tagName !== "FORM" && event.detail.fetchOptions.method === "get") {
2632
+ const cached = prefetchCache.get(event.detail.url.toString());
2633
+ if (cached) {
2634
+ event.detail.fetchRequest = cached;
2635
+ }
2636
+ prefetchCache.clear();
2637
+ }
2638
+ };
2639
+ prepareRequest(request) {
2640
+ const link = request.target;
2641
+ request.headers["Sec-Purpose"] = "prefetch";
2642
+ if (link.dataset.turboFrame && link.dataset.turboFrame !== "_top") {
2643
+ request.headers["Turbo-Frame"] = link.dataset.turboFrame;
2644
+ } else if (link.dataset.turboFrame !== "_top") {
2645
+ const turboFrame = link.closest("turbo-frame");
2646
+ if (turboFrame) {
2647
+ request.headers["Turbo-Frame"] = turboFrame.id;
2648
+ }
2649
+ }
2650
+ if (link.hasAttribute("data-turbo-stream")) {
2651
+ request.acceptResponseType("text/vnd.turbo-stream.html");
2652
+ }
2653
+ }
2654
+ requestSucceededWithResponse() {}
2655
+ requestStarted(fetchRequest) {}
2656
+ requestErrored(fetchRequest) {}
2657
+ requestFinished(fetchRequest) {}
2658
+ requestPreventedHandlingResponse(fetchRequest, fetchResponse) {}
2659
+ requestFailedWithResponse(fetchRequest, fetchResponse) {}
2660
+ get #cacheTtl() {
2661
+ return Number(getMetaContent("turbo-prefetch-cache-time")) || cacheTtl;
2662
+ }
2663
+ #isPrefetchable(link) {
2664
+ const href = link.getAttribute("href");
2665
+ if (!href || href === "#" || link.dataset.turbo === "false" || link.dataset.turboPrefetch === "false") {
2666
+ return false;
2667
+ }
2668
+ if (link.origin !== document.location.origin) {
2669
+ return false;
2670
+ }
2671
+ if (![ "http:", "https:" ].includes(link.protocol)) {
2672
+ return false;
2673
+ }
2674
+ if (link.pathname + link.search === document.location.pathname + document.location.search) {
2675
+ return false;
2676
+ }
2677
+ if (link.dataset.turboMethod && link.dataset.turboMethod !== "get") {
2678
+ return false;
2679
+ }
2680
+ if (targetsIframe(link)) {
2681
+ return false;
2682
+ }
2683
+ if (link.pathname + link.search === document.location.pathname + document.location.search) {
2684
+ return false;
2685
+ }
2686
+ const turboPrefetchParent = findClosestRecursively(link, "[data-turbo-prefetch]");
2687
+ if (turboPrefetchParent && turboPrefetchParent.dataset.turboPrefetch === "false") {
2688
+ return false;
2689
+ }
2690
+ return true;
2691
+ }
2692
+ }
2693
+
2694
+ const targetsIframe = link => !doesNotTargetIFrame(link);
2695
+
2507
2696
  class Navigator {
2508
2697
  constructor(delegate) {
2509
2698
  this.delegate = delegate;
@@ -2894,504 +3083,545 @@ class ErrorRenderer extends Renderer {
2894
3083
  }
2895
3084
  }
2896
3085
 
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
- }
3086
+ var Idiomorph = function() {
3087
+ let EMPTY_SET = new Set;
3088
+ let defaults = {
3089
+ morphStyle: "outerHTML",
3090
+ callbacks: {
3091
+ beforeNodeAdded: noOp,
3092
+ afterNodeAdded: noOp,
3093
+ beforeNodeMorphed: noOp,
3094
+ afterNodeMorphed: noOp,
3095
+ beforeNodeRemoved: noOp,
3096
+ afterNodeRemoved: noOp,
3097
+ beforeAttributeUpdated: noOp
3098
+ },
3099
+ head: {
3100
+ style: "merge",
3101
+ shouldPreserve: function(elt) {
3102
+ return elt.getAttribute("im-preserve") === "true";
3103
+ },
3104
+ shouldReAppend: function(elt) {
3105
+ return elt.getAttribute("im-re-append") === "true";
3106
+ },
3107
+ shouldRemove: noOp,
3108
+ afterHeadMorphed: noOp
3109
+ }
3110
+ };
3111
+ function morph(oldNode, newContent, config = {}) {
3112
+ if (oldNode instanceof Document) {
3113
+ oldNode = oldNode.documentElement;
3114
+ }
3115
+ if (typeof newContent === "string") {
3116
+ newContent = parseContent(newContent);
3117
+ }
3118
+ let normalizedContent = normalizeContent(newContent);
3119
+ let ctx = createMorphContext(oldNode, normalizedContent, config);
3120
+ return morphNormalizedContent(oldNode, normalizedContent, ctx);
3121
+ }
3122
+ function morphNormalizedContent(oldNode, normalizedNewContent, ctx) {
3123
+ if (ctx.head.block) {
3124
+ let oldHead = oldNode.querySelector("head");
3125
+ let newHead = normalizedNewContent.querySelector("head");
3126
+ if (oldHead && newHead) {
3127
+ let promises = handleHeadElement(newHead, oldHead, ctx);
3128
+ Promise.all(promises).then((function() {
3129
+ morphNormalizedContent(oldNode, normalizedNewContent, Object.assign(ctx, {
3130
+ head: {
3131
+ block: false,
3132
+ ignore: true
3133
+ }
3134
+ }));
2923
3135
  }));
2924
- }));
2925
- return;
3136
+ return;
3137
+ }
2926
3138
  }
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);
3139
+ if (ctx.morphStyle === "innerHTML") {
3140
+ morphChildren(normalizedNewContent, oldNode, ctx);
3141
+ return oldNode.children;
3142
+ } else if (ctx.morphStyle === "outerHTML" || ctx.morphStyle == null) {
3143
+ let bestMatch = findBestNodeMatch(normalizedNewContent, oldNode, ctx);
3144
+ let previousSibling = bestMatch?.previousSibling;
3145
+ let nextSibling = bestMatch?.nextSibling;
3146
+ let morphedNode = morphOldNodeTo(oldNode, bestMatch, ctx);
3147
+ if (bestMatch) {
3148
+ return insertSiblings(previousSibling, morphedNode, nextSibling);
3149
+ } else {
3150
+ return [];
3151
+ }
2938
3152
  } else {
2939
- return [];
3153
+ throw "Do not understand how to morph style " + ctx.morphStyle;
2940
3154
  }
2941
- } else {
2942
- throw "Do not understand how to morph style " + ctx.morphStyle;
2943
3155
  }
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;
3156
+ function ignoreValueOfActiveElement(possibleActiveElement, ctx) {
3157
+ return ctx.ignoreActiveValue && possibleActiveElement === document.activeElement;
2969
3158
  }
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) {
3159
+ function morphOldNodeTo(oldNode, newContent, ctx) {
3160
+ if (ctx.ignoreActive && oldNode === document.activeElement) ; else if (newContent == null) {
3161
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3162
+ oldNode.remove();
3163
+ ctx.callbacks.afterNodeRemoved(oldNode);
3164
+ return null;
3165
+ } else if (!isSoftMatch(oldNode, newContent)) {
3166
+ if (ctx.callbacks.beforeNodeRemoved(oldNode) === false) return oldNode;
3167
+ if (ctx.callbacks.beforeNodeAdded(newContent) === false) return oldNode;
3168
+ oldNode.parentElement.replaceChild(newContent, oldNode);
3169
+ ctx.callbacks.afterNodeAdded(newContent);
3170
+ ctx.callbacks.afterNodeRemoved(oldNode);
3171
+ return newContent;
3172
+ } else {
3173
+ if (ctx.callbacks.beforeNodeMorphed(oldNode, newContent) === false) return oldNode;
3174
+ if (oldNode instanceof HTMLHeadElement && ctx.head.ignore) ; else if (oldNode instanceof HTMLHeadElement && ctx.head.style !== "morph") {
3175
+ handleHeadElement(newContent, oldNode, ctx);
3176
+ } else {
3177
+ syncNodeFrom(newContent, oldNode, ctx);
3178
+ if (!ignoreValueOfActiveElement(oldNode, ctx)) {
3179
+ morphChildren(newContent, oldNode, ctx);
3180
+ }
3181
+ }
3182
+ ctx.callbacks.afterNodeMorphed(oldNode, newContent);
3183
+ return oldNode;
3184
+ }
3185
+ }
3186
+ function morphChildren(newParent, oldParent, ctx) {
3187
+ let nextNewChild = newParent.firstChild;
3188
+ let insertionPoint = oldParent.firstChild;
3189
+ let newChild;
3190
+ while (nextNewChild) {
3191
+ newChild = nextNewChild;
3192
+ nextNewChild = newChild.nextSibling;
3193
+ if (insertionPoint == null) {
3194
+ if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3195
+ oldParent.appendChild(newChild);
3196
+ ctx.callbacks.afterNodeAdded(newChild);
3197
+ removeIdsFromConsideration(ctx, newChild);
3198
+ continue;
3199
+ }
3200
+ if (isIdSetMatch(newChild, insertionPoint, ctx)) {
3201
+ morphOldNodeTo(insertionPoint, newChild, ctx);
3202
+ insertionPoint = insertionPoint.nextSibling;
3203
+ removeIdsFromConsideration(ctx, newChild);
3204
+ continue;
3205
+ }
3206
+ let idSetMatch = findIdSetMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3207
+ if (idSetMatch) {
3208
+ insertionPoint = removeNodesBetween(insertionPoint, idSetMatch, ctx);
3209
+ morphOldNodeTo(idSetMatch, newChild, ctx);
3210
+ removeIdsFromConsideration(ctx, newChild);
3211
+ continue;
3212
+ }
3213
+ let softMatch = findSoftMatch(newParent, oldParent, newChild, insertionPoint, ctx);
3214
+ if (softMatch) {
3215
+ insertionPoint = removeNodesBetween(insertionPoint, softMatch, ctx);
3216
+ morphOldNodeTo(softMatch, newChild, ctx);
3217
+ removeIdsFromConsideration(ctx, newChild);
3218
+ continue;
3219
+ }
2980
3220
  if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
2981
- oldParent.appendChild(newChild);
3221
+ oldParent.insertBefore(newChild, insertionPoint);
2982
3222
  ctx.callbacks.afterNodeAdded(newChild);
2983
3223
  removeIdsFromConsideration(ctx, newChild);
2984
- continue;
2985
3224
  }
2986
- if (isIdSetMatch(newChild, insertionPoint, ctx)) {
2987
- morphOldNodeTo(insertionPoint, newChild, ctx);
3225
+ while (insertionPoint !== null) {
3226
+ let tempNode = insertionPoint;
2988
3227
  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;
3228
+ removeNode(tempNode, ctx);
3005
3229
  }
3006
- if (ctx.callbacks.beforeNodeAdded(newChild) === false) return;
3007
- oldParent.insertBefore(newChild, insertionPoint);
3008
- ctx.callbacks.afterNodeAdded(newChild);
3009
- removeIdsFromConsideration(ctx, newChild);
3010
3230
  }
3011
- while (insertionPoint !== null) {
3012
- let tempNode = insertionPoint;
3013
- insertionPoint = insertionPoint.nextSibling;
3014
- removeNode(tempNode, ctx);
3231
+ function ignoreAttribute(attr, to, updateType, ctx) {
3232
+ if (attr === "value" && ctx.ignoreActiveValue && to === document.activeElement) {
3233
+ return true;
3234
+ }
3235
+ return ctx.callbacks.beforeAttributeUpdated(attr, to, updateType) === false;
3015
3236
  }
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);
3237
+ function syncNodeFrom(from, to, ctx) {
3238
+ let type = from.nodeType;
3239
+ if (type === 1) {
3240
+ const fromAttributes = from.attributes;
3241
+ const toAttributes = to.attributes;
3242
+ for (const fromAttribute of fromAttributes) {
3243
+ if (ignoreAttribute(fromAttribute.name, to, "update", ctx)) {
3244
+ continue;
3245
+ }
3246
+ if (to.getAttribute(fromAttribute.name) !== fromAttribute.value) {
3247
+ to.setAttribute(fromAttribute.name, fromAttribute.value);
3248
+ }
3249
+ }
3250
+ for (let i = toAttributes.length - 1; 0 <= i; i--) {
3251
+ const toAttribute = toAttributes[i];
3252
+ if (ignoreAttribute(toAttribute.name, to, "remove", ctx)) {
3253
+ continue;
3254
+ }
3255
+ if (!from.hasAttribute(toAttribute.name)) {
3256
+ to.removeAttribute(toAttribute.name);
3257
+ }
3026
3258
  }
3027
3259
  }
3028
- for (const toAttribute of toAttributes) {
3029
- if (!from.hasAttribute(toAttribute.name)) {
3030
- to.removeAttribute(toAttribute.name);
3260
+ if (type === 8 || type === 3) {
3261
+ if (to.nodeValue !== from.nodeValue) {
3262
+ to.nodeValue = from.nodeValue;
3031
3263
  }
3032
3264
  }
3033
- }
3034
- if (type === 8 || type === 3) {
3035
- if (to.nodeValue !== from.nodeValue) {
3036
- to.nodeValue = from.nodeValue;
3265
+ if (!ignoreValueOfActiveElement(to, ctx)) {
3266
+ syncInputValue(from, to, ctx);
3037
3267
  }
3038
3268
  }
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;
3269
+ function syncBooleanAttribute(from, to, attributeName, ctx) {
3270
+ if (from[attributeName] !== to[attributeName]) {
3271
+ let ignoreUpdate = ignoreAttribute(attributeName, to, "update", ctx);
3272
+ if (!ignoreUpdate) {
3273
+ to[attributeName] = from[attributeName];
3274
+ }
3275
+ if (from[attributeName]) {
3276
+ if (!ignoreUpdate) {
3277
+ to.setAttribute(attributeName, from[attributeName]);
3278
+ }
3279
+ } else {
3280
+ if (!ignoreAttribute(attributeName, to, "remove", ctx)) {
3281
+ to.removeAttribute(attributeName);
3282
+ }
3283
+ }
3054
3284
  }
3055
3285
  }
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);
3286
+ function syncInputValue(from, to, ctx) {
3287
+ if (from instanceof HTMLInputElement && to instanceof HTMLInputElement && from.type !== "file") {
3288
+ let fromValue = from.value;
3289
+ let toValue = to.value;
3290
+ syncBooleanAttribute(from, to, "checked", ctx);
3291
+ syncBooleanAttribute(from, to, "disabled", ctx);
3292
+ if (!from.hasAttribute("value")) {
3293
+ if (!ignoreAttribute("value", to, "remove", ctx)) {
3294
+ to.value = "";
3295
+ to.removeAttribute("value");
3296
+ }
3297
+ } else if (fromValue !== toValue) {
3298
+ if (!ignoreAttribute("value", to, "update", ctx)) {
3299
+ to.setAttribute("value", fromValue);
3300
+ to.value = fromValue;
3301
+ }
3302
+ }
3303
+ } else if (from instanceof HTMLOptionElement) {
3304
+ syncBooleanAttribute(from, to, "selected", ctx);
3305
+ } else if (from instanceof HTMLTextAreaElement && to instanceof HTMLTextAreaElement) {
3306
+ let fromValue = from.value;
3307
+ let toValue = to.value;
3308
+ if (ignoreAttribute("value", to, "update", ctx)) {
3309
+ return;
3310
+ }
3311
+ if (fromValue !== toValue) {
3312
+ to.value = fromValue;
3313
+ }
3314
+ if (to.firstChild && to.firstChild.nodeValue !== fromValue) {
3315
+ to.firstChild.nodeValue = fromValue;
3316
+ }
3064
3317
  }
3065
3318
  }
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") {
3319
+ function handleHeadElement(newHeadTag, currentHead, ctx) {
3320
+ let added = [];
3321
+ let removed = [];
3322
+ let preserved = [];
3323
+ let nodesToAppend = [];
3324
+ let headMergeStyle = ctx.head.style;
3325
+ let srcToNewHeadNodes = new Map;
3326
+ for (const newHeadChild of newHeadTag.children) {
3327
+ srcToNewHeadNodes.set(newHeadChild.outerHTML, newHeadChild);
3328
+ }
3329
+ for (const currentHeadElt of currentHead.children) {
3330
+ let inNewContent = srcToNewHeadNodes.has(currentHeadElt.outerHTML);
3331
+ let isReAppended = ctx.head.shouldReAppend(currentHeadElt);
3332
+ let isPreserved = ctx.head.shouldPreserve(currentHeadElt);
3333
+ if (inNewContent || isPreserved) {
3091
3334
  if (isReAppended) {
3092
3335
  removed.push(currentHeadElt);
3093
- nodesToAppend.push(currentHeadElt);
3336
+ } else {
3337
+ srcToNewHeadNodes.delete(currentHeadElt.outerHTML);
3338
+ preserved.push(currentHeadElt);
3094
3339
  }
3095
3340
  } else {
3096
- if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3097
- removed.push(currentHeadElt);
3341
+ if (headMergeStyle === "append") {
3342
+ if (isReAppended) {
3343
+ removed.push(currentHeadElt);
3344
+ nodesToAppend.push(currentHeadElt);
3345
+ }
3346
+ } else {
3347
+ if (ctx.head.shouldRemove(currentHeadElt) !== false) {
3348
+ removed.push(currentHeadElt);
3349
+ }
3098
3350
  }
3099
3351
  }
3100
3352
  }
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);
3353
+ nodesToAppend.push(...srcToNewHeadNodes.values());
3354
+ let promises = [];
3355
+ for (const newNode of nodesToAppend) {
3356
+ let newElt = document.createRange().createContextualFragment(newNode.outerHTML).firstChild;
3357
+ if (ctx.callbacks.beforeNodeAdded(newElt) !== false) {
3358
+ if (newElt.href || newElt.src) {
3359
+ let resolve = null;
3360
+ let promise = new Promise((function(_resolve) {
3361
+ resolve = _resolve;
3362
+ }));
3363
+ newElt.addEventListener("load", (function() {
3364
+ resolve();
3365
+ }));
3366
+ promises.push(promise);
3367
+ }
3368
+ currentHead.appendChild(newElt);
3369
+ ctx.callbacks.afterNodeAdded(newElt);
3370
+ added.push(newElt);
3116
3371
  }
3117
- currentHead.appendChild(newElt);
3118
- ctx.callbacks.afterNodeAdded(newElt);
3119
- added.push(newElt);
3120
3372
  }
3121
- }
3122
- for (const removedElement of removed) {
3123
- if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3124
- currentHead.removeChild(removedElement);
3125
- ctx.callbacks.afterNodeRemoved(removedElement);
3373
+ for (const removedElement of removed) {
3374
+ if (ctx.callbacks.beforeNodeRemoved(removedElement) !== false) {
3375
+ currentHead.removeChild(removedElement);
3376
+ ctx.callbacks.afterNodeRemoved(removedElement);
3377
+ }
3126
3378
  }
3379
+ ctx.head.afterHeadMorphed(currentHead, {
3380
+ added: added,
3381
+ kept: preserved,
3382
+ removed: removed
3383
+ });
3384
+ return promises;
3385
+ }
3386
+ function noOp() {}
3387
+ function mergeDefaults(config) {
3388
+ let finalConfig = {};
3389
+ Object.assign(finalConfig, defaults);
3390
+ Object.assign(finalConfig, config);
3391
+ finalConfig.callbacks = {};
3392
+ Object.assign(finalConfig.callbacks, defaults.callbacks);
3393
+ Object.assign(finalConfig.callbacks, config.callbacks);
3394
+ finalConfig.head = {};
3395
+ Object.assign(finalConfig.head, defaults.head);
3396
+ Object.assign(finalConfig.head, config.head);
3397
+ return finalConfig;
3398
+ }
3399
+ function createMorphContext(oldNode, newContent, config) {
3400
+ config = mergeDefaults(config);
3401
+ return {
3402
+ target: oldNode,
3403
+ newContent: newContent,
3404
+ config: config,
3405
+ morphStyle: config.morphStyle,
3406
+ ignoreActive: config.ignoreActive,
3407
+ ignoreActiveValue: config.ignoreActiveValue,
3408
+ idMap: createIdMap(oldNode, newContent),
3409
+ deadIds: new Set,
3410
+ callbacks: config.callbacks,
3411
+ head: config.head
3412
+ };
3127
3413
  }
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;
3414
+ function isIdSetMatch(node1, node2, ctx) {
3415
+ if (node1 == null || node2 == null) {
3416
+ return false;
3178
3417
  }
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;
3418
+ if (node1.nodeType === node2.nodeType && node1.tagName === node2.tagName) {
3419
+ if (node1.id !== "" && node1.id === node2.id) {
3420
+ return true;
3421
+ } else {
3422
+ return getIdIntersectionCount(ctx, node1, node2) > 0;
3213
3423
  }
3214
- potentialMatch = potentialMatch.nextSibling;
3215
3424
  }
3425
+ return false;
3216
3426
  }
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;
3427
+ function isSoftMatch(node1, node2) {
3428
+ if (node1 == null || node2 == null) {
3429
+ return false;
3230
3430
  }
3231
- if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3232
- siblingSoftMatchCount++;
3233
- nextSibling = nextSibling.nextSibling;
3234
- if (siblingSoftMatchCount >= 2) {
3235
- return null;
3431
+ return node1.nodeType === node2.nodeType && node1.tagName === node2.tagName;
3432
+ }
3433
+ function removeNodesBetween(startInclusive, endExclusive, ctx) {
3434
+ while (startInclusive !== endExclusive) {
3435
+ let tempNode = startInclusive;
3436
+ startInclusive = startInclusive.nextSibling;
3437
+ removeNode(tempNode, ctx);
3438
+ }
3439
+ removeIdsFromConsideration(ctx, endExclusive);
3440
+ return endExclusive.nextSibling;
3441
+ }
3442
+ function findIdSetMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3443
+ let newChildPotentialIdCount = getIdIntersectionCount(ctx, newChild, oldParent);
3444
+ let potentialMatch = null;
3445
+ if (newChildPotentialIdCount > 0) {
3446
+ let potentialMatch = insertionPoint;
3447
+ let otherMatchCount = 0;
3448
+ while (potentialMatch != null) {
3449
+ if (isIdSetMatch(newChild, potentialMatch, ctx)) {
3450
+ return potentialMatch;
3451
+ }
3452
+ otherMatchCount += getIdIntersectionCount(ctx, potentialMatch, newContent);
3453
+ if (otherMatchCount > newChildPotentialIdCount) {
3454
+ return null;
3455
+ }
3456
+ potentialMatch = potentialMatch.nextSibling;
3236
3457
  }
3237
3458
  }
3238
- potentialSoftMatch = potentialSoftMatch.nextSibling;
3459
+ return potentialMatch;
3239
3460
  }
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>/)) {
3461
+ function findSoftMatch(newContent, oldParent, newChild, insertionPoint, ctx) {
3462
+ let potentialSoftMatch = insertionPoint;
3463
+ let nextSibling = newChild.nextSibling;
3464
+ let siblingSoftMatchCount = 0;
3465
+ while (potentialSoftMatch != null) {
3466
+ if (getIdIntersectionCount(ctx, potentialSoftMatch, newContent) > 0) {
3467
+ return null;
3468
+ }
3469
+ if (isSoftMatch(newChild, potentialSoftMatch)) {
3470
+ return potentialSoftMatch;
3471
+ }
3472
+ if (isSoftMatch(nextSibling, potentialSoftMatch)) {
3473
+ siblingSoftMatchCount++;
3474
+ nextSibling = nextSibling.nextSibling;
3475
+ if (siblingSoftMatchCount >= 2) {
3476
+ return null;
3477
+ }
3478
+ }
3479
+ potentialSoftMatch = potentialSoftMatch.nextSibling;
3480
+ }
3481
+ return potentialSoftMatch;
3482
+ }
3483
+ function parseContent(newContent) {
3484
+ let parser = new DOMParser;
3485
+ let contentWithSvgsRemoved = newContent.replace(/<svg(\s[^>]*>|>)([\s\S]*?)<\/svg>/gim, "");
3486
+ if (contentWithSvgsRemoved.match(/<\/html>/) || contentWithSvgsRemoved.match(/<\/head>/) || contentWithSvgsRemoved.match(/<\/body>/)) {
3487
+ let content = parser.parseFromString(newContent, "text/html");
3488
+ if (contentWithSvgsRemoved.match(/<\/html>/)) {
3489
+ content.generatedByIdiomorph = true;
3490
+ return content;
3491
+ } else {
3492
+ let htmlElement = content.firstChild;
3493
+ if (htmlElement) {
3494
+ htmlElement.generatedByIdiomorph = true;
3495
+ return htmlElement;
3496
+ } else {
3497
+ return null;
3498
+ }
3499
+ }
3500
+ } else {
3501
+ let responseDoc = parser.parseFromString("<body><template>" + newContent + "</template></body>", "text/html");
3502
+ let content = responseDoc.body.querySelector("template").content;
3249
3503
  content.generatedByIdiomorph = true;
3250
3504
  return content;
3505
+ }
3506
+ }
3507
+ function normalizeContent(newContent) {
3508
+ if (newContent == null) {
3509
+ const dummyParent = document.createElement("div");
3510
+ return dummyParent;
3511
+ } else if (newContent.generatedByIdiomorph) {
3512
+ return newContent;
3513
+ } else if (newContent instanceof Node) {
3514
+ const dummyParent = document.createElement("div");
3515
+ dummyParent.append(newContent);
3516
+ return dummyParent;
3251
3517
  } else {
3252
- let htmlElement = content.firstChild;
3253
- if (htmlElement) {
3254
- htmlElement.generatedByIdiomorph = true;
3255
- return htmlElement;
3256
- } else {
3257
- return null;
3518
+ const dummyParent = document.createElement("div");
3519
+ for (const elt of [ ...newContent ]) {
3520
+ dummyParent.append(elt);
3258
3521
  }
3522
+ return dummyParent;
3259
3523
  }
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);
3524
+ }
3525
+ function insertSiblings(previousSibling, morphedNode, nextSibling) {
3526
+ let stack = [];
3527
+ let added = [];
3528
+ while (previousSibling != null) {
3529
+ stack.push(previousSibling);
3530
+ previousSibling = previousSibling.previousSibling;
3531
+ }
3532
+ while (stack.length > 0) {
3533
+ let node = stack.pop();
3534
+ added.push(node);
3535
+ morphedNode.parentElement.insertBefore(node, morphedNode);
3536
+ }
3537
+ added.push(morphedNode);
3538
+ while (nextSibling != null) {
3539
+ stack.push(nextSibling);
3540
+ added.push(nextSibling);
3541
+ nextSibling = nextSibling.nextSibling;
3282
3542
  }
3283
- return dummyParent;
3543
+ while (stack.length > 0) {
3544
+ morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3545
+ }
3546
+ return added;
3547
+ }
3548
+ function findBestNodeMatch(newContent, oldNode, ctx) {
3549
+ let currentElement;
3550
+ currentElement = newContent.firstChild;
3551
+ let bestElement = currentElement;
3552
+ let score = 0;
3553
+ while (currentElement) {
3554
+ let newScore = scoreElement(currentElement, oldNode, ctx);
3555
+ if (newScore > score) {
3556
+ bestElement = currentElement;
3557
+ score = newScore;
3558
+ }
3559
+ currentElement = currentElement.nextSibling;
3560
+ }
3561
+ return bestElement;
3284
3562
  }
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;
3563
+ function scoreElement(node1, node2, ctx) {
3564
+ if (isSoftMatch(node1, node2)) {
3565
+ return .5 + getIdIntersectionCount(ctx, node1, node2);
3566
+ }
3567
+ return 0;
3293
3568
  }
3294
- while (stack.length > 0) {
3295
- let node = stack.pop();
3296
- added.push(node);
3297
- morphedNode.parentElement.insertBefore(node, morphedNode);
3569
+ function removeNode(tempNode, ctx) {
3570
+ removeIdsFromConsideration(ctx, tempNode);
3571
+ if (ctx.callbacks.beforeNodeRemoved(tempNode) === false) return;
3572
+ tempNode.remove();
3573
+ ctx.callbacks.afterNodeRemoved(tempNode);
3298
3574
  }
3299
- added.push(morphedNode);
3300
- while (nextSibling != null) {
3301
- stack.push(nextSibling);
3302
- added.push(nextSibling);
3303
- nextSibling = nextSibling.nextSibling;
3575
+ function isIdInConsideration(ctx, id) {
3576
+ return !ctx.deadIds.has(id);
3304
3577
  }
3305
- while (stack.length > 0) {
3306
- morphedNode.parentElement.insertBefore(stack.pop(), morphedNode.nextSibling);
3578
+ function idIsWithinNode(ctx, id, targetNode) {
3579
+ let idSet = ctx.idMap.get(targetNode) || EMPTY_SET;
3580
+ return idSet.has(id);
3307
3581
  }
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;
3582
+ function removeIdsFromConsideration(ctx, node) {
3583
+ let idSet = ctx.idMap.get(node) || EMPTY_SET;
3584
+ for (const id of idSet) {
3585
+ ctx.deadIds.add(id);
3321
3586
  }
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
3587
  }
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;
3588
+ function getIdIntersectionCount(ctx, node1, node2) {
3589
+ let sourceSet = ctx.idMap.get(node1) || EMPTY_SET;
3590
+ let matchCount = 0;
3591
+ for (const id of sourceSet) {
3592
+ if (isIdInConsideration(ctx, id) && idIsWithinNode(ctx, id, node2)) {
3593
+ ++matchCount;
3594
+ }
3363
3595
  }
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);
3596
+ return matchCount;
3597
+ }
3598
+ function populateIdMapForNode(node, idMap) {
3599
+ let nodeParent = node.parentElement;
3600
+ let idElements = node.querySelectorAll("[id]");
3601
+ for (const elt of idElements) {
3602
+ let current = elt;
3603
+ while (current !== nodeParent && current != null) {
3604
+ let idSet = idMap.get(current);
3605
+ if (idSet == null) {
3606
+ idSet = new Set;
3607
+ idMap.set(current, idSet);
3608
+ }
3609
+ idSet.add(elt.id);
3610
+ current = current.parentElement;
3378
3611
  }
3379
- idSet.add(elt.id);
3380
- current = current.parentElement;
3381
3612
  }
3382
3613
  }
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
- };
3614
+ function createIdMap(oldContent, newContent) {
3615
+ let idMap = new Map;
3616
+ populateIdMapForNode(oldContent, idMap);
3617
+ populateIdMapForNode(newContent, idMap);
3618
+ return idMap;
3619
+ }
3620
+ return {
3621
+ morph: morph,
3622
+ defaults: defaults
3623
+ };
3624
+ }();
3395
3625
 
3396
3626
  class MorphRenderer extends Renderer {
3397
3627
  async render() {
@@ -3412,7 +3642,7 @@ class MorphRenderer extends Renderer {
3412
3642
  }
3413
3643
  #morphElements(currentElement, newElement, morphStyle = "outerHTML") {
3414
3644
  this.isMorphingTurboFrame = this.#isFrameReloadedWithMorph(currentElement);
3415
- idiomorph.morph(currentElement, newElement, {
3645
+ Idiomorph.morph(currentElement, newElement, {
3416
3646
  morphStyle: morphStyle,
3417
3647
  callbacks: {
3418
3648
  beforeNodeAdded: this.#shouldAddElement,
@@ -3525,6 +3755,9 @@ class PageRenderer extends Renderer {
3525
3755
  this.copyNewHeadScriptElements();
3526
3756
  await mergedHeadElements;
3527
3757
  await newStylesheetElements;
3758
+ if (this.willRender) {
3759
+ this.removeUnusedHeadStylesheetElements();
3760
+ }
3528
3761
  }
3529
3762
  async replaceBody() {
3530
3763
  await this.preservingPermanentElements((async () => {
@@ -3548,6 +3781,11 @@ class PageRenderer extends Renderer {
3548
3781
  document.head.appendChild(activateScriptElement(element));
3549
3782
  }
3550
3783
  }
3784
+ removeUnusedHeadStylesheetElements() {
3785
+ for (const element of this.unusedHeadStylesheetElements) {
3786
+ document.head.removeChild(element);
3787
+ }
3788
+ }
3551
3789
  async mergeProvisionalElements() {
3552
3790
  const newHeadElements = [ ...this.newHeadProvisionalElements ];
3553
3791
  for (const element of this.currentHeadProvisionalElements) {
@@ -3600,6 +3838,12 @@ class PageRenderer extends Renderer {
3600
3838
  async assignNewBody() {
3601
3839
  await this.renderElement(this.currentElement, this.newElement);
3602
3840
  }
3841
+ get unusedHeadStylesheetElements() {
3842
+ return this.oldHeadStylesheetElements.filter((element => !(element.hasAttribute("data-turbo-permanent") || element.hasAttribute("data-tag-name"))));
3843
+ }
3844
+ get oldHeadStylesheetElements() {
3845
+ return this.currentHeadSnapshot.getStylesheetElementsNotInSnapshot(this.newHeadSnapshot);
3846
+ }
3603
3847
  get newHeadStylesheetElements() {
3604
3848
  return this.newHeadSnapshot.getStylesheetElementsNotInSnapshot(this.currentHeadSnapshot);
3605
3849
  }
@@ -3790,6 +4034,7 @@ class Session {
3790
4034
  adapter=new BrowserAdapter(this);
3791
4035
  pageObserver=new PageObserver(this);
3792
4036
  cacheObserver=new CacheObserver;
4037
+ linkPrefetchObserver=new LinkPrefetchObserver(this, document);
3793
4038
  linkClickObserver=new LinkClickObserver(this, window);
3794
4039
  formSubmitObserver=new FormSubmitObserver(this, document);
3795
4040
  scrollObserver=new ScrollObserver(this);
@@ -3803,14 +4048,18 @@ class Session {
3803
4048
  progressBarDelay=500;
3804
4049
  started=false;
3805
4050
  formMode="on";
4051
+ #pageRefreshDebouncePeriod=150;
3806
4052
  constructor(recentRequests) {
3807
4053
  this.recentRequests = recentRequests;
3808
4054
  this.preloader = new Preloader(this, this.view.snapshotCache);
4055
+ this.debouncedRefresh = this.refresh;
4056
+ this.pageRefreshDebouncePeriod = this.pageRefreshDebouncePeriod;
3809
4057
  }
3810
4058
  start() {
3811
4059
  if (!this.started) {
3812
4060
  this.pageObserver.start();
3813
4061
  this.cacheObserver.start();
4062
+ this.linkPrefetchObserver.start();
3814
4063
  this.formLinkClickObserver.start();
3815
4064
  this.linkClickObserver.start();
3816
4065
  this.formSubmitObserver.start();
@@ -3830,6 +4079,7 @@ class Session {
3830
4079
  if (this.started) {
3831
4080
  this.pageObserver.stop();
3832
4081
  this.cacheObserver.stop();
4082
+ this.linkPrefetchObserver.stop();
3833
4083
  this.formLinkClickObserver.stop();
3834
4084
  this.linkClickObserver.stop();
3835
4085
  this.formSubmitObserver.stop();
@@ -3886,6 +4136,13 @@ class Session {
3886
4136
  get restorationIdentifier() {
3887
4137
  return this.history.restorationIdentifier;
3888
4138
  }
4139
+ get pageRefreshDebouncePeriod() {
4140
+ return this.#pageRefreshDebouncePeriod;
4141
+ }
4142
+ set pageRefreshDebouncePeriod(value) {
4143
+ this.refresh = debounce(this.debouncedRefresh.bind(this), value);
4144
+ this.#pageRefreshDebouncePeriod = value;
4145
+ }
3889
4146
  shouldPreloadLink(element) {
3890
4147
  const isUnsafe = element.hasAttribute("data-turbo-method");
3891
4148
  const isStream = element.hasAttribute("data-turbo-stream");
@@ -3920,6 +4177,9 @@ class Session {
3920
4177
  return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
3921
4178
  }
3922
4179
  submittedFormLinkToLocation() {}
4180
+ canPrefetchRequestToLocation(link, location) {
4181
+ return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation);
4182
+ }
3923
4183
  willFollowLinkToLocation(link, location, event) {
3924
4184
  return this.elementIsNavigatable(link) && locationIsVisitable(location, this.snapshot.rootLocation) && this.applicationAllowsFollowingLinkToLocation(link, location, event);
3925
4185
  }
@@ -3984,17 +4244,17 @@ class Session {
3984
4244
  this.notifyApplicationBeforeCachingSnapshot();
3985
4245
  }
3986
4246
  }
3987
- allowsImmediateRender({element: element}, isPreview, options) {
3988
- const event = this.notifyApplicationBeforeRender(element, isPreview, options);
4247
+ allowsImmediateRender({element: element}, options) {
4248
+ const event = this.notifyApplicationBeforeRender(element, options);
3989
4249
  const {defaultPrevented: defaultPrevented, detail: {render: render}} = event;
3990
4250
  if (this.view.renderer && render) {
3991
4251
  this.view.renderer.renderElement = render;
3992
4252
  }
3993
4253
  return !defaultPrevented;
3994
4254
  }
3995
- viewRenderedSnapshot(_snapshot, isPreview, renderMethod) {
4255
+ viewRenderedSnapshot(_snapshot, _isPreview, renderMethod) {
3996
4256
  this.view.lastRenderedLocation = this.history.location;
3997
- this.notifyApplicationAfterRender(isPreview, renderMethod);
4257
+ this.notifyApplicationAfterRender(renderMethod);
3998
4258
  }
3999
4259
  preloadOnLoadLinksForView(element) {
4000
4260
  this.preloader.preloadOnLoadLinksForView(element);
@@ -4045,20 +4305,18 @@ class Session {
4045
4305
  notifyApplicationBeforeCachingSnapshot() {
4046
4306
  return dispatch("turbo:before-cache");
4047
4307
  }
4048
- notifyApplicationBeforeRender(newBody, isPreview, options) {
4308
+ notifyApplicationBeforeRender(newBody, options) {
4049
4309
  return dispatch("turbo:before-render", {
4050
4310
  detail: {
4051
4311
  newBody: newBody,
4052
- isPreview: isPreview,
4053
4312
  ...options
4054
4313
  },
4055
4314
  cancelable: true
4056
4315
  });
4057
4316
  }
4058
- notifyApplicationAfterRender(isPreview, renderMethod) {
4317
+ notifyApplicationAfterRender(renderMethod) {
4059
4318
  return dispatch("turbo:render", {
4060
4319
  detail: {
4061
- isPreview: isPreview,
4062
4320
  renderMethod: renderMethod
4063
4321
  }
4064
4322
  });
@@ -4386,7 +4644,7 @@ class FrameController {
4386
4644
  formSubmissionFinished({formElement: formElement}) {
4387
4645
  clearBusyState(formElement, this.#findFrameElement(formElement));
4388
4646
  }
4389
- allowsImmediateRender({element: newFrame}, _isPreview, options) {
4647
+ allowsImmediateRender({element: newFrame}, options) {
4390
4648
  const event = dispatch("turbo:before-frame-render", {
4391
4649
  target: this.element,
4392
4650
  detail: {