|
| 1 | +/** |
| 2 | + * Internal dependencies |
| 3 | + */ |
| 4 | +import { shortestCommonSupersequence } from './scs'; |
| 5 | + |
1 | 6 | export type StyleElement = HTMLLinkElement | HTMLStyleElement;
|
2 | 7 |
|
| 8 | +const isStyleEqual = ( a: StyleElement, b: StyleElement ): boolean => { |
| 9 | + if ( a === b ) { |
| 10 | + return true; |
| 11 | + } |
| 12 | + |
| 13 | + const [ normalizedA, normalizedB ] = [ a, b ].map( ( element ) => { |
| 14 | + if ( element.getAttribute( 'media' ) === 'preload' ) { |
| 15 | + element = element.cloneNode( true ) as StyleElement; |
| 16 | + const { originalMedia } = element.dataset; |
| 17 | + if ( originalMedia ) { |
| 18 | + element.setAttribute( 'media', originalMedia ); |
| 19 | + element.removeAttribute( 'data-original-media' ); |
| 20 | + } else { |
| 21 | + element.removeAttribute( 'media' ); |
| 22 | + } |
| 23 | + } |
| 24 | + return element; |
| 25 | + } ); |
| 26 | + |
| 27 | + const result = normalizedA.isEqualNode( normalizedB ); |
| 28 | + |
| 29 | + return result; |
| 30 | +}; |
| 31 | + |
| 32 | +export function updateStylesWithSCS( |
| 33 | + X: StyleElement[], |
| 34 | + Y: StyleElement[], |
| 35 | + parent: Element = window.document.head |
| 36 | +) { |
| 37 | + if ( X.length === 0 ) { |
| 38 | + return Y.map( ( element ) => { |
| 39 | + parent.appendChild( element ); |
| 40 | + return prepareStyleElement( element ); |
| 41 | + } ); |
| 42 | + } |
| 43 | + |
| 44 | + const scs = shortestCommonSupersequence( X, Y, isStyleEqual ); |
| 45 | + const xLength = X.length; |
| 46 | + const yLength = Y.length; |
| 47 | + const promises = []; |
| 48 | + let last = X[ xLength - 1 ]; |
| 49 | + let xIndex = 0; |
| 50 | + let yIndex = 0; |
| 51 | + |
| 52 | + for ( const element of scs ) { |
| 53 | + if ( xIndex < xLength && isStyleEqual( X[ xIndex ], element ) ) { |
| 54 | + if ( yIndex < yLength && isStyleEqual( Y[ yIndex ], element ) ) { |
| 55 | + promises.push( Promise.resolve( X[ xIndex ] ) ); |
| 56 | + yIndex++; |
| 57 | + } |
| 58 | + xIndex++; |
| 59 | + } else { |
| 60 | + const clone = Y[ yIndex ].cloneNode( true ) as StyleElement; |
| 61 | + promises.push( prepareStyleElement( clone ) ); |
| 62 | + if ( xIndex < xLength ) { |
| 63 | + X[ xIndex ].before( clone ); |
| 64 | + yIndex++; |
| 65 | + } else { |
| 66 | + last.after( clone ); |
| 67 | + last = clone; |
| 68 | + } |
| 69 | + } |
| 70 | + } |
| 71 | + |
| 72 | + return promises; |
| 73 | +} |
| 74 | + |
| 75 | +const prepareStyleElement = ( |
| 76 | + element: StyleElement |
| 77 | +): Promise< StyleElement > => { |
| 78 | + if ( element.media ) { |
| 79 | + element.dataset.originalMedia = element.media; |
| 80 | + } |
| 81 | + |
| 82 | + element.media = 'preload'; |
| 83 | + |
| 84 | + if ( element instanceof HTMLStyleElement ) { |
| 85 | + return Promise.resolve( element ); |
| 86 | + } |
| 87 | + |
| 88 | + const loadPromise = new Promise< HTMLLinkElement >( ( resolve, reject ) => { |
| 89 | + element.addEventListener( 'load', () => resolve( element ) ); |
| 90 | + element.addEventListener( 'error', ( event ) => { |
| 91 | + const { href } = event.target as HTMLLinkElement; |
| 92 | + reject( |
| 93 | + Error( |
| 94 | + `The style sheet with the following URL failed to load. ${ href }` |
| 95 | + ) |
| 96 | + ); |
| 97 | + } ); |
| 98 | + } ); |
| 99 | + |
| 100 | + return loadPromise; |
| 101 | +}; |
| 102 | + |
3 | 103 | const styleSheetCache = new Map< string, Promise< StyleElement >[] >();
|
4 | 104 |
|
5 | 105 | export const prepareStyles = (
|
6 | 106 | doc: Document,
|
7 | 107 | url: string = ( doc.location || window.location ).href
|
8 | 108 | ): Promise< StyleElement >[] => {
|
9 | 109 | if ( ! styleSheetCache.has( url ) ) {
|
10 |
| - if ( doc !== window.document ) { |
11 |
| - window.document.head.appendChild( |
12 |
| - window.document.createComment( |
13 |
| - `@wordpress/interactivity-router: prefetched styles for ${ url }` |
14 |
| - ) |
15 |
| - ); |
16 |
| - } |
17 |
| - styleSheetCache.set( |
18 |
| - url, |
19 |
| - [ ...doc.querySelectorAll( 'style,link[rel=stylesheet]' ) ].map( |
20 |
| - ( element: StyleElement ) => { |
21 |
| - if ( doc === window.document ) { |
22 |
| - return Promise.resolve( element ); |
23 |
| - } |
24 |
| - |
25 |
| - const cloned = element.cloneNode( true ) as |
26 |
| - | HTMLStyleElement |
27 |
| - | HTMLLinkElement; |
28 |
| - |
29 |
| - if ( cloned.media ) { |
30 |
| - cloned.dataset.originalMedia = cloned.media; |
31 |
| - } |
32 |
| - cloned.media = 'preload'; |
33 |
| - |
34 |
| - if ( cloned instanceof HTMLStyleElement ) { |
35 |
| - window.document.head.appendChild( cloned ); |
36 |
| - return Promise.resolve( cloned ); |
37 |
| - } |
38 |
| - const loadPromise = new Promise< HTMLLinkElement >( |
39 |
| - ( resolve, reject ) => { |
40 |
| - cloned.addEventListener( |
41 |
| - 'load', |
42 |
| - () => resolve( cloned ), |
43 |
| - { once: true } |
44 |
| - ); |
45 |
| - cloned.addEventListener( |
46 |
| - 'error', |
47 |
| - ( event ) => { |
48 |
| - const { href } = |
49 |
| - event.target as HTMLLinkElement; |
50 |
| - reject( |
51 |
| - Error( |
52 |
| - `The style sheet with the following URL failed to load. ${ href }` |
53 |
| - ) |
54 |
| - ); |
55 |
| - }, |
56 |
| - { once: true } |
57 |
| - ); |
58 |
| - } |
59 |
| - ); |
60 |
| - |
61 |
| - window.document.head.appendChild( cloned ); |
62 |
| - return loadPromise; |
63 |
| - } |
| 110 | + const currentStyleElements = Array.from( |
| 111 | + window.document.querySelectorAll< StyleElement >( |
| 112 | + 'style,link[rel=stylesheet]' |
64 | 113 | )
|
65 | 114 | );
|
66 |
| - if ( doc !== window.document ) { |
67 |
| - window.document.head.appendChild( |
68 |
| - window.document.createComment( |
69 |
| - `@wordpress/interactivity-router: end of prefetched styles` |
70 |
| - ) |
71 |
| - ); |
72 |
| - } |
| 115 | + const newStyleElements = Array.from( |
| 116 | + doc.querySelectorAll< StyleElement >( 'style,link[rel=stylesheet]' ) |
| 117 | + ); |
| 118 | + |
| 119 | + // Set styles in order. |
| 120 | + const stylePromises = updateStylesWithSCS( |
| 121 | + currentStyleElements, |
| 122 | + newStyleElements |
| 123 | + ); |
| 124 | + |
| 125 | + styleSheetCache.set( url, stylePromises ); |
73 | 126 | }
|
74 | 127 | return styleSheetCache.get( url );
|
75 | 128 | };
|
|
0 commit comments