1"use strict"; 2 3/*jshint sub:true*/ 4// Small global functions can go here. 5// Custom scripts can go in their own file. 6// Globally used scripts are loaded via layout.twig 7// Page specific scripts are loaded via {% block footer_scripts %} 8 9var fitVidInit = function fitVidInit() { 10 $('.container').fitVids(); 11}; 12 13// lodash style throttle function to not respond to every event 14var hrThrottle = function hrThrottle(fn, wait) { 15 var time = Date.now(); 16 return function () { 17 if (time + wait - Date.now() < 0) { 18 fn(); 19 time = Date.now(); 20 } 21 }; 22}; 23var smoothScrollInit = function smoothScrollInit() { 24 $('a:not(.no-smooth-scroll)').smoothScroll(); 25}; 26var matchHeightInit = function matchHeightInit() { 27 //basic match height 28 $('[data-mh]:not(.js-mh-by-row-false)').matchHeight(); 29 30 // alternate, not by row (add js-mh-by-row-false class) 31 $('[data-mh].js-mh-by-row-false').matchHeight({ 32 byRow: false 33 }); 34}; 35 36//hrMatchWidths (which calls hrMatchWidthsResize) 37// Need one element's width (target) to match another width of another element (source)? Add these pair of data attributes with a unique common value 38// data-match-width-source="some-page-unique-value" 39// data-match-width-target="some-page-unique-value" 40 41var hrMatchWidthsResize = function hrMatchWidthsResize() { 42 document.querySelectorAll('[data-match-width-target]').forEach(function (mwTargetEl, i) { 43 var mwName = mwTargetEl.getAttribute('data-match-width-target'); 44 var mwSourceEl = document.querySelector('[data-match-width-source="' + mwName + '"]'); 45 if (mwSourceEl) { 46 mwTargetEl.style.width = mwSourceEl.offsetWidth + 'px'; 47 } 48 }); 49}; 50var hrMatchWidths = function hrMatchWidths() { 51 if (!document.querySelector('[data-match-width-target]')) { 52 return; 53 } 54 55 // carry out the matches 56 hrMatchWidthsResize(); 57 58 // if the window is resized carry them out again 59 window.addEventListener('resize', hrThrottle(hrMatchWidthsResize, 150)); 60}; 61var headerNav = function headerNav() { 62 // header menu navigation 63 $('#header-nav-btn').click(function () { 64 $('#header-nav-btn').toggleClass('is-active'); 65 }); 66 67 // better dropdown hover intent ( inspired by https://stackoverflow.com/a/42183824/462002 ) 68 $('.component-main-header').on('mouseenter mouseleave', '.dropdown', function (e) { 69 var $dropdown = $(e.target).closest('.dropdown'); 70 $dropdown.addClass('show'); 71 setTimeout(function () { 72 $dropdown[$dropdown.is(':hover') ? 'addClass' : 'removeClass']('show'); 73 }, 300); 74 }); 75 76 // header and footer current issue links (have hr-current-issue-link class) is populated 77 var current_issue_url = document.querySelector('[data-current-issue-url]').dataset.currentIssueUrl; 78 document.querySelectorAll('.hr-current-issue-link>a:first-child').forEach(function (el, i) { 79 if (i !== 4) { 80 el.setAttribute('href', current_issue_url); 81 } 82 }); 83}; 84 85// A widow (single word on it's own line) preventer. 86// Typically fills in the last line with previous word(s) 87// used for some header (h2, ..) and flow (p) tags 88// overrride by adding class .no-widont 89// inspired by http://justinhileman.info/article/a-jquery-widont-snippet/ 90var widont = function widont() { 91 var wRegex = new RegExp(/\S+/); 92 $('h2,h3,p').not('.no-widont').each(function () { 93 var $element = $(this); 94 95 // no need to un-widow one word strings with a nonbreaking space 96 if ($element.text().split(wRegex).length - 1 < 2) { 97 return; 98 } 99 100 // use .no-widont to opt out of using it on the element above (handy for react element, highlighed text, ..) 101 if ($element.hasClass('no-widont')) { 102 return; 103 } 104 $element.html($(this).html().replace(/\s([^\s<]{0,8})\s*$/, ' $1')); 105 }); 106}; 107 108// '+' and '-' to increase text, mostly used for flowing longer text (.wysiwyg-content, ..) 109var ariaFontAdjusts = function ariaFontAdjusts() { 110 var fontSizes = ['x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large', 'x4-large'], 111 nextFontSize; 112 var $content = $('.wysiwyg-content'); 113 114 // the '+' font increase link 115 $('.aria-font-adjusts .font-increase').on('click', function (e) { 116 e.preventDefault(); 117 var currentFontSize = $content.attr('font-size') || 'x-large'; 118 var nextFontSizeIndex = !!currentFontSize ? fontSizes.indexOf(currentFontSize) + 1 : 1; 119 if (nextFontSizeIndex < fontSizes.length) { 120 nextFontSize = fontSizes[nextFontSizeIndex % fontSizes.length]; 121 $content.data('font-size', nextFontSize); 122 $content.attr('font-size', nextFontSize); 123 $content.prop('font-size', nextFontSize); 124 } //otherwise keep as is 125 }); 126 127 // the '-' font decrease link 128 $('.aria-font-adjusts .font-decrease').on('click', function (e) { 129 e.preventDefault(); 130 var currentFontSize = $content.attr('font-size') || 'x-large'; 131 var nextFontSize; 132 var nextFontSizeIndex = !!currentFontSize ? fontSizes.indexOf(currentFontSize) - 1 : 1; 133 // console.log('currentFontSize', currentFontSize); 134 // console.log('nextFontSizeIndex', nextFontSizeIndex); 135 if (nextFontSizeIndex > 0) { 136 nextFontSize = fontSizes[nextFontSizeIndex % fontSizes.length]; 137 $content.data('font-size', nextFontSize); 138 $content.attr('font-size', nextFontSize); 139 $content.prop('font-size', nextFontSize); 140 } //otherwise keep as is 141 }); 142 143 // placement of the bar where it's used in featured 144 //TODO: no longer used after #125. confirm can be removed 145 // var sectionMetaBar = document.querySelector('body.article-featured .section-meta-bar'); 146 // if ( !!sectionMetaBar ){ 147 // document.querySelector('.wysiwyg-content').prepend(sectionMetaBar); 148 // sectionMetaBar.classList.remove('d-none'); 149 // } 150}; 151 152// feature image gallery (bootstrap carousel + modal) 153var featureGalleryInit = function featureGalleryInit() { 154 var $modal = $('#feature-galleries-modal'); 155 156 // turn on all feature galleries on page 157 document.querySelectorAll('.component-feature-gallery:not(.is-set)').forEach(function (galleryEl, i) { 158 var $gallery = $(galleryEl); 159 160 // zap various wpautop p and br (wpautop gremlins) 161 // .carousel-item p 162 $gallery.find('.carousel-item+p, .carousel-control-prev br, .carousel-control-next br, .carousel-control-prev+br').remove();
162 //wysiwig added autop 163 164 var galleryId = $gallery.data('id'); 165 166 //refresh carousel after changes (or start carousels just loaded in via ajax) 167 $gallery.carousel({ 168 // ride: 'carousel', 169 interval: false // debug: false for no autoplay. otherwise autoplay in ms 170 }); 171 172 // with the carousel dom/events settled. add additional carousel events 173 174 //events 175 176 //modael expand 177 $gallery.find('.carousel-item,.image-gallery-popup').on('click', function (e) { 178 e.preventDefault(); 179 var $gallery = $(this).closest('.component-feature-gallery'); 180 $gallery.appendTo($modal.find('.modal-body')); 181 $modal.modal(); 182 }); 183 }); 184 185 // closing the modal restores the slider gallery inline 186 $modal.on('hidden.bs.modal', function () { 187 var $modalGallery = $(this).find('.component-feature-gallery'); 188 var galleryId; 189 190 // is it a feature gallery or a single gallery post? restore accordingly 191 if (!!$modalGallery && $modalGallery.length >= 1) { 192 galleryId = $modalGallery.data('id'); 193 $modalGallery.appendTo($('.feature-gallery-holder[data-id=' + galleryId + ']')); 194 } else { 195 $modalGallery = $(this).find('.gallery-single-item'); 196 galleryId = $modalGallery.data('item-id'); 197 console.log('gallery-single-item'); 198 $modalGallery.appendTo($('.gallery-single-item-holder[data-item-id=' + galleryId + ']')); 199 } 200 }); 201}; 202 203//helper for the /galleries/ page run whenever new gallery images are added 204window.hrGalleriesPageHelper = function () { 205 if (!!!document.querySelector('.gallery-single-item')) { 206 return; 207 } 208 var $modal = $('.galleries-page-modal'); 209 $('.gallery-single-item:not(.set-up) .gallery-img-holder').on('click', function (e) { 210 e.preventDefault(); 211 // console.log($modal[0]); 212 $(this).closest('.gallery-single-item').appendTo($modal.find('.modal-body')); 213 $modal.modal(); 214 }); 215 $('.gallery-single-item:not(.set-up) ').addClass('set-up'); 216 217 // closing the modal restores the slider gallery inline 218 $modal.on('hidden.bs.modal', function () { 219 var $gallery_item = $modal.find('.gallery-single-item'); 220 221 // debugger; 222 $gallery_item.appendTo($('.gallery-single-item-holder[data-item-id="' + $gallery_item.data('item-id') + '"]')); 223 }); 224}; 225 226// misc smoothing of content inside wysiwyg-content class (e.g.: wpautop filtered content) 227var wysiwigContentHelper = function wysiwigContentHelper() { 228 if (!!!document.querySelector('.wysiwyg-content')) { 229 return; 230 } 231 232 //remove empty paragraphs (those tend to be accidental. br's are kept as they add intentional vertical space) 233 234 document.querySelectorAll('.wysiwyg-content .cols-2 p, .wysiwyg-content .cols-3 p').forEach(function (pEl, i) { 235 if (pEl.textContent.trim() === '') { 236 pEl.parentNode.removeChild(pEl); 237 } 238 }); 239 240 //more wysywig content added brs to zap 241 // document.querySelectorAll('.wysiwyg-content .footnote-link+br').forEach(function(brEl, i){ 242 // brEl.parentNode.removeChild(brEl); 243 // }); 244 245 // document.querySelectorAll('.wysiwyg-content .cols-2 br,.wysiwyg-content .cols-3 br').forEach(function(brEl, i){ 246 // brEl.parentNode.removeChild(brEl); 247 // }); 248}; 249var hrOffloadSidebarItem = function hrOffloadSidebarItem(sidebarEl) { 250 var sidebarElements = sidebarEl.querySelectorAll('.sidebar-item-loaded,.sidebar-ad2'); 251 if (sidebarElements.length === 0) { 252 return; 253 } 254 var currentFlexSection = sidebarEl.closest('.flex-section'); 255 var nextFlexSection = currentFlexSection.nextElementSibling; 256 var nextSidebarEl; 257 while (!!nextFlexSection) { 258 nextFlexSection = nextFlexSection.nextElementSibling; 259 nextSidebarEl = nextFlexSection.querySelector('aside'); 260 if (!!!nextSidebarEl) { 261 continue; 262 } 263 264 //otherwise, found a sidebar to offload? 265 //carry out offload 266 sidebarElements.forEach(function (elToOffload, i) { 267 // if (sidebarEl.offsetHeight > sidebarEl.parentElement.previousElementSibling.offsetHeight){ 268 // console.log('offloading', elToOffload); 269 nextSidebarEl.append(elToOffload); 270 // } 271 }); 272 break; 273 } 274}; 275 276// helper for article flex layout 277var articleFlexHelper = function articleFlexHelper() { 278 //only applies to flex layouts (in articles) 279 if (!!!document.querySelector('.article-content .flex-sections')) { 280 return; 281 } 282 283 //avoid "heavy sidebars" (taller than their sibling main content) via offloading excess to other sidebar 284 var contentRows = document.querySelectorAll('.row-article-flex-content'); 285 contentRows.forEach(function (flexRowEl, i) { 286 var mainEl = flexRowEl.querySelector('.row-article-flex-content .entry-content'); 287 var asideEl = flexRowEl.querySelector('.row-article-flex-content aside'); 288 if (!!!asideEl) { 289 // has a sidebar? (not full width) 290 return; 291 } 292 if (asideEl.offsetHeight > mainEl.offsetHeight) { 293 //heavy sidebar? offload an item 294 hrOffloadSidebarItem(asideEl); 295 } 296 }); 297}; 298var sidebarHelper = function sidebarHelper() { 299 // for articles, if no sidebar, show the alternate pdf link section 300 // sidebar hiding is tricky to figure out at template time as it depends on 301 //sections that come after the heading is rendered. So we test for it at page 302 // load and show the alternate if no sidebar is present. 303 if (document.body.classList.contains('single-article')) { 304 if (!document.querySelector('aside.sidebar,aside>.sidebar')) { 305 var pdfSection = document.querySelector('.pdf-section'); 306 if (pdfSection) { 307 pdfSection.classList.remove('d-none'); 308 } 309 } 310 } 311}; 312var modalSimple = function modalSimple() { 313 // for now, expectation is there's only one on a page 314 // TODO Coookie 315 316 document.querySelectorAll('.modal-simple').forEach(function (modalEl, i) { 317 // change to cycling through multiple modal-simples (some are on scroll) 318 var $modalSimple = $(modalEl); 319 320 // el.getAttribute('data-foo'); 321 322 var modal_id = modalEl.getAttribute('id'); 323 var modal_was_shown = Cookies.get(modal_id); 324 325 // more modal specific business logic for deciding whether to show this specific modal 326 if (!modal_was_shown) { 327 // for freebie article signup offer, if current article is fully showing (no leaky_paywall_message shown) and freebie offer wasn't used re
327cently 328 if (modalEl.id === 'hr-modal-paywall-freebie-article-signup') { 329 var isPaywalledSelector = window.location.href.includes('.loc/') ? '#paywall #leaky_paywall_message' : '#leaky_paywall_message'; 330 if (document.querySelector(isPaywalledSelector)) { 331 //not a freebie article (content not fully showing) 332 modal_was_shown = true; 333 } else { 334 // user is reading through a freebie. offer another if not recently offed 335 modal_was_shown = Cookies.get('hr_freebie_offer'); 336 } 337 } 338 } 339 340 // decided not to show this modal (already shown, etc.) ? nothing to do 341 if (modal_was_shown) { 342 return; 343 } 344 345 // is this going to be set up as a scroll triggered modal? (or just set up as one showing on page load?) 346 var scrollTriggerSelector = modalEl.getAttribute('data-scroll-trigger-selector'), 347 scrollTriggerEl; 348 if (scrollTriggerSelector) { 349 scrollTriggerEl = document.querySelector(scrollTriggerSelector); 350 if (scrollTriggerEl) { 351 // console.log('scrollTriggerEl', scrollTriggerEl); 352 353 var controller = new ScrollMagic.Controller(); 354 var scene = new ScrollMagic.Scene({ 355 reverse: true, 356 triggerElement: scrollTriggerEl, 357 triggerHook: 'onEnter' 358 }).addTo(controller).on('enter', function (e) { 359 // We've scrolled there! now what? 360 361 // console.log('hrAddArticleScrollEvent enter ', scrollTriggerEl, ' , direction:', e.scrollDirection); 362 363 $modalSimple.modal(); 364 $modalSimple.on('shown.bs.modal', function () { 365 document.body.classList.add('modal-open-modal-simple-freebie-article'); 366 }); 367 368 // closing the modal sets the cookie not to show again 369 $modalSimple.on('hidden.bs.modal', function () { 370 //set a cookie to check 371 document.body.classList.remove('modal-open-modal-simple-freebie-article'); 372 var modalExpiresDays = modalEl.getAttribute('data-modal-expires-days'); 373 if (modalExpiresDays) { 374 var cookieExpireDays = parseInt(modalExpiresDays); 375 if (cookieExpireDays >= 1) { 376 // 0 or -1 means no expiry 377 Cookies.set(modal_id, true, { 378 expires: parseInt(modalExpiresDays) * 24 * 60 * 60 379 }); 380 } 381 } else { 382 Cookies.set(modal_id, true, { 383 expires: 1000 * 24 * 60 * 60 384 }); // default is "one time" (1000 days) 385 } 386 }); 387 388 // close modal when a submit happens in it? 389 if (!$modalSimple.data('stay-open-on-submit')) { 390 $modalSimple.find('form').on('submit', function () { 391 $modalSimple.modal('hide'); 392 }); 393 } 394 scene = scene.destroy(); // this is a one time scroll event (only loading the modal once) 395 }); 396 } 397 } 398 }); 399}; 400 401// A more robust dropCap helper than ::first-letter (which includes punctuation 402// marks following the first letter in what's selected). Uses DOM TreeWalker to 403// find the first text node directly, avoiding innerHTML manipulation that can 404// corrupt <img> and other tag attributes (#1126). 405var dropCapHelper = function dropCapHelper() { 406 document.querySelectorAll('p.drop-cap,div.drop-cap').forEach(function (dropCapEl) { 407 var firstParagraphText = dropCapEl.textContent.trim(); 408 if (firstParagraphText.length <= 2) { 409 return; 410 } 411 412 // if it's a special character (doublequote) include the next letter in dropcapping 413 var specialCharacters = ['"', "\u201C"]; 414 var dropCappedLetters = specialCharacters.includes(firstParagraphText[0]) ? firstParagraphText[0] + firstParagraphText[1] : firstParagraphText[0]; 415 416 // Walk the DOM tree to find the first non-empty text node 417 var walker = document.createTreeWalker(dropCapEl, NodeFilter.SHOW_TEXT); 418 var textNode; 419 while (textNode = walker.nextNode()) { 420 if (textNode.textContent.trim()) break; 421 } 422 if (!textNode) return; 423 424 // Wrap the drop-cap letter(s) in a span, leave the rest of the text node intact 425 var span = document.createElement('span'); 426 span.className = 'drop-cap-first-letter'; 427 span.textContent = dropCappedLetters; 428 textNode.textContent = textNode.textContent.substring(dropCappedLetters.length); 429 textNode.parentNode.insertBefore(span, textNode); 430 }); 431}; 432 433// Cinematic video flex block: muted autoplay loop, then click to play full 434// video with audio and native controls. No lightbox â plays inline. 435var videoFlexInit = function videoFlexInit() {
436 document.querySelectorAll('.flex-section-video').forEach(function (section) { 437 var video = section.querySelector('.video-player'); 438 var playBtn = section.querySelector('.video-play-btn'); 439 var overlay = section.querySelector('.video-overlay'); 440 var fullVideoUrl = section.dataset.fullVideoUrl || null; 441 if (!video || !playBtn) { 442 return; 443 } 444 var enterFullPlayback = function enterFullPlayback() { 445 // Swap to the full video if a separate URL is provided 446 if (fullVideoUrl && video.querySelector('source').src !== fullVideoUrl) { 447 video.querySelector('source').src = fullVideoUrl; 448 video.load(); 449 } 450 video.muted = false; 451 video.loop = false; 452 video.controls = true; 453 video.currentTime = 0; 454 var playPromise = video.play(); 455 if (playPromise && typeof playPromise["catch"] === 'function') { 456 playPromise["catch"](function () { 457 // Autoplay-with-audio can be blocked. Fall back: show controls and let the user retry. 458 video.controls = true; 459 }); 460 } 461 if (overlay) { 462 overlay.style.opacity = '0'; 463 overlay.style.pointerEvents = 'none'; 464 } 465 video.style.cursor = 'pointer'; 466 }; 467 var loopVideoSrc = video.querySelector('source').src; 468 var exitToLoop = function exitToLoop() { 469 // Swap back to the short loop clip if we switched to the full video 470 if (fullVideoUrl && video.querySelector('source').src !== loopVideoSrc) { 471 video.querySelector('source').src = loopVideoSrc; 472 video.load(); 473 } 474 video.controls = false; 475 video.muted = true; 476 video.loop = true; 477 video.currentTime = 0; 478 video.play(); 479 if (overlay) { 480 overlay.style.opacity = ''; 481 overlay.style.pointerEvents = ''; 482 } 483 video.style.cursor = ''; 484 }; 485 playBtn.addEventListener('click', enterFullPlayback); 486 video.addEventListener('ended', exitToLoop); 487 }); 488}; 489 490// init within document.ready 491(function ($) { 492 window.addEventListener('load', function () { 493 dropCapHelper(); 494 headerNav(); 495 featureGalleryInit(); 496 // fitVidInit(); correct load issue first . also not in major use on site 497 hrMatchWidths(); 498 matchHeightInit(); 499 smoothScrollInit(); 500 analyticsSourcing(); 501 ariaFontAdjusts(); 502 sidebarHelper(); 503 hrGalleriesPageHelper(); 504 widont(); // disabled per #557 505 506 modalSimple(); 507 wysiwigContentHelper(); 508 videoFlexInit(); 509 510 // functions needing delay, even after load (e.g.: wait for ads to load) 511 setTimeout(function () { 512 articleFlexHelper(); 513 }, 3000); 514 }); 515})(jQuery);
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.