1. /* eslint-disable dot-notation */
    
  2. 
    
  3. // Shared implementation and constants between the inline script and external
    
  4. // runtime instruction sets.
    
  5. 
    
  6. export const COMMENT_NODE = 8;
    
  7. export const SUSPENSE_START_DATA = '$';
    
  8. export const SUSPENSE_END_DATA = '/$';
    
  9. export const SUSPENSE_PENDING_START_DATA = '$?';
    
  10. export const SUSPENSE_FALLBACK_START_DATA = '$!';
    
  11. 
    
  12. // TODO: Symbols that are referenced outside this module use dynamic accessor
    
  13. // notation instead of dot notation to prevent Closure's advanced compilation
    
  14. // mode from renaming. We could use extern files instead, but I couldn't get it
    
  15. // working. Closure converts it to a dot access anyway, though, so it's not an
    
  16. // urgent issue.
    
  17. 
    
  18. export function clientRenderBoundary(
    
  19.   suspenseBoundaryID,
    
  20.   errorDigest,
    
  21.   errorMsg,
    
  22.   errorComponentStack,
    
  23. ) {
    
  24.   // Find the fallback's first element.
    
  25.   const suspenseIdNode = document.getElementById(suspenseBoundaryID);
    
  26.   if (!suspenseIdNode) {
    
  27.     // The user must have already navigated away from this tree.
    
  28.     // E.g. because the parent was hydrated.
    
  29.     return;
    
  30.   }
    
  31.   // Find the boundary around the fallback. This is always the previous node.
    
  32.   const suspenseNode = suspenseIdNode.previousSibling;
    
  33.   // Tag it to be client rendered.
    
  34.   suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
    
  35.   // assign error metadata to first sibling
    
  36.   const dataset = suspenseIdNode.dataset;
    
  37.   if (errorDigest) dataset['dgst'] = errorDigest;
    
  38.   if (errorMsg) dataset['msg'] = errorMsg;
    
  39.   if (errorComponentStack) dataset['stck'] = errorComponentStack;
    
  40.   // Tell React to retry it if the parent already hydrated.
    
  41.   if (suspenseNode['_reactRetry']) {
    
  42.     suspenseNode['_reactRetry']();
    
  43.   }
    
  44. }
    
  45. 
    
  46. export function completeBoundary(suspenseBoundaryID, contentID, errorDigest) {
    
  47.   const contentNode = document.getElementById(contentID);
    
  48.   // We'll detach the content node so that regardless of what happens next we don't leave in the tree.
    
  49.   // This might also help by not causing recalcing each time we move a child from here to the target.
    
  50.   contentNode.parentNode.removeChild(contentNode);
    
  51. 
    
  52.   // Find the fallback's first element.
    
  53.   const suspenseIdNode = document.getElementById(suspenseBoundaryID);
    
  54.   if (!suspenseIdNode) {
    
  55.     // The user must have already navigated away from this tree.
    
  56.     // E.g. because the parent was hydrated. That's fine there's nothing to do
    
  57.     // but we have to make sure that we already deleted the container node.
    
  58.     return;
    
  59.   }
    
  60.   // Find the boundary around the fallback. This is always the previous node.
    
  61.   const suspenseNode = suspenseIdNode.previousSibling;
    
  62. 
    
  63.   if (!errorDigest) {
    
  64.     // Clear all the existing children. This is complicated because
    
  65.     // there can be embedded Suspense boundaries in the fallback.
    
  66.     // This is similar to clearSuspenseBoundary in ReactFiberConfigDOM.
    
  67.     // TODO: We could avoid this if we never emitted suspense boundaries in fallback trees.
    
  68.     // They never hydrate anyway. However, currently we support incrementally loading the fallback.
    
  69.     const parentInstance = suspenseNode.parentNode;
    
  70.     let node = suspenseNode.nextSibling;
    
  71.     let depth = 0;
    
  72.     do {
    
  73.       if (node && node.nodeType === COMMENT_NODE) {
    
  74.         const data = node.data;
    
  75.         if (data === SUSPENSE_END_DATA) {
    
  76.           if (depth === 0) {
    
  77.             break;
    
  78.           } else {
    
  79.             depth--;
    
  80.           }
    
  81.         } else if (
    
  82.           data === SUSPENSE_START_DATA ||
    
  83.           data === SUSPENSE_PENDING_START_DATA ||
    
  84.           data === SUSPENSE_FALLBACK_START_DATA
    
  85.         ) {
    
  86.           depth++;
    
  87.         }
    
  88.       }
    
  89. 
    
  90.       const nextNode = node.nextSibling;
    
  91.       parentInstance.removeChild(node);
    
  92.       node = nextNode;
    
  93.     } while (node);
    
  94. 
    
  95.     const endOfBoundary = node;
    
  96. 
    
  97.     // Insert all the children from the contentNode between the start and end of suspense boundary.
    
  98.     while (contentNode.firstChild) {
    
  99.       parentInstance.insertBefore(contentNode.firstChild, endOfBoundary);
    
  100.     }
    
  101. 
    
  102.     suspenseNode.data = SUSPENSE_START_DATA;
    
  103.   } else {
    
  104.     suspenseNode.data = SUSPENSE_FALLBACK_START_DATA;
    
  105.     suspenseIdNode.setAttribute('data-dgst', errorDigest);
    
  106.   }
    
  107. 
    
  108.   if (suspenseNode['_reactRetry']) {
    
  109.     suspenseNode['_reactRetry']();
    
  110.   }
    
  111. }
    
  112. 
    
  113. export function completeSegment(containerID, placeholderID) {
    
  114.   const segmentContainer = document.getElementById(containerID);
    
  115.   const placeholderNode = document.getElementById(placeholderID);
    
  116.   // We always expect both nodes to exist here because, while we might
    
  117.   // have navigated away from the main tree, we still expect the detached
    
  118.   // tree to exist.
    
  119.   segmentContainer.parentNode.removeChild(segmentContainer);
    
  120.   while (segmentContainer.firstChild) {
    
  121.     placeholderNode.parentNode.insertBefore(
    
  122.       segmentContainer.firstChild,
    
  123.       placeholderNode,
    
  124.     );
    
  125.   }
    
  126.   placeholderNode.parentNode.removeChild(placeholderNode);
    
  127. }
    
  128. 
    
  129. // This is the exact URL string we expect that Fizz renders if we provide a function action.
    
  130. // We use this for hydration warnings. It needs to be in sync with Fizz. Maybe makes sense
    
  131. // as a shared module for that reason.
    
  132. const EXPECTED_FORM_ACTION_URL =
    
  133.   // eslint-disable-next-line no-script-url
    
  134.   "javascript:throw new Error('A React form was unexpectedly submitted.')";
    
  135. 
    
  136. export function listenToFormSubmissionsForReplaying() {
    
  137.   // A global replay queue ensures actions are replayed in order.
    
  138.   // This event listener should be above the React one. That way when
    
  139.   // we preventDefault in React's handling we also prevent this event
    
  140.   // from queing it. Since React listens to the root and the top most
    
  141.   // container you can use is the document, the window is fine.
    
  142.   // eslint-disable-next-line no-restricted-globals
    
  143.   addEventListener('submit', event => {
    
  144.     if (event.defaultPrevented) {
    
  145.       // We let earlier events to prevent the action from submitting.
    
  146.       return;
    
  147.     }
    
  148.     const form = event.target;
    
  149.     const submitter = event['submitter'];
    
  150.     let action = form.action;
    
  151.     let formDataSubmitter = submitter;
    
  152.     if (submitter) {
    
  153.       const submitterAction = submitter.getAttribute('formAction');
    
  154.       if (submitterAction != null) {
    
  155.         // The submitter overrides the action.
    
  156.         action = submitterAction;
    
  157.         // If the submitter overrides the action, and it passes the test below,
    
  158.         // that means that it was a function action which conceptually has no name.
    
  159.         // Therefore, we exclude the submitter from the formdata.
    
  160.         formDataSubmitter = null;
    
  161.       }
    
  162.     }
    
  163.     if (action !== EXPECTED_FORM_ACTION_URL) {
    
  164.       // The form is a regular form action, we can bail.
    
  165.       return;
    
  166.     }
    
  167. 
    
  168.     // Prevent native navigation.
    
  169.     // This will also prevent other React's on the same page from listening.
    
  170.     event.preventDefault();
    
  171. 
    
  172.     // Take a snapshot of the FormData at the time of the event.
    
  173.     let formData;
    
  174.     if (formDataSubmitter) {
    
  175.       // The submitter's value should be included in the FormData.
    
  176.       // It should be in the document order in the form.
    
  177.       // Since the FormData constructor invokes the formdata event it also
    
  178.       // needs to be available before that happens so after construction it's too
    
  179.       // late. We use a temporary fake node for the duration of this event.
    
  180.       // TODO: FormData takes a second argument that it's the submitter but this
    
  181.       // is fairly new so not all browsers support it yet. Switch to that technique
    
  182.       // when available.
    
  183.       const temp = document.createElement('input');
    
  184.       temp.name = formDataSubmitter.name;
    
  185.       temp.value = formDataSubmitter.value;
    
  186.       formDataSubmitter.parentNode.insertBefore(temp, formDataSubmitter);
    
  187.       formData = new FormData(form);
    
  188.       temp.parentNode.removeChild(temp);
    
  189.     } else {
    
  190.       formData = new FormData(form);
    
  191.     }
    
  192. 
    
  193.     // Queue for replaying later. This field could potentially be shared with multiple
    
  194.     // Reacts on the same page since each one will preventDefault for the next one.
    
  195.     // This means that this protocol is shared with any React version that shares the same
    
  196.     // javascript: URL placeholder value. So we might not be the first to declare it.
    
  197.     // We attach it to the form's root node, which is the shared environment context
    
  198.     // where we preserve sequencing and where we'll pick it up from during hydration.
    
  199.     // In practice, this is just the same as document but we might support shadow trees
    
  200.     // in the future.
    
  201.     const root = form.getRootNode();
    
  202.     (root['$$reactFormReplay'] = root['$$reactFormReplay'] || []).push(
    
  203.       form,
    
  204.       submitter,
    
  205.       formData,
    
  206.     );
    
  207.   });
    
  208. }