1/** 2 * Overlay header: find the element that should actually be sticky, publish the 3 * header height, keep the hero clear of the menu, and pick a readable colour 4 * scheme for whatever is underneath. 5 * 6 * Why any of this needs JS â measured on the live front page rather than 7 * assumed. WordPress renders a template part wrapped in its area tag, so the 8 * markup is: 9 * 10 * <div class="wp-site-blocks"> 11 * <header class="wp-block-template-part"> 12 * <div class="wp-block-group sin-header sin-header--overlay"> â ours 13 * </header> 14 * <p><!-- Hero Section --></p> 15 * <hr class="wp-block-separator"> 16 * <div class="wp-block-group sin-hero-manifest"> â the hero 17 * 18 * Three things follow from that, and all three were live bugs: 19 * 20 * 1. position:sticky is bounded by the element's PARENT box. Putting it on 21 * the group meant the parent was the <header> wrapper, exactly as tall as 22 * the header itself â zero travel, so it unstuck the moment you scrolled. 23 * The sticky behaviour has to sit on the .wp-site-blocks child. 24 * 25 * 2. The header group has no next sibling at all, so every rule and lookup 26 * written as "the thing after the header" silently did nothing. 27 * 28 * 3. Even one level up, the next sibling is a stray <p> holding an HTML 29 * comment, then an <hr>. The hero is the third sibling. Anything that 30 * assumes "next element = hero" pads a comment. 31 * 32 * Below the nav's overlay breakpoint none of this applies: the header is an 33 * ordinary solid bar in normal flow, so there is nothing to compensate for and 34 * nothing to adapt to. See the matching media query in style.css. 35 */ 36( function () { 37 'use strict'; 38 39 var header = document.querySelector( '.sin-header--overlay' ); 40 41 if ( ! header ) { 42 return; 43 } 44 45 // Matches core's overlayMenu:"mobile" breakpoint, which is where the 46 // hamburger takes over. style.css switches the overlay off at the same 47 // point â keep the two in step. 48 var MOBILE = '(max-width: 599.98px)'; 49 50 // How far you have to scroll before the bar goes frosted. Small enough to 51 // feel immediate, large enough that a rubber-band or a jittery trackpad 52 // doesn't flicker it. 53 var SCROLLED_AT = 24; 54 55 /** 56 * Walk up to the element that is a direct child of .wp-site-blocks. That is 57 * the box sticky has to live on, whatever wrappers WordPress puts between. 58 */ 59 function findShell( el ) { 60 var node = el; 61 62 while ( node.parentElement && node.parentElement !== document.body ) { 63 if ( node.parentElement.classList.contains( 'wp-site-blocks' ) ) { 64 return node; 65 } 66 node = node.parentElement; 67 } 68 69 return el; 70 } 71 72 var shell = findShell( header ); 73 shell.classList.add( 'sin-header-shell' ); 74 75 /** 76 * The hero is the first following sibling with real height. Empty 77 * paragraphs, separators and comment wrappers are skipped â a quarter of 78 * the viewport is well above anything incidental and well below any hero. 79 */ 80 function findHero() { 81 var node = shell.nextElementSibling; 82 var floor = window.innerHeight * 0.25; 83 var firstVisible = null; 84 85 while ( node ) { 86 if ( node.offsetHeight > 0 && ! firstVisible ) { 87 firstVisible = node; 88 } 89 if ( node.offsetHeight >= floor ) { 90 return node; 91 } 92 node = node.nextElementSibling; 93 } 94 95 return firstVisible; 96 } 97 98 var hero = findHero(); 99 100 function isMobile() { 101 return window.matchMedia( MOBILE ).matches; 102 } 103 104 /** 105 * Raise the hero's top padding only if it isn't already clear of the menu. 106 * 107 * This used to be a CSS rule with !important, which had to shout because 108 * hero blocks carry inline padding. Reading the computed value instead 109 * means the common case â a hero that already has generous top padding â 110 * is left completely alone. 111 */ 112 function clearHero() { 113 if ( ! hero ) { 114 return; 115 } 116 117 // Always clear our own previous value first: on mobile the header is in 118 // normal flow and needs no compensation at all, and on desktop each 119 // pass would otherwise read back what the last one wrote. 120 hero.style.paddingTop = ''; 121 122 if ( isMobile() ) { 123 return; 124 } 125 126 var needed = header.offsetHeight + 32; 127 var current = parseFloat( window.getComputedStyle( hero ).paddingTop ) || 0; 128 129 if ( current < needed ) { 130 hero.style.paddingTop = needed + 'px'; 131 } 132 } 133 134 function parseRgb( value ) { 135 var parts = /rgba?\(([^)]+)\)/.exec( value || '' ); 136 137 if ( ! parts ) { 138 return null; 139 } 140 141 var n = parts[ 1 ].split( /[\s,\/]+/ ).filter( Boolean ).map( parseFloat ); 142 143 return { 144 r: n[ 0 ], 145 g: n[ 1 ], 146 b: n[ 2 ], 147 a: n.length > 3 ? n[ 3 ] : 1, 148 }; 149 } 150 151 /** 152 * Is the thing under the header light or dark? 153 * 154 * The overlay header's white logo and menu only work over a dark hero. The 155 * front page's manifest hero is white, so the header rendered white on
156 * white â invisible until you selected the text. Rather than keeping a list 157 * of which heroes are dark, measure it. 158 * 159 * Walks up from the hero until it finds a background colour that is 160 * actually opaque, because a block with no colour of its own is showing 161 * whatever is behind it. A cover block keeps its overlay colour on a child 162 * span, so that is checked first. 163 */ 164 function backdropIsLight() { 165 var node = hero || document.body; 166 var found = null; 167 168 var dim = hero && hero.querySelector( '.wp-block-cover__background' ); 169 if ( dim ) { 170 var dimColor = parseRgb( window.getComputedStyle( dim ).backgroundColor ); 171 if ( dimColor && dimColor.a > 0.4 ) { 172 found = dimColor; 173 } 174 } 175 176 while ( ! found && node ) { 177 var color = parseRgb( window.getComputedStyle( node ).backgroundColor ); 178 if ( color && color.a > 0.5 ) { 179 found = color; 180 } 181 node = node.parentElement; 182 } 183 184 // Nothing opaque anywhere up the chain means the page background, which 185 // is white. 186 if ( ! found ) { 187 return true; 188 } 189 190 // Rec. 709 luma. The threshold sits above 0.5 on purpose: in the 191 // ambiguous middle, dark text on a mid tone stays readable where white 192 // text does not. 193 return ( 0.2126 * found.r + 0.7152 * found.g + 0.0722 * found.b ) / 255 > 0.55; 194 } 195 196 /** 197 * Frosted bar as soon as the page leaves the top. 198 * 199 * This was an IntersectionObserver watching the hero's bottom edge, from 200 * when the scrolled state was navy and could only be shown over light 201 * content. It's a translucent white bar now, which is readable over 202 * anything, so waiting for a whole section to pass just made the header 203 * look broken for the first screenful or two. 204 */ 205 function updateScrolled() { 206 header.classList.toggle( 'is-scrolled', window.scrollY > SCROLLED_AT ); 207 } 208 209 function refresh() { 210 document.documentElement.style.setProperty( 211 '--sin-header-h', 212 header.offsetHeight + 'px' 213 ); 214 215 clearHero(); 216 217 // On mobile the bar is solid white, so the light treatment is always 218 // the right one â no measuring needed. 219 header.classList.toggle( 'is-on-light', isMobile() || backdropIsLight() ); 220 221 updateScrolled(); 222 } 223 224 refresh(); 225 226 window.addEventListener( 'scroll', updateScrolled, { passive: true } ); 227 228 if ( 'ResizeObserver' in window ) { 229 new ResizeObserver( refresh ).observe( header ); 230 } else { 231 window.addEventListener( 'resize', refresh ); 232 } 233 234 /* 235 * Re-resolve once images have loaded. Heights measured before then can pick 236 * the wrong sibling as the hero â an image-led hero is 0px tall until its 237 * image arrives. 238 */ 239 window.addEventListener( 'load', function () { 240 hero = findHero(); 241 refresh(); 242 } ); 243}() );
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.