|
7 | 7 | <title>DSpace</title> |
8 | 8 | <meta name="viewport" content="width=device-width,minimum-scale=1"> |
9 | 9 | <meta http-equiv="cache-control" content="no-store"> |
| 10 | + <style id="__dspace-ssr-overlay-style"> |
| 11 | + /* Overlay used to mask the Angular 15 bootstrap re-render of the SSR DOM. |
| 12 | + See src/index.html bootstrap script + AppComponent.removeSsrOverlayWhenStable. |
| 13 | + The overlay element holds the SSR-rendered children moved out of <ds-app>, |
| 14 | + so it keeps every original Angular view-encapsulation attribute (_ngcontent-scXXX), |
| 15 | + inline style, and lifecycle context — i.e. it looks pixel-identical to what SSR sent. */ |
| 16 | + #__dspace_ssr_overlay { |
| 17 | + position: absolute; |
| 18 | + top: 0; |
| 19 | + left: 0; |
| 20 | + right: 0; |
| 21 | + width: 100%; |
| 22 | + z-index: 10000; |
| 23 | + background: #fff; |
| 24 | + pointer-events: none; |
| 25 | + } |
| 26 | + /* Keep <ds-app> taking layout space (height of CSR result) but hidden, so the page |
| 27 | + does not collapse the moment we drop the overlay. */ |
| 28 | + ds-app[data-dspace-ssr-hidden] { visibility: hidden; } |
| 29 | + </style> |
10 | 30 | </head> |
11 | 31 | <body> |
12 | 32 | <!-- dependencies --> |
13 | 33 | <script type="text/javascript" src="https://code.jquery.com/jquery-2.1.4.min.js"></script> |
14 | 34 | <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script> |
15 | 35 | <ds-app></ds-app> |
| 36 | +<script> |
| 37 | +/* |
| 38 | + Anti-flicker overlay. |
| 39 | + Why this exists: this codebase is on Angular 15 + ngUniversal. There is no provideClientHydration |
| 40 | + available before Angular 16, so on every browser load Angular tears down the entire SSR DOM and |
| 41 | + re-renders the component tree from scratch. The rebuild takes ~600-1500 ms on slow connections, |
| 42 | + during which the user sees the SSR view -> blank/half-built CSR view -> final CSR view. |
| 43 | + This script captures the SSR DOM as a non-interactive snapshot the moment it's parsed (before |
| 44 | + any module/main script runs - those are type=module and therefore deferred). While Angular |
| 45 | + rebuilds the real <ds-app> invisibly, the snapshot keeps the page looking stable. AppComponent |
| 46 | + removes the overlay once ApplicationRef.isStable settles. |
| 47 | +*/ |
| 48 | +(function () { |
| 49 | + if (typeof window === 'undefined' || typeof document === 'undefined') return; |
| 50 | + // Skip when Cypress is driving the page. The overlay duplicates SSR DOM (moved into the |
| 51 | + // overlay) alongside the CSR DOM (rendered into <ds-app>) during the masking window — so |
| 52 | + // any cy.get('#some-id').click() picks up two elements and fails. The overlay is a pure |
| 53 | + // UX nicety, and Cypress E2E doesn't measure visual smoothness anyway; bail early. |
| 54 | + if (typeof window.Cypress !== 'undefined') return; |
| 55 | + try { |
| 56 | + var app = document.querySelector('ds-app'); |
| 57 | + // If SSR was skipped for this route (excludePathPatterns), there are no children; nothing to mask. |
| 58 | + if (!app || !app.firstElementChild) return; |
| 59 | + if (document.getElementById('__dspace_ssr_overlay')) return; |
| 60 | + |
| 61 | + // Critical: Angular's BrowserModule.withServerTransition removes ALL <style ng-transition="..."> |
| 62 | + // tags during bootstrap. Those tags hold the component-scoped CSS that styles the SSR DOM |
| 63 | + // via attribute selectors like [_ngcontent-scXXX]. If we don't preserve copies, the overlay |
| 64 | + // renders unstyled. Clone the SSR styles into <style data-dspace-ssr-keep> tags that Angular |
| 65 | + // ignores, so the overlay keeps looking like the page the user already saw. |
| 66 | + var ssrStyles = document.querySelectorAll('style[ng-transition]'); |
| 67 | + var keptStyles = []; |
| 68 | + for (var i = 0; i < ssrStyles.length; i++) { |
| 69 | + var copy = document.createElement('style'); |
| 70 | + copy.setAttribute('data-dspace-ssr-keep', ''); |
| 71 | + copy.textContent = ssrStyles[i].textContent; |
| 72 | + document.head.appendChild(copy); |
| 73 | + keptStyles.push(copy); |
| 74 | + } |
| 75 | + |
| 76 | + // Build overlay and MOVE (not clone) the SSR children into it. Moving keeps every live DOM |
| 77 | + // detail (Angular's view-encapsulation attributes, computed inline styles, image-load state) |
| 78 | + // so the overlay is pixel-identical to what the user already saw before Angular booted. |
| 79 | + // Cloning via innerHTML loses parent-context-dependent rendering. |
| 80 | + // |
| 81 | + // Accessibility note: we deliberately do NOT set aria-hidden on the overlay. The overlay |
| 82 | + // *is* the visible page during the masking window, so assistive technologies should read |
| 83 | + // it. The original <ds-app> underneath gets visibility:hidden (via attribute + CSS rule), |
| 84 | + // which removes both itself and its children from the accessibility tree. |
| 85 | + var overlay = document.createElement('div'); |
| 86 | + overlay.id = '__dspace_ssr_overlay'; |
| 87 | + while (app.firstChild) { |
| 88 | + overlay.appendChild(app.firstChild); |
| 89 | + } |
| 90 | + |
| 91 | + // Hide the now-empty <ds-app> so Angular can rebuild into it invisibly. We use an attribute |
| 92 | + // (CSS in <head> targets it) rather than setting .style.visibility directly so Angular's |
| 93 | + // template doesn't blow it away on first ChangeDetection. |
| 94 | + app.setAttribute('data-dspace-ssr-hidden', ''); |
| 95 | + document.body.appendChild(overlay); |
| 96 | + |
| 97 | + var removing = false; |
| 98 | + window.__dspaceRemoveSsrOverlay = function () { |
| 99 | + // Re-entrancy guard: null the pointer up-front so a racing isStable + 15s safety |
| 100 | + // fallback cannot start two interleaving fade-out passes (which would re-remove |
| 101 | + // the kept styles from underneath the first pass). |
| 102 | + if (removing) return; |
| 103 | + removing = true; |
| 104 | + window.__dspaceRemoveSsrOverlay = null; |
| 105 | + |
| 106 | + var el = document.getElementById('__dspace_ssr_overlay'); |
| 107 | + if (!el) return; |
| 108 | + app.removeAttribute('data-dspace-ssr-hidden'); |
| 109 | + el.style.transition = 'opacity 150ms ease-out'; |
| 110 | + el.style.opacity = '0'; |
| 111 | + setTimeout(function () { |
| 112 | + if (el && el.parentNode) el.parentNode.removeChild(el); |
| 113 | + for (var i = 0; i < keptStyles.length; i++) { |
| 114 | + if (keptStyles[i].parentNode) keptStyles[i].parentNode.removeChild(keptStyles[i]); |
| 115 | + } |
| 116 | + keptStyles = []; |
| 117 | + }, 200); |
| 118 | + }; |
| 119 | + |
| 120 | + // Safety net: if the app never reaches isStable (e.g. permanent HTTP poll), remove anyway. |
| 121 | + setTimeout(function () { |
| 122 | + if (typeof window.__dspaceRemoveSsrOverlay === 'function') { |
| 123 | + window.__dspaceRemoveSsrOverlay(); |
| 124 | + } |
| 125 | + }, 15000); |
| 126 | + } catch (e) { |
| 127 | + // Don't let the overlay logic kill the page, but surface the failure so a silently-broken |
| 128 | + // flicker fix is at least diagnosable in DevTools. |
| 129 | + if (window.console && typeof console.warn === 'function') { |
| 130 | + console.warn('[dspace-ssr-overlay] disabled due to error:', e); |
| 131 | + } |
| 132 | + } |
| 133 | +})(); |
| 134 | +</script> |
16 | 135 | </body> |
17 | 136 |
|
18 | 137 | <!-- do not include client bundle, it is injected with Zone already loaded --> |
|
0 commit comments