1/** 2 * This is an automatic html-based animation system. 3 * How to use AnimationControls: 4 * Step 1. Define an animation root by assigning [data-animate-root] to an element 5 * Step 2. Set triggers by adding data-animate-trigger="click" to an triggering element 6 * Step 3: Set the trigger's targets by assigning data-animate-select"{selector}" on the triggering element 7 * Step 4: Set the effects on the target by assigning data-animate-effect="{AnimationEffect}" on a target element 8 * Set up any additional required attributes for the effeect 9 * For AnimationEffect.Classify, set data-animate-class="myclass" 10 * For AnimationEffect.Accordion, set data-animate-siblings="{selector}" 11 * Step 5: Set the initial state of the animation effect directly in the HTML. 12 * For AnimationEffect.Slide, set data-animation-active to make the element visible 13 * For AnimationEffect.Classify, set data-animation-active if you want it to be "active" 14 * - This distinction only matters if you're programmatically planning to deactivate things 15 * For AnimationEffect.Accordion, set data-animation-active to make the element visible 16 */ 17 18/** 19 * @readonly 20 * @enum {string} 21 */ 22const AnimationEffect = { 23 Slide: 'slide', 24 Classify: 'classify', 25 Accordion: 'accordion', 26}; 27 28const AnimationResizer = new ResizeObserver((entries) => { 29 for (const entry of entries) { 30 const measure = entry.target; 31 const target = measure.parentNode; 32 target.style.height = `${measure.scrollHeight}px`; 33 } 34}); 35 36class AnimationControls { 37 static setupAllAnimationGroups() { 38 const roots = document.querySelectorAll('[data-animate-root]'); 39 roots.forEach(AnimationControls.setupSingleAnimationGroup) 40 } 41 42 /** 43 * @param {HTMLElement} root 44 */ 45 static setupSingleAnimationGroup(root) { 46 const triggers = [...root.querySelectorAll('[data-animate-trigger]')].filter((trigger) => { 47 const ancestor = AnimationControls.findAncestor(trigger, ['[data-animate-root]']); 48 return ancestor == root; 49 }); 50 triggers.forEach((trigger) => { 51 AnimationControls.setupAnimationTrigger(root, trigger); 52 }); 53 } 54 55 /** 56 * @param {HTMLElement} root 57 * @param {HTMLElement} trigger 58 * @returns {() => void} 59 */ 60 static setupAnimationTrigger(root, trigger) { 61 const selector = trigger.getAttribute('data-animate-select'); 62 const targets = AnimationControls.selectTargets(root, selector); 63 64 for (const target of targets) { 65 /** 66 * @type {AnimationEffect | null} 67 */ 68 const effect = target.getAttribute('data-animate-effect'); 69 if (effect != null) { 70 AnimationControls.initSingleAnimation(target, effect); 71 } 72 } 73 74 const event = trigger.getAttribute('data-animate-trigger'); 75 const animateTargets = AnimationControls.makeAnimationHandler(root, targets); 76 if (event == 'click') { 77 trigger.addEventListener('click', animateTargets); 78 } 79 return animateTargets; 80 } 81 82 /** 83 * @param {HTMLElement} root 84 * @param {HTMLElement[]} targets 85 * @returns {(event: MouseEvent) => void} 86 */ 87 static makeAnimationHandler(root, targets) { 88 return (_event) => { 89 for (const target of targets) { 90 const effect = target.getAttribute('data-animate-effect'); 91 if (effect == null) { 92 continue; 93 } 94 AnimationControls.runSingleAnimation(root, target, effect); 95 } 96 }; 97 } 98 99 /** 100 * @param {HTMLElement} root 101 * @param {HTMLElement} target 102 * @param {AnimationEffect} effect 103 */ 104 static runSingleAnimation(root, target, effect) { 105 if (effect === AnimationEffect.Slide) { 106 const active = target.getAttribute('data-animate-active'); 107 if (active) { 108 AnimationControls.slideShrink(target); 109 } else { 110 AnimationControls.slideExpand(target, true); 111 } 112 } else if (effect === AnimationEffect.Accordion) { 113 const active = target.getAttribute('data-animate-active'); 114 if (active) { 115 AnimationControls.slideShrink(target); 116 } else { 117 const selector = target.getAttribute('data-animate-siblings'); 118 const activeSiblings = AnimationControls.selectTargets(root, selector).filter((sib) => { 119 return ( 120 sib.getAttribute('data-animate-effect') === AnimationEffect.Accordion && 121 sib.getAttribute('data-animate-active') 122 ); 123 }); 124 for (const sibling of activeSiblings) { 125 AnimationControls.slideShrink(sibling); 126 } 127 AnimationControls.slideExpand(target); 128 } 129 } else if (effect === AnimationEffect.Classify) { 130 const active = target.getAttribute('data-animate-active'); 131 if (active) { 132 AnimationControls.unclassify(target); 133 } else { 134 AnimationControls.classify(target); 135 } 136 } 137 } 138 139 /** 140 * @param {HTMLElement} target 141 * @param {AnimationEffect} effect 142 */ 143 static initSingleAnimation(target, effect) { 144 if (effect === AnimationEffect.Slide) { 145 const measure = AnimationControls.createMeasure(target); 146 const active = target.getAttribute('data-animate-active'); 147 if (active) { 148 target.style.height = `${measure.scrollHeight}px`; 149 AnimationResizer.observe(measure); 150 } else { 151 target.style.height = '0'; 152 } 153 } else if (effect === AnimationEffect.Accordion) { 154 const measure = AnimationControls.createMeasure(target); 155 const active = target.getAttribute('data-animate-active'); 156 if (active) { 157 target.style.height = `${measure.scrollHeight}px`; 158 AnimationResizer.observe(measure); 159 } else { 160 target.style.height = '0'; 161 } 162 } else if (effect === AnimationEffect.Classify) {
163 // Do nothing 164 } 165 } 166 167 /** 168 * @param {HTMLElement} root 169 * @param {string} selector 170 * @returns {HTMLElement[]} 171 */ 172 static selectTargets(root, selector) { 173 const targets = [ 174 ...root.querySelectorAll(selector), 175 ]; 176 if (root.matches(selector)) { 177 targets.push(root); 178 } 179 return targets; 180 } 181 182 /** 183 * @param {HTMLElement} target 184 */ 185 static slideExpand(target) { 186 target.setAttribute('data-animate-active', true); 187 target.style.height = `${target.scrollHeight}px`; 188 const measure = target.querySelector('.measure'); 189 AnimationResizer.observe(measure); 190 } 191 192 /** 193 * @param {HTMLElement} target 194 */ 195 static slideShrink(target) { 196 target.removeAttribute('data-animate-active'); 197 const measure = target.querySelector('.measure'); 198 AnimationResizer.unobserve(measure); 199 target.style.height = '0'; 200 } 201 202 /** 203 * @param {HTMLElement} target 204 */ 205 static classify(target) { 206 if (target.hasAttribute('data-animate-active')) { 207 return; 208 } 209 const klass = target.getAttribute('data-animate-class'); 210 target.setAttribute('data-animate-active', true); 211 if (target.classList.contains(klass)) { 212 target.classList.remove(klass); 213 } else { 214 target.classList.add(klass); 215 } 216 } 217 218 /** 219 * @param {HTMLElement} target 220 */ 221 static unclassify(target) { 222 if (!target.hasAttribute('data-animate-active')) { 223 return; 224 } 225 const klass = target.getAttribute('data-animate-class'); 226 target.removeAttribute('data-animate-active'); 227 if (target.classList.contains(klass)) { 228 target.classList.remove(klass); 229 } else { 230 target.classList.add(klass); 231 } 232 } 233 234 /** 235 * @param {HTMLElement} node 236 * @param {selector} string 237 * @returns {HTMLElement} 238 */ 239 static findAncestor(node, selector) { 240 while (node != null) { 241 if (node.matches(selector)) { 242 return node; 243 } else { 244 node = node.parentElement; 245 } 246 } 247 return null; 248 } 249 250 /** 251 * @param {HTMLElement} node 252 * @returns {HTMLElement} 253 */ 254 static createMeasure(node) { 255 const measure = document.createElement('div'); 256 measure.className = 'measure'; 257 while (node.childNodes.length) { 258 measure.appendChild(node.firstChild); 259 } 260 node.appendChild(measure); 261 return measure; 262 } 263} 264 265function updateHeaderShadow(header) { 266 if (window.scrollY == 0) { 267 header.classList.add('noshadow'); 268 } else { 269 header.classList.remove('noshadow'); 270 } 271 return true; 272} 273 274function onDocumentScrollNavbarUpdate(header, actions) { 275 // Maybe the header shadow needs updating 276 updateHeaderShadow(header); 277 278 // If the navbar actions are open, then it means we need to close everything 279 // This just an optimizzation, technically this block is idempotent 280 if (actions.getAttribute('data-animate-active')) { 281 // Collapse all the top navbar menu accordions 282 const navAccordions = header.querySelectorAll('.top-menu-accordion'); 283 for (const nava of navAccordions) { 284 AnimationControls.slideShrink(nava); 285 } 286 287 // Simulate the nav-toggle disabling if needed 288 const toggler = header.querySelector('.nav-toggle'); 289 const targets = header.querySelectorAll(toggler.getAttribute('data-animate-select')); 290 for (const target of targets) { 291 const effect = target.getAttribute('data-animate-effect'); 292 const active = target.getAttribute('data-animate-active'); 293 if (!active) { 294 continue; 295 } if (effect == AnimationEffect.Slide) { 296 AnimationControls.slideShrink(target); 297 } else if (effect == AnimationEffect.Accordion) { 298 AnimationControls.slideShrink(target); 299 } else if (effect == AnimationEffect.Classify) { 300 AnimationControls.unclassify(target); 301 } 302 } 303 } 304} 305 306/** 307 * @param {HTMLElement} header 308 */ 309function setupHeaderLogic(header) { 310 // Setup special case for site-header-actions to prevent it from being wimbly wombly 311 // due to nested accordions resizing 312 const actions = header.querySelector('.site-header-actions'); 313 const content = actions.querySelector('.site-header-actions-content'); 314 const nested = new ResizeObserver(() => { 315 if (actions.getAttribute('data-animate-active')) {
316 actions.style.transition = 'none'; 317 requestAnimationFrame(() => { 318 actions.style.removeProperty('transition'); 319 }); 320 } 321 }); 322 nested.observe(content); 323 324 // Collapse all header accordions whenever nav is toggled 325 const toggler = header.querySelector('.nav-toggle'); 326 toggler.addEventListener('click', () => { 327 requestAnimationFrame(() => { 328 const isOpen = actions.getAttribute('data-animate-active'); 329 if (isOpen) { 330 return; 331 } 332 // Collapse all the top navbar menu accordions 333 const navAccordions = header.querySelectorAll('.top-menu-accordion'); 334 for (const nava of navAccordions) { 335 AnimationControls.slideShrink(nava); 336 } 337 }); 338 }); 339 340 // Collapse all navbar when document is scrolled 341 window.addEventListener('scroll', () => onDocumentScrollNavbarUpdate(header, actions)); 342} 343 344function setupAnalytics() { 345 function analyticsButtonClick(event) { 346 const gevent = { 347 page: document.title.trim().replace(/\s+/g, ' '), 348 }; 349 if (event.target.id) { 350 gevent.anchor = event.target.id.trim(); 351 } 352 if (event.target.textContent) { 353 gevent.text = event.target.textContent.trim().replace(/\s+/g, ' '); 354 } 355 gtag('event', 'button_click', gevent); 356 } 357 const wpbuttons = document.querySelectorAll(".wp-block-button,.track-click"); 358 for (const button of wpbuttons) { 359 button.addEventListener('click', analyticsButtonClick); 360 } 361} 362 363function setupLinkScrolling() { 364 /** 365 * Helper function to remove trailing slash from URL paths 366 * @param {string} url 367 * @returns {string} 368 */ 369 function noslash(url) { 370 return url.replace(/\/$/, ''); 371 } 372 373 // Select all anchor links 374 const anchorLinks = document.querySelectorAll('a'); 375 376 anchorLinks.forEach(link => { 377 // Get the full URL of the link 378 const linkHref = link.getAttribute('href'); 379 const currentUrl = noslash(window.location.href.split('#')[0]); 380 const targetUrl = new URL(linkHref, currentUrl); 381 382 // Ignore if the link doesn't point to the same page 383 if ( 384 targetUrl.origin !== window.location.origin || 385 noslash(targetUrl.pathname) !== noslash(window.location.pathname) 386 ) { 387 return; 388 } 389 390 link.addEventListener('click', function (event) { 391 // Prevent default link behavior 392 event.preventDefault(); 393 394 // Get the target element by ID 395 const targetId = targetUrl.hash.substring(1); 396 const targetElement = document.getElementById(targetId); 397 398 if (targetElement) { 399 // Calculate the position of the target element 400 const scrollTop = window.pageYOffset || document.documentElement.scrollTop; 401 const targetPosition = scrollTop + targetElement.getBoundingClientRect().top; 402 403 const magicOffset = 150; 404 window.scrollTo({ 405 top: targetPosition - magicOffset, 406 behavior: 'smooth' 407 }); 408 409 // Update the URL hash without jumping 410 history.pushState(null, null, `#${targetId}`); 411 } else if (targetId === "") { 412 // Handle the case where the href is just "#" or "#" 413 // Scroll to the top of the page 414 window.scrollTo({ 415 top: 0, 416 behavior: 'smooth' 417 }); 418 419 // Update the URL hash without jumping 420 history.pushState(null, null, "#"); 421 } 422 }); 423 }); 424} 425 426function setupEverything() { 427 const header = document.querySelector('.site-header'); 428 if (header != null) { 429 updateHeaderShadow(header); 430 setupHeaderLogic(header); 431 } 432 433 AnimationControls.setupAllAnimationGroups(); 434 435 const videos = document.querySelectorAll('video'); 436 for (const video of videos) { 437 new Plyr(video, { 438 iconUrl: '/wp-content/themes/invested/vendor/plyr.svg', 439 }); 440 } 441 442 setupLinkScrolling(); 443 444 setupAnalytics(); 445} 446 447document.addEventListener("DOMContentLoaded", setupEverything);
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.