PageSourceSearch

https://wp.harpers.org/wp-content/themes/timber/static/js/main.0fac1f62.js

js harpers.org collected 2026-10-01 08:44:06 UTC 19,748 bytes, 515 lines download raw bytes

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*$/, '&nbsp;$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.