PageSourceSearch

https://www.same.org/wp-content/themes/rd-theme/assets/js/scripts.js?ver=1790909330

js same.org collected 2026-10-02 03:24:04 UTC 61,547 bytes, 1,831 lines download raw bytes

1/* common RD UI utilities
2 * dependencies: lodash
3 */
4
5var RDJS = (function (window, document) {
6
7  'use strict';
8
9
10  /* =helper functions
11   * =================
12   */
13
14  // quick no-effort polyfill for Element.matches
15  if (!Element.prototype.matches) {
16    Element.prototype.matches = Element.prototype.msMatchesSelector ||
17                                Element.prototype.webkitMatchesSelector;
18  }
19
20
21  // cast to array, because _.toArray and _.castArray are inadequate when
22  // the argument could be either a node or nodeList. Also doubles as
23  // an array copier
24  function toArray(collection) {
25    var result = [];
26    if (collection) {
27      if (typeof collection.length === 'number') {
28        for (var i = 0; i < collection.length; i++) {
29          result.push(collection[i]);
30        }
31      } else {
32        result.push(collection);
33      }
34    }
35    return result;
36  }
37
38
39  // get all elements not contained within certain parents
40  // because css selectors are currently inadequate to solve this problem
41  // in the general case
42  function getElementsWithoutParent(selector, parentBlacklist, currentParent) {
43    if (!currentParent) {
44      currentParent = document.documentElement;
45    }
46
47    var result = [];
48
49    if (!currentParent.children) {
50      return result;
51    }
52
53    for (var i = 0; i < currentParent.children.length; i++) {
54      if (currentParent.children[i].matches(selector)) {
55        result.push(currentParent.children[i]);
56      }
57      if (!currentParent.children[i].matches(parentBlacklist)) {
58        result = result.concat(getElementsWithoutParent(selector, parentBlacklist, currentParent.children[i]));
59      }
60    }
61    return result;
62  }
63
64
65  // return a valid url-encoded string of parameters from an object
66  // currently does not support nested objects
67  function objectToParameters(obj) {
68    var parts = [];
69    for (var key in obj) {
70      if (obj.hasOwnProperty(key)) {
71        parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
72      }
73    }
74    return parts.join('&');
75  }
76
77
78  // produce a valid html id (with some additional considerations) from an
79  // arbitrary string. Providing a value for 'defaultId' will cover cases where
80  // the string is blank or consists entirely of characters we will strip out
81  function generateHtmlId(str, defaultId) {
82    // sanitize our id value a bit. The html5 spec is /very/ permissive about
83    // what can be in an id (pretty much anything but spaces goes), but strip
84    // out some additional characters in case we're using this as a url fragment
85    var idString = str
86      .trim()
87      .replace(/\s+/g, '-')
88      .replace(/[\.\?\$\/ #&%@,]/g, '')
89      .toLowerCase();
90
91    idString = idString || defaultId || '';  // in case str was empty or all special chars
92
93    // if we have an id conflict, increment until we don't. We could append
94    // some kind of random value, but we want this to produce the same
95    // linkable hash each run
96    var tryCount = 0;
97    var existingElement = document.getElementById(idString);
98    while (existingElement) {
99      tryCount++;
100      existingElement = document.getElementById(idString + '-' + String(tryCount));
101    }
102    if (tryCount > 0) {
103      idString += '-' + String(tryCount);
104    }
105
106    return idString;
107  }
108
109
110  // xhr helper
111  function xhr(method, url, callback, data) {
112    var _validMethods = ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'TRACE', 'PATCH'],
113        xhr = new XMLHttpRequest();
114
115    if (!method) {
116      console.error('No method provided to xhr');
117      return false;
118    }
119    method = method.toUpperCase();
120    if (!_validMethods.includes(method)) {
121      console.error(method + ' is not a valid http request method');
122      return false;
123    }
124
125    if (!url) {
126      console.error('No url provided to xhr');
127      return false;
128    }
129
130    xhr.onreadystatechange = function () {
131      if (xhr.readyState === XMLHttpRequest.DONE) {
132        if (xhr.status === 200) {
133          if (callback && typeof callback === 'function') {
134            callback(xhr.responseText);
135          } else {
136            console.log(xhr.responseText);
137          }
138        } else {
139          console.error('XHR returned a status of ' + xhr.status);
140          console.log(xhr);
141        }
142      }
143    };
144
145    xhr.open(method, url);
146
147    if (method === 'POST' && data) {
148      xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
149      if (typeof data === 'object') {
150        xhr.send(objectToParameters(data));
151      } else {
152        xhr.send(data);
153      }
154    } else {
155      xhr.send();
156    }
157  }
158
159
160  /* =toggles
161   * ========
162   *
163   * a one-to-many class toggler activated on click
164   *
165   * default elements:
166   * <el data-toggle-target="[keyword|selector]" />
167   *
168   * external toggle interactions
169   * <el data-toggle-exit="esc outside" />
170   */
171
172  var toggles = (function () {
173    var toggleList = [],
174
175        togglePrototype = {
176          origin: null,
177          targets: [],
178          activeClass: 'is-active',
179          deactivateOnEsc: false,
180          deactivateOnOutside: false,
181          useFragments: false,
182
183          // make toggle go
184          activate: function activate(force) {
185            _.forEach([this.origin].concat(this.targets), function (el, i) {
186              if (typeof force === 'boolean') {
187                el.classList.toggle(this.activeClass, force);
188              } else {
189                el.classList.toggle(this.activeClass);
190              }
191              // if this is the origin, set control state
192              if (i === 0) {
193                el.setAttribute('aria-expanded', (el.classList.contains(this.activeClass)).toString());
194              // if this is a toggle, set aria hidden state
195              } else {
196                el.setAttribute('aria-hidden', (!el.classList.contains(this.activeClass)).toString());
197              }
198            }.bind(this));
199
200            // focus first target as a convenience to kb/sr users
201            if (this.origin.classList.contains(this.activeClass) && this.targets.length > 0) {
202              this.targets[0].focus();
203            }
204
205            // write url fragment, if applicable
206            if (this.useFragments && this.origin.id) {
207              if (this.origin.classList.contains(this.activeClass)) {
208                window.history.replaceState(null, '', '#' + encodeURIComponent(this.origin.id));
209              } else {
210                window.history.replaceState(null, '', window.location.href.split('#')[0]);
211              }
212            }
213
214            if (typeof this.onActivate === 'function') {
215              this.onActivate(this);
216            }
217
218            return this;
219          },
220
221          // get targets, generate a11y attributes, and add event listeners
222          init: function init() {
223            this.targets = toArray(this.targets);
224
225            // set up ids
226            this.origin.id = this.origin.id || generateHtmlId(this.origin.textContent.substr(0, 24), 'toggle');
227            _.forEach(this.targets, function generateTargetId(target) {
228              target.id = target.id || generateHtmlId(this.origin.textContent.substr(0, 24) + '-pane', 'pane');
229            }.bind(this));
230
231            // set initial a11y attributes
232            this.origin.setAttribute('role', 'button');
233            this.origin.setAttribute('aria-expanded', (this.origin.classList.contains(this.activeClass)).toString());
234            this.origin.setAttribute('aria-controls', this.targets[0].id);
235            _.forEach(this.targets, function linkTargetToOrigin(target) {
236              target.setAttribute('aria-labelledby', this.origin.id);
237              target.setAttribute('aria-hidden', (!this.origin.classList.contains(this.activeClass)).toString());
238            }.bind(this));
239
240            // make toggle focusable if it isn't a focusable element by default
241            this.origin.setAttribute('tabindex', '0');
242
243            // allow programmatic focus on first target content element
244            if (this.targets.length > 0) {
245              this.targets[0].setAttribute('tabindex', '-1');
246            }
247
248            // bind listeners
249            this.origin.addEventListener('click', function (ev) {
250              ev.preventDefault();
251              this.activate();
252            }.bind(this));
253            this.origin.addEventListener('keydown', function (ev) {
254              // bind spacebar and return in case we're not using a native
255              // button element that includes these interactions implicitly
256              if (ev.keyCode === 13 || ev.keyCode === 32) {
257                ev.preventDefault();
258                this.activate();
259              }
260            }.bind(this));
261
262            if (typeof this.onInit === 'function') {
263              this.onInit(this);
264            }
265
266            return this;
267          },
268
269          // optional callbacks
270          onActivate: null,
271          onInit: null
272        };
273
274    // private: parses a target string and returns a matching array
275    function findTargets(el) {
276      var targetString = el.getAttribute('data-toggle-target');
277      if (!targetString) {
278        return [];
279      }
279 else if (targetString === 'next') {
280        return [el.nextElementSibling];
281      } else if (targetString === 'parent') {
282        return [el.parentNode];
283      } else if (targetString === 'parentparent') {
284        return [el.parentNode.parentNode];
285      } else if (targetString === 'parentnext') {
286        return [el.parentNode.nextElementSibling];
287      } else if (targetString === 'parentparentnext') {
288        return [el.parentNode.parentNode.nextElementSibling];
289      }
290      return toArray(document.querySelectorAll(targetString));
291    }
292
293    // public: make node or nodeList toggleable
294    function add(els, options) {
295      options = options || {};
296      els = toArray(els);
297      _.forEach(els, function (el) {
298        var toggle = _.assign(Object.create(togglePrototype), { origin: el }, options);
299        toggleList.push(toggle);
300        toggle.init();
301      });
302      return list();
303    }
304
305    // public: returns an array of all toggles on the page
306    function list() {
307      return toggleList;
308    }
309
310    // public: returns all toggle objects whose origins match a given css selector
311    function find(selector) {
312      var result = [];
313      _.forEach(list(), function (toggle) {
314        if (toggle.origin.matches(selector)) {
315          result.push(toggle);
316        }
317      });
318      return result;
319    }
320
321    // public: returns a single toggle object whose origin matches a given css selector
322    function get(selector) {
323      var result = null;
324      _.forEach(list(), function (toggle) {
325        if (toggle.origin.matches(selector)) {
326          result = toggle;
327          return false;
328        }
329      });
330      return result;
331    }
332
333    // public: initialize by adding default elements
334    function init() {
335      _.forEach(document.querySelectorAll('[data-toggle-target]'), function addNewToggle(el) {
336        var options = {
337          targets: findTargets(el),
338          deactivateOnEsc: (/\besc\b/.test(el.getAttribute('data-toggle-exit'))),
339          deactivateOnOutside: (/\boutside\b/.test(el.getAttribute('data-toggle-exit'))),
340          useFragments: Boolean(el.getAttribute('data-toggle-fragment')),
341        };
342        add(el, options);
343      });
344
345      // open any toggles that match url fragment
346      if (window.location.hash) {
347        var urlFragment = decodeURIComponent(window.location.hash.substr(1));
348        _.forEach(list(), function matchGroupFragment(group) {
349          _.forEach([group.origin].concat(group.targets), function matchElementFragment(el) {
350            if (el.id === urlFragment) {
351              group.activate(true);
352            }
353          });
354        });
355      }
356
357      // add global close listeners
358      document.documentElement.addEventListener('keyup', function (ev) {
359        if (ev.keyCode === 27) {
360          _.forEach(list(), function (toggle) {
361            if (toggle.deactivateOnEsc) {
362              toggle.activate(false);
363            }
364          });
365        }
366      });
367      document.body.addEventListener('click', function (ev) {
368        _.forEach(list().filter(function (toggle) { return toggle.deactivateOnOutside; }), function (toggle) {
369          var deactivate = true,
370              els = [toggle.origin].concat(toggle.targets);
371          for (var i = els.length - 1; i >= 0; i--) {
372            if (els[i] === ev.target || els[i].contains(ev.target)) {
373              deactivate = false;
374              break;
375            }
376          }
377          if (deactivate) {
378            toggle.activate(false);
379          }
380        });
381      });
382    }
383
384    return {
385      add: add,
386      list: list,
387      find: find,
388      get: get,
389      init: init
390    };
391  }());
392
393
394  /* =tabs
395   * =====
396   *
397   * a class switcher wherein only one element (or pair of elements)
398   * may be active at a time
399   *
400   * default elements:
401   * <el class="tabs [is-automated]?">
402   *   <el class="tabs__tab" />
403   *   <el class="tabs__pane" />
404   * </el>
405   */
406
407  var tabs = (function () {
408    var tabGroupList = [],
409
410        tabGroupPrototype = {
411          // preferences
412          automated: false,
413          slideDuration: 5000,
414          useFragments: false,
415
416          // calculated attributes
417          active: 0,
418          count: 0,
419
420          // elements
421          container: null,
422          tabs: null,
423          panes: null,
424          nextTriggers: null,
425          previousTriggers: null,
426          playTriggers: null,
427          pauseTriggers: null,
428
429          // default (overrideable) selectors used in init()
430          containerSelector: '.tabs',
431          tabSelector: '.tabs__tab',
432          paneSelector: '.tabs__pane',
433          nextSelector: '.tabs__next',
434          previousSelector: '.tabs__previous',
435          playSelector: '.tabs__play',
436          pauseSelector: '.tabs__pause',
437
438          // classes
439          activeClass: 'is-active',
440          lastActiveClass: 'is-last-active',
441          nextClass: 'is-next',
442          previousClass: 'is-previous',
443          initClass: 'is-enhanced',
444          playingClass: 'is-playing',
445
446          // utility
447          timer: null,
448
449          changeTo: function changeTo(index, interruptTimer, isFirstLoad) {
450            if (index === 'next') {
451              index = this.getNextIndex();
452            } else if (index === 'previous') {
453              index = this.getPreviousIndex();
454            } else if (typeof index !== 'number' || index < 0 || index >= this.count) {
455              console.warn('given index not recognized');
456              return this;
457            }
458
459            if (interruptTimer && this.automated) {
460              this.pause();
461            }
462
463            // reset state classes to blank slate
464            _.forEach(this.tabs.concat(this.panes), function (el) {
465              el.classList.remove(this.activeClass);
466              el.classList.remove(this.lastActiveClass);
467              el.classList.remove(this.nextClass);
468              el.classList.remove(this.previousClass);
469            }.bind(this));
470
471            // reset aria attributes to defaults
472            _.forEach(this.tabs, function (tab) {
473              tab.setAttribute('aria-selected', 'false');
474            }.bind(this));
475            _.forEach(this.panes, function (pane) {
476              pane.setAttribute('aria-hidden', 'true');
477            }.bind(this));
478
479            // mark the tab we're changing /from/ for the purposes of animation
480            if (this.active !== index) {
481              if (this.tabs[this.active]) {
482                this.tabs[this.active].classList.add(this.lastActiveClass);
483              }
484              if (this.panes[this.active]) {
485                this.panes[this.active].classList.add(this.lastActiveClass);
486              }
487            }
488
489            // make it so
490            this.active = index;
491
492            // add all state classes and aria states
493            if (this.tabs[index]) {
494              this.tabs[index].classList.add(this.activeClass);
495              this.tabs[index].setAttribute('aria-selected', 'true');
496            }
497            if (this.panes[index]) {
498              this.panes[index].classList.add(this.activeClass);
499              this.panes[index].setAttribute('aria-hidden', 'false');
500            }
501            if (this.tabs[this.getNextIndex()]) {
502              this.tabs[this.getNextIndex()].classList.add(this.nextClass);
503            }
504            if (this.panes[this.getNextIndex()]) {
505              this.panes[this.getNextIndex()].classList.add(this.nextClass);
506            }
507            if (this.tabs[this.getPreviousIndex()]) {
508              this.tabs[this.getPreviousIndex()].classList.add(this.previousClass);
509            }
510            if (this.panes[this.getPreviousIndex()]) {
511              this.panes[this.getPreviousIndex()].classList.add(this.previousClass);
512            }
513
514            // handle fragment ids
515            // no need to do this on initial page load, because either it's loading the default
516            // (first) tab or we're loading from an existing fragment specifier
517            if (this.useFragments && this.tabs[index] && this.tabs[index].id && !isFirstLoad) {
518              window.history.replaceState(null, '', '#' + encodeURIComponent(this.tabs[index].id));
519            }
520
521            // focus first element inside selected pane for a better screen-reader experience
522            if (!isFirstLoad && !this.automated && this.panes[index] && this.panes[index].children.length > 0) {
523              this.panes[index].children[0].setAttribute('tabindex', '-1');
524              this.panes[index].children[0].focus();
525            }
526
527            if (interruptTimer && this.automated) {
528              this.play();
529            }
530            if (typeof this.onChange === 'function') {
531              this.onChange(this);
532            }
533            return this;
534          },
535
536          play: function play() {
537            this.container.classList.add(this.playingClass);
538            if (this.timer > 0) {
539              return false;
540            }
541            this.timer = setInterval(function () {
542              this.changeTo('next');
543            }.bind(this), this.slideDuration);
544            if (typeof this.onPlay === 'function') {
545              this.onPlay(this);
546            }
547            return this;
548          },
549
550          pause: function pause() {
551            this.container.classList.remove(this.playingClass);
552            clearInterval(this.timer);
553            this.timer = 0;
554            if (typeof this.onPause === 'function') {
555              this.onPause(this);
556            }
557            return this;
558          },
559
560          getNextIndex: function getNextIndex() {
561            return (this.active + 1) % this.count;
562          },
563
564          getPreviousIndex: function getPreviousIndex() {
565            return this.active <= 0 ? this.count - 1 : this.active - 1;
566          },
567
568          generateTabIds: function generateTabIds() {
569            _.forEach(this.tabs, function generateTabId(el) {
570              if (el.id) return false;  // skip if an id has been provided for us
571              el.id = generateHtmlId(el.textContent, 'tab');
572            });
573          },
574
575          generateAriaAttributes: function generateAriaAttributes() {
576            for (var i = 0; i < this.count; i++) {
577              // make sure each pane also has an id
578              // if an id is not present, generate from the associated tab id
579              if (this.tabs[i] && this.panes[i]) {
580                if (!this.panes[i].id) {
581                  this.panes[i].id = this.tabs[i].id + '-pane';
582                }
583
584                // make sure each tab is linked to its associated pane
585                if (this.tabs[i].tagName.toLowerCase() == 'a') {
586                  this.tabs[i].href = '#' + this.panes[i].id;
587                }
588                this.tabs[i].setAttribute('aria-controls', this.panes[i].id);
589
590                // make sure each pane is labeled by its associated tab
591                this.panes[i].setAttribute('aria-labelledby', this.tabs[i].id);
592              }
593            }
594          },
595
596          init: function init() {
597            // default elements
598            this.tabs = toArray(this.tabs ||
599              getElementsWithoutParent(this.tabSelector, this.containerSelector, this.container));
600            this.panes = toArray(this.panes ||
601              getElementsWithoutParent(this.paneSelector, this.containerSelector, this.container));
602            this.nextTriggers = toArray(this.nextTriggers ||
603              getElementsWithoutParent(this.nextSelector, this.containerSelector, this.container));
604            this.previousTriggers = toArray(this.previousTriggers ||
605              getElementsWithoutParent(this.previousSelector, this.containerSelector, this.container));
606            this.playTriggers = toArray(this.playTriggers ||
607              getElementsWithoutParent(this.playSelector, this.containerSelector, this.container));
608            this.pauseTriggers = toArray(this.pauseTriggers ||
609              getElementsWithoutParent(this.pauseSelector, this.containerSelector, this.container));
610
611            // get final tabs count
612            this.count = Math.max(this.tabs.length, this.panes.length);
613
614            // generate tab ids for a11y and fragment links
615            this.generateTabIds();
616
617            // link tabs and panes in markup for screen reader benefit
618            this.generateAriaAttributes();
619
620            // apply count as container class to help css sizing
621            this.container.classList.add('tabs--' + String(this.count));
622
623            // attach event listeners
624            _.forEach(this.tabs, function (tab, index) {
625              tab.setAttribute('tabindex', '0');
626              tab.addEventListener('click', function (ev) {
627                ev.preventDefault();
628                this.changeTo(index, true);
629              }.bind(this));
630              tab.addEventListener('keydown', function (ev) {
631                if (ev.keyCode === 13 || ev.keyCode === 32) {
632                  ev.preventDefault();
633                  this.changeTo(index, true);
634                }
635              }.bind(this));
636            }.bind(this));
637            _.forEach(this.nextTriggers, function (el) {
638              el.addEventListener('click', function (ev) {
639                ev.preventDefault();
640                this.changeTo('next', true);
641              }.bind(this));
642            }.bind(this));
643            _.forEach(this.previousTriggers, function (el) {
644              el.addEventListener('click', function (ev) {
645                ev.preventDefault();
646                this.changeTo('previous', true);
647              }.bind(this));
648            }.bind(this));
649            _.forEach(this.pauseTriggers, function (el) {
650              el.addEventListener('click', function (ev) {
651                ev.preventDefault();
652                this.pause();
653              }.bind(this));
654            }.bind(this));
655            _.forEach(this.playTriggers, function (el) {
656              el.addEventListener('click', function (ev) {
657                ev.preventDefault();
658                this.play();
659              }.bind(this));
660            }.bind(this));
661
662            // automated slideshows get paused on mouseover
663            if (this.automated) {
664              this.container.addEventListener('mouseenter', this.pause.bind(this));
665              this.container.addEventListener('mouseleave', this.play.bind(this));
666            }
667
668            // if we have a fragment specifier, use it; otherwise, the first tab is open by default
669            if (this.useFragments && window.location.hash) {
670              var fragment = decodeURIComponent(window.location.hash.substr(1));
671              for (var i = 0; i < this.tabs.length; i++) {
672                if (this.tabs[i].id === fragment) {
673                  this.active = i;
674                  break;
675                }
676              }
677            }
678
679            this.changeTo(this.active, false, true);
680
681            if (this.automated) {
682              this.play();
683            }
684
685            if (typeof this.onInit === 'function') {
686              this.onInit(this);
687            }
688
689            this.container.classList.add(this.initClass);
690
691            return this;
692          },
693
694          // optional callbacks
695          onChange: null,
696          onPlay: null,
697          onPause: null,
698          onInit: null
699        };
700
701    // public: make node or nodeList into tab groups
702    function add(els, options) {
703      options = options || {};
704      els = toArray(els);
705      _.forEach(els, function (el) {
706        var tabGroup = _.assign(Object.create(tabGroupPrototype), { container: el }, options);
707        tabGroupList.push(tabGroup);
708        tabGroup.init();
709      });
710      return list();
711    }
712
713    // public: returns an array of all tab groups on the page
714    function list() {
715      return tabGroupList;
716    }
717
718    // public: returns all tab groups whose containers match a given css selector
719    function find(selector) {
720      var result = [];
721      _.forEach(list(), function (tabGroup) {
722        if (tabGroup.container.matches(selector)) {
723          result.push(tabGroup);
724        }
725      });
726      return result;
727    }
728
729    // public: returns a single tab group whose container matches a given css selector
730    function get(selector) {
731      var result = null;
732      _.forEach(list(), function (tabGroup) {
733        if (tabGroup.container.matches(selector)) {
734          result = tabGroup;
735          return false;
736        }
737      });
738      return result;
739    }
740
741    // public: initialize by adding default elements
742    function init() {
743      _.forEach(toArray(document.getElementsByClassName('tabs')), function addTabsContainer(container) {
744        add(container, {
745          automated: container.matches('.tabs--automated'),
746          useFragments: container.matches('.tabs--use-fragments'),
747        });
748      });
749    }
750
751    return {
752      add: add,
753      list: list,
754      find: find,
755      get: get,
756      init: init
757    };
758  }());
759
760
761  /* =shared height elements
762   * =======================
763   *
764   * match minimum heights across disparate elements
765   *
766   * usage:
767   * <el data-height-group="groupname" />
768   */
769
770  var sharedHeights = (function () {
771    var heightGroups = {};
772
773    // public: update all shared-height elements
774    function update() {
775      var g, thisGroup, heights, maxHeight;
776
777      function resetHeight(el) {
778        el.style.minHeight = 0;
779        heights.push(el.offsetHeight);
780      }
781      function applyHeight(el) {
782        el.style.minHeight = maxHeight + 'px';
783      }
784
785      for (g in heightGroups) {
786        if (heightGroups.hasOwnProperty(g)) {
787          thisGroup = heightGroups[g];
788          heights = [];
789          _.forEach(thisGroup, resetHeight);
790          maxHeight = Math.max.apply(null, heights);
791          _.forEach(thisGroup, applyHeight);
792        }
793      }
794    }
795
796    // public: add shared-height functionality to a node or nodelist
797    function add(els, groupName) {
798      _.forEach(toArray(els), function (el) {
799        var group = groupName || el.getAttribute('data-height-group');
800        if (!group) {
801          console.warn('No group specified for shared-height element');
802          return false;
803        }
804        heightGroups[group] = heightGroups[group] || [];
805        heightGroups[group].push(el);
806      });
807      return list();
808    }
809
810    // public: return heightGroups object
811    function list() {
812        return heightGroups;
813    }
814
815    // public: initialize with default elements
816    function init() {
817      add(document.querySelectorAll('[data-height-group]'));
818      update();
819    }
820
821    var debouncedUpdate = _.debounce(update, 75);
822    window.addEventListener('resize', debouncedUpdate);
823    window.addEventListener('load', debouncedUpdate);
824
825    return {
826      add: add,
827      list: list,
828      update: update,
829      init: init
830    };
831  }());
832
833
834  /* =waypoints
835   * ==========
836   *
837   * a scrolling waypoint-based class switcher, for when skrollr is overkill
838   *
839   * default elements:
840   * <el data-waypoints="waypoint[ persist]?: class" />
841   *     where waypoint is a number and a unit of measurement (%, px, em).
842   *     multiple waypoints may be separated by commas
843   */
844
845  var waypoints = (function () {
846    var waypointElementList = [],
847        waypointElementPrototype = {
848          el: null,
849          waypoints: [],
850
851          init: function () {
852            var configString = this.el.getAttribute('data-waypoints');
853            if (!configString) { return false; }
854
855            _.forEach(configString.split(','), function (config) {
856              var configParts = config.split(':'),
857                  val, cl, unit, persist = false, waypoint;
858
859              try {
860                val = configParts[0].trim();
861                cl = configParts[1].trim();
862                if (val.substr(-7) === 'persist') {
863                  persist = true;
864                  val = val.slice(0, -7).trim();
865                }
866                unit = val.replace(/\d|,/g, '') || '%';
867                val = parseFloat(val);
868                if (!cl || isNaN(val) || typeof val !== 'number') {
869                  throw 'Bad config';
870                }
871                waypoint = _.assign(Object.create(waypointPrototype), {
872                  unit: unit,
873                  val: val,
874                  cl: cl,
875                  persistent: persist
876                });
877                this.waypoints.push(waypoint);
878              } catch (e) {
879                console.warn('Bad inline configuration provided to waypoints in element ', this.el, ' at waypoint ', configParts[0]);
880              }
881            }.bind(this));
882          }
883        },
884
885        waypointPrototype = {
886          val: 50,
887          unit: '%',
888          cl: '',
889          persistent: false
890        };
891
892    function scrollListener() {
893      var viewportHeight = document.documentElement.clientHeight;
894      _.forEachRight(waypointElementList, function (waypointElement, elementIndex) {
895        var offset = waypointElement.el.getBoundingClientRect().top - viewportHeight,
896            scroll = window.scrollY;
897        if (offset < 0 && window.scrollY > 0) {
898          _.forEachRight(waypointElement.waypoints, function (waypoint, waypointIndex) {
899            var test = false;
900            switch (waypoint.unit) {
901              case '%':
902                var percentage = -offset / waypointElement.el.offsetHeight * 100;
903                test = percentage > waypoint.val;
904                break;
905              case 'em':
906                var elementFontSize = parseFloat(getComputedStyle(waypointElement.el).fontSize);
907                test = -offset > elementFontSize * waypoint.val;
908                break;
909              case 'rem':
910                var documentFontSize = parseFloat(getComputedStyle(document.body).fontSize);
911                test = -offset > documentFontSize * waypoint.val;
912                break;
913              case 'vh':
914                test = -offset > viewportHeight / 100 * waypoint.val;
915                break;
916              default:  // px
917                test = -offset > waypoint.val;
918                break;
919            }
920            if (test) {
921              waypointElement.el.classList.add(waypoint.cl);
922
923              // no need to continue tracking persistent elements after activation
924              if (waypoint.persistent) {
925                waypointElement.waypoints.splice(waypointIndex, 1);
926              }
927            } else {
928              waypointElement.el.classList.remove(waypoint.cl);
929            }
930          });
931        } else {
932          _.forEach(waypointElement.waypoints, function (waypoint) {
933            waypointElement.el.classList.remove(waypoint.cl);
934          });
935        }
936
937        // prune inactive waypoint elements
938        if (waypointElement.waypoints.length === 0) {
939          waypointElementList.splice(elementIndex, 1);
940        }
941      });
942    }
943
944    // public: add elements to scroll tracking
945    function add(els, options) {
946      options = options || {};
947      els = toArray(els);
948      _.forEach(els, function (el) {
949        var waypointElement = _.assign(Object.create(waypointElementPrototype), { el: el, waypoints: [] }, options);
950        waypointElementList.push(waypointElement);
951        waypointElement.init();
952      });
953      return list();
954    }
955
956    // public: returns an array of all elements currently tracked by waypoints
957    function list() {
958      return waypointElementList;
959    }
960
961    // public: track default elements and add scroll listener
962    function init() {
963      _.forEach(document.querySelectorAll('[data-waypoints]'), add);
964      // do initial pass
965      scrollListener();
966      // debounce function and listen
967      window.addEventListener('scroll', _.throttle(scrollListener, 150));
968    }
969
970    return {
971      init: init,
972      list: list,
973      add: add
974    };
975  }());
976
977
978  /* =social share links
979   * ===================
980   *
981   * unobtrusive 'share' buttons that don't use too much bandwidth or create
982   * privacy concerns
983   *
984   * default elements:
985   * <div data-share-to="facebook twitter google linkedin reddit pinterest tumblr email"></div>
986   */
987
988   var sharing = (function () {
989
990    // glean some default parameters from the page
991    var defaultOptions = {
992      services: 'facebook twitter linkedin email',
993      url: (function () {
994        var link = document.querySelector('link[rel="canonical"]'),
995            og = document.querySelector('meta[property="og:url"]');
996        if (link) {
997          return link.getAttribute('href');
998        } else if (og) {
999          return og.getAttribute('content');
1000        }
1001        return window.location.href;
1002      })(),
1003      title: (function () {
1004        var og = document.querySelector('meta[property="og:title"]');
1005        if (og) {
1006          return og.getAttribute('content');
1007        }
1008        return document.title;
1009      })(),
1010      abstract: (function () {
1011        var og = document.querySelector('meta[property="og:description"]'),
1012            meta = document.querySelector('meta[name="description"]');
1013        if (og) {
1014          return og.getAttribute('content');
1015        } else if (meta) {
1016          return meta.getAttribute('content');
1017        }
1018        return null;
1019      })(),
1020      thumbnail: (function () {
1021        var og = document.querySelector('meta[property="og:image"]'),
1022            apple = document.querySelector('link[rel="apple-touch-icon-precomposed"]');
1023        if (og) {
1024          return og.getAttribute('content');
1025        } else if (apple) {
1026          return apple.getAttribute('href');
1027        }
1028        return null;
1029      })(),
1030      cite: (function () {
1031        var og = document.querySelector('meta[property="og:site_name"]'),
1032            link = document.querySelector('link[rel="start"][title]');
1033        if (og) {
1034          return og.getAttribute('content');
1035        } else if (link) {
1036          return link.getAttribute('title');
1037        }
1038        return null;
1039      })()
1040    };
1041
1042    // generate services and their params using passed-in options data
1043    function generateServiceData(options) {
1044      options = options || {};
1045      return {
1046        facebook: {
1047          name: 'Facebook',
1048          icon: 'fab fa-facebook-square',
1049          url: 'https://www.facebook.com/sharer/sharer.php',
1050          params: [
1051            { name: 'u', value: options.url },
1052            { name: 'title', value: options.title },
1053            { name: 'quote', value: options.text },
1054            { name: 'description', value: options.abstract },
1055            { name: 'caption', value: options.cite },
1056            { name: 'picture', value: options.thumbnail }
1057          ]
1058        },
1059        twitter: {
1060          name: 'Twitter',
1061          icon: 'fab fa-twitter-square',
1062          url: 'https://twitter.com/intent/tweet',
1063          params: [
1064            { name: 'url', value: options.url },
1065            { name: 'text', value: options.text }
1066          ]
1067        },
1068        google: {
1069          name: 'Google Plus',
1070          icon: 'fab fa-google-plus-square',
1071          url: 'https://plus.google.com/share',
1072          params: [
1073            { name: 'url', value: options.url }
1074          ]
1075        },
1076        linkedin: {
1077          name: 'LinkedIn',
1078          icon: 'fab fa-linkedin',
1079          url: 'https://www.linkedin.com/shareArticle',
1080          params: [
1081            { name: 'url', value: options.url },
1082            { name: 'mini', value: 'true' },
1083            { name: 'title', value: options.title },
1084            { name: 'summary', value: options.abstract },
1085            { name: 'source', value: options.cite }
1086          ]
1087        },
1088        reddit: {
1089          name: 'Reddit',
1090          icon: 'fab fa-reddit-square',
1091          url: 'https://www.reddit.com/submit',
1092          params: [
1093            { name: 'url', value: options.url },
1094            { name: 'title', value: options.title }
1095          ]
1096        },
1097        pinterest: {
1098          name: 'Pinterest',
1099          icon: 'fab fa-pinterest-square',
1100          url: 'https://pinterest.com/pin/create/button/',
1101          params: [
1102            { name: 'url', value: options.url },
1103            { name: 'media', value: options.thumbnail },
1104            { name: 'description', value: options.title }
1105          ]
1106        },
1107        tumblr: {
1108          name: 'Tumblr',
1109          icon: 'fab fa-tumblr-square',
1110          url: 'https://www.tumblr.com/widgets/share/tool',
1111          params: [
1112            { name: 'canonicalUrl', value: options.url },
1113            { name: 'title', value: options.title },
1114            { name: 'caption', value: options.abstract }
1115          ]
1116        },
1117        email: {
1118          name: 'Email',
1119          icon: 'fas fa-envelope-square',
1120          url: 'mailto:',
1121          params: [
1122            { name: 'subject', value: options.title },
1123            { name: 'body', value: options.url + '\n' + (options.text || '') },
1124          ]
1125        }
1126      };
1127    }
1128
1129    // populate our container with share buttons
1130    function generateMarkup(el, options) {
1131      var serviceData = generateServiceData(options),
1132          button = document.createElement('a');
1133      button.setAttribute('target', '_blank');
1134      button.className = 'share-buttons__button';
1135
1136      _.forEach(options.services.split(/\s+/), function (service) {
1137        if (!service) return false;
1138
1139        var thisButton = button.cloneNode(true),
1140            thisData = serviceData[service];
1141
1142        if (!thisData) return false;
1143
1144        thisButton.innerHTML = '<span class="' + thisData.icon + '"></span>';
1145        thisButton.setAttribute('title', 'Share via ' + thisData.name);
1146        thisButton.href = thisData.url + '?' + thisData.params.map(function (param) {
1147          if (param.value) {
1148            return encodeURIComponent(param.name) + '=' + encodeURIComponent(param.value);
1149          }
1150          return null;
1151        }).filter(function (param) {
1152          return !!param;
1153        }).join('&');
1154
1155        el.appendChild(thisButton);
1156      });
1157    }
1158
1159    // add share buttons to a container
1160    function add(els, options) {
1161      els = toArray(els);
1162      options = options || {};
1163      _.forEach(els, function (el) {
1164        // look for 'data-share-to' attribute on container if no services are passed
1165        var declaredServices = {};
1166        if (!options.services && el.getAttribute('data-share-to')) {
1167          declaredServices.services = el.getAttribute('data-share-to');
1168        }
1169        generateMarkup(el, _.assign(defaultOptions, options, declaredServices));
1170      });
1171    }
1172
1173    // initialize with default elements
1174    function init() {
1175      add(document.querySelector('[data-share-to]'));
1176    }
1177
1178    return {
1179      init: init,
1180      add: add
1181    };
1182  })();
1183
1184
1185  /* =truncators
1186   * ===========
1187   *
1188   * truncate containers to number of lines, fixed height, and so on
1189   *
1190   * usage:
1191   * <div data-truncate-to="5 lines" data-truncate-text="Show More"></div>
1192   * <div data-truncate-to="100px"></div>
1193   * <div data-truncate-to="2em"></div>
1194   * <div data-truncate-to="3 items"></div>
1195   * etc.
1196   */
1197
1198  var truncators = (function () {
1199    var buttonPrototype = document.createElement('button'),
1200        truncatorList = [],
1201
1202        truncatorPrototype = {
1203          container: null,
1204          button: null,
1205          buttonText: "Show More",
1206          expandedButtonText: "Show Less",
1207          limit: null,
1208          limitUnit: null,
1209          isRepeatable: false,
1210          isTruncated: false,
1211
1212          init: function () {
1213            var configString = this.container.getAttribute('data-truncate-to');
1214
1215            // parse units
1216            if ((!this.limit || !this.limitUnit) && configString) {
1217              this.limit = parseInt((configString.match(/^\d+/) || [''])[0], 10);
1218              this.limitUnit = (configString.match(/[a-z]+$/i) || [''])[0].trim();
1219            }
1220            if (!this.limit || isNaN(this.limit) || !this.limitUnit) {
1221              console.error("Bad limit or unit config provided to following truncator:", this.container);
1222              return;
1223            }
1224
1225            if (!this.button) {
1226              this.button = buttonPrototype.cloneNode(true);
1227            }
1228
1229            this.truncate();
1230
1231            this.button.addEventListener('click', function (ev) {
1232              if (this.isTruncated) {
1233                this.expand();
1234              } else {
1235                this.truncate();
1236              }
1237            }.bind(this));
1238          },
1239
1240          truncate: function () {
1241            var normalizedLimit,
1242                buttonSuffix = '';
1243
1244            // if we're limiting by items, we can hide items instead of limiting pixel height
1245            if (this.limitUnit === 'item' || this.limitUnit === 'items') {
1246              var items = this.container.children,
1247                  hideCount = items.length - this.limit;
1248
1249              // do no truncation if there aren't enough items
1250              if (hideCount <= 0) {
1251                return;
1252              }
1253
1254              for (var i = this.limit; i < items.length; i++) {
1255                items[i].style.display = 'none';
1256              }
1257
1258              if (hideCount > 0) {
1259                buttonSuffix = ' (' + hideCount + (hideCount > 1 ? ' items' : ' item') + ')';
1260              }
1261
1262            // if we're not limiting by items, we'll need to calculate a max-height
1263            } else {
1264              this.container.style.overflow = 'hidden';
1265              switch (this.limitUnit) {
1266                case '%':
1267                  normalizedLimit = this.container.clientHeight * this.limit / 100;
1268                  break;
1269
1270                case 'em':
1271                  // compute font size of element in px and multiply
1272                  normalizedLimit = parseFloat(getComputedStyle(this.container).fontSize) * this.limit;
1273                  break;
1274
1275                case 'rem':
1276                  // compute font size of body in px and multiply
1277                  normalizedLimit = parseFloat(getComputedStyle(document.body).fontSize) * this.limit;
1278                  break;
1279
1280                case 'vh':
1281                  // take a percentage of viewport height
1282                  normalizedLimit = document.documentElement.clientHeight * this.limit / 100;
1283                  break;
1284
1285                case 'vw':
1286                  // take a percentage of viewport width
1287                  normalizedLimit = document.documentElement.clientWidth * this.limit / 100;
1288                  break;
1289
1290                case 'line':
1291                case 'lines':
1292                  // calculate pixel height of a line and multiply
1293                  (function (container, limit) {
1294                    var testEl = document.createElement('div'),
1295                        lineHeight;
1296                    testEl.textContent = 'A';
1297                    container.insertBefore(testEl, container.firstElementChild);
1298                    lineHeight = testEl.offsetHeight;
1299                    container.removeChild(testEl);
1300                    normalizedLimit = lineHeight * limit;
1301                  })(this.container, this.limit);
1302                  break;
1303
1304                default:  // px
1305                  normalizedLimit = this.limit;
1306                  break;
1307              }
1308
1309              this.container.style.maxHeight = normalizedLimit + 'px';
1310            }
1311
1312            this.button.textContent = this.buttonText + buttonSuffix;
1313            this.container.parentNode.insertBefore(this.button, this.container.nextElementSibling);
1314
1315            this.isTruncated = true;
1316          },
1317
1318          expand: function () {
1319            if (this.limitUnit === 'item' || this.limitUnit === 'items') {
1320              _.forEach(this.container.children, function (item) {
1321                item.style.display = '';
1322              });
1323            } else {
1324              this.container.style.overflow = '';
1325              this.container.style.maxHeight = '';
1326            }
1327
1328            if (this.isRepeatable) {
1329              this.button.textContent = this.expandedButtonText;
1330            } else {
1331              this.button.parentNode.removeChild(this.button);
1332            }
1333
1334            this.isTruncated = false;
1335          }
1336        };
1337
1338    buttonPrototype.setAttribute('type', 'button');
1339    buttonPrototype.className = 'truncator-expand';
1340
1341    function add(els, options) {
1342      els = toArray(els);
1343      options = options || {};
1344      _.forEach(els, function (el) {
1345        var truncator = _.assign(Object.create(truncatorPrototype), {container: el}, options);
1346        truncatorList.push(truncator);
1347        truncator.init();
1348      });
1349    }
1350
1351    function init() {
1352      add(document.querySelectorAll('[data-truncate-to]'));
1353    }
1354
1355    return {
1356      init: init,
1357      add: add
1358    };
1359  })();
1360
1361
1362  /* =horizontal scrollers
1363   * =====================
1364   *
1365   * enable non-touch controls for horizontal scrolling sections
1366   *
1367   * <section class="horizontal-scroll">
1368   *   <div class="horizontal-scroll__wrapper">
1369   *     <ul class="horizontal-scroll__contents"> ... </ul>
1370   *   </div>
1371   *   <button class="horizontal-scroll__control--left"> ... </button>
1372   *   <button class="horizontal-scroll__control--right"> ... </button>
1373   * </setion>
1374   */
1375
1376  var horizontalScrollers = (function () {
1377    var scrollers = [],
1378
1379        scrollerPrototype = {
1380          el: null,
1381          wrapper: null,
1382          contents: null,
1383          leftButton: null,
1384          rightButton: null,
1385
1386          // adjusts how big the 'gravity well' at the start and end of the scrollable area is
1387          gravity: 24,
1388          // when using the scroller arrows, if the remaining area to be scrolled in the direction
1389          // the user just scrolled is less than this value wide (in pixels), it will just scroll
1390          // the rest of the way
1391          pagingTolerance: 180,
1392          // when using the scroller arrows, if we don't end up at an endcap, snap the left side to
1393          // the nearest item edge?
1394          pagingSnap: false,
1395
1396          startClass: 'horizontal-scroll--is-at-start',
1397          endClass: 'horizontal-scroll--is-at-end',
1398
1399          init: function init(container) {
1400            this.el = container;
1401            this.wrapper = container.querySelector('.horizontal-scroll__wrapper');
1402            this.contents = container.querySelector('.horizontal-scroll__contents');
1403            this.leftButton = container.querySelector('.horizontal-scroll__control--left');
1404            this.rightButton = container.querySelector('.horizontal-scroll__control--right');
1405
1406            if (!this.wrapper || !this.contents) {
1407              throw new Error('Structure of horizontal scroller is not correct');
1408            }
1409
1410            this.wrapper.addEventListener('scroll', this.scrollListener.bind(this));
1411
1412            if (this.leftButton) {
1413              this.leftButton.addEventListener('click', this.pageLeft.bind(this));
1414            }
1415            if (this.rightButton) {
1416              this.rightButton.addEventListener('click', this.pageRight.bind(this));
1417            }
1418
1419            this.checkPosition();
1420          },
1421
1422          scrollListener: (function () {
1423            return _.throttle(function throttledScrollListener() {
1424              this.checkPosition();
1425            }, 200, { leading: false });
1426          })(),
1427
1428          checkPosition: function checkPosition() {
1429            // check if we're at either edge of the scrollable area, and apply classes accordingly
1430            var scrollSize = this.wrapper.clientWidth,
1431                contentSize = this.contents.scrollWidth,
1432                leftOffset = this.wrapper.scrollLeft;
1433
1434              this.el.classList.remove(this.startClass);
1435              this.el.classList.remove(this.endClass);
1436
1437            if (leftOffset < this.gravity) {
1438              this.el.classList.add(this.startClass);
1439              this.scrollToStart();
1440            } else if (leftOffset > contentSize - scrollSize - this.gravity) {
1441              this.el.classList.add(this.endClass);
1442              this.scrollToEnd();
1443            }
1444          },
1445
1446          scrollToStart: function scrollToStart() {
1447            this.wrapper.scrollLeft = 0;
1448          },
1449
1450          scrollToEnd: function scrollToEnd() {
1451            this.wrapper.scrollLeft = this.contents.scrollWidth - this.wrapper.clientWidth;
1452          },
1453
1454          pageLeft: function pageLeft() {
1455            var pageSize = this.wrapper.clientWidth,
1456                currentOffset = this.wrapper.scrollLeft,
1457                destinationOffset = Math.max(currentOffset - pageSize, 0);
1458
1459            if (destinationOffset < this.pagingTolerance) {
1460              this.scrollToStart();
1461            } else {
1462              this.wrapper.scrollLeft = this.pagingSnap ? this.snapToNearest(destinationOffset) 
1462: destinationOffset;
1463            }
1464          },
1465
1466          pageRight: function pageRight() {
1467            var pageSize = this.wrapper.clientWidth,
1468                contentSize = this.contents.scrollWidth,
1469                currentOffset = this.wrapper.scrollLeft,
1470                maxOffset = contentSize - pageSize,
1471                destinationOffset = Math.min(currentOffset + pageSize, maxOffset);
1472
1473            if (destinationOffset > maxOffset - this.pagingTolerance) {
1474              this.scrollToEnd();
1475            } else {
1476              this.wrapper.scrollLeft = this.pagingSnap ? this.snapToNearest(destinationOffset) : destinationOffset;
1477            }
1478          },
1479
1480          snapToNearest: function snapToNearest(targetOffset) {
1481            var nearestItemOffset = 0,
1482                leftCandidate = 0,
1483                rightCandidate = 0,
1484                leftDiff = 0,
1485                rightDiff = 0,
1486                thisChild = null;
1487
1488            for (var i = 0; i < this.contents.children.length; i++) {
1489              thisChild 
1489= this.contents.children[i];
1490              if (nearestItemOffset + thisChild.clientWidth > targetOffset) {
1491                leftCandidate = nearestItemOffset;
1492                rightCandidate = nearestItemOffset + thisChild.clientWidth;
1493                break;
1494              }
1495              nearestItemOffset += thisChild.clientWidth;
1496            }
1497
1498            leftDiff = targetOffset - leftCandidate;
1499            rightDiff = rightCandidate - targetOffset;
1500            return leftDiff < rightDiff ? leftCandidate : rightCandidate;
1501          }
1502        };
1503
1504    // public: return a list of scrolling sections tracked by this module
1505    function list() {
1506      return scrollers;
1507    }
1508
1509    // public: add this module's functionality to a container element
1510    // see module comment for expected structure
1511    function add(container) {
1512      var scroller = Object.create(scrollerPrototype);
1513      try {
1514        scroller.init(container);
1515        scrollers.push(scroller);
1516      } catch (e) {
1517        console.error(e);
1518      }
1519    }
1520
1521    // public: initialize this module
1522    function init() {
1523      _.forEach(document.querySelectorAll('.horizontal-scroll'), function (container) {
1524        add(container);
1525      });
1526    }
1527
1528    return {
1529      add: add,
1530      list: list,
1531      init: init,
1532    };
1533  }());
1534
1535
1536  /* =misc
1537   * =====
1538   *
1539   * miscellaneous enhancements that aren't large enough to justify a module
1540   */
1541
1542  var misc = (function () {
1543
1544    var exports = Object.create(null);
1545
1546
1547    // mobile menu toggle
1548    exports.addMobileMenuToggle = function addMobileMenuToggle() {
1549      var inactiveClass = 'mobile-menu-was-open';
1550      toggles.add(document.getElementsByClassName('page-header__toggle'), {
1551        targets: toArray(document.querySelectorAll('body, .page-header__content')),
1552        activeClass: 'mobile-menu-is-open',
1553        deactivateOnEsc: true,
1554        onActivate: function (toggle) {
1555          // add an inactive class only after the menu has been opened and then closed
1556          if (!toggle.origin.classList.contains(toggle.activeClass)) {
1557            _.forEach([toggle.origin].concat(toggle.targets), function (el) {
1558              el.classList.add(inactiveClass);
1559            });
1560          } else {
1561            _.forEach([toggle.origin].concat(toggle.targets), function (el) {
1562              el.classList.remove(inactiveClass);
1563            });
1564          }
1565        }
1566      });
1567    };
1568
1569
1570    // subnav toggles for mobile drilldown
1571    exports.addNavDrilldownButtons = function addNavDrilldownButtons() {
1572      var toggle = document.createElement('button');
1573      toggle.className = 'header-nav__toggle';
1574      toggle.setAttribute('type', 'button');
1575      toggle.setAttribute('data-toggle-target', 'next');
1576      toggle.innerHTML = 'Expand subnavigation for previous item';
1577
1578      _.forEach(document.querySelectorAll('.header-nav a + ul'), function (sublist) {
1579        sublist.parentNode.insertBefore(toggle.cloneNode(true), sublist);
1580      });
1581    };
1582
1583
1584    // allow visual focus and selection indicators of input labels
1585    exports.focusInputLabels = function focusInputLabels() {
1586      _.forEach(document.querySelectorAll('label[for]'), function (label) {
1587        var input = document.getElementById(label.getAttribute('for'));
1588        // track search container for disclosure search only
1589        var searchContainer = null;
1590        if (label.classList.contains('header-search__label')) {
1591          searchContainer = document.querySelector('.header-search');
1592        }
1593
1594        if (input) {
1595          input.addEventListener('focus', function (ev) {
1596            label.classList.add('is-focused');
1597            if (searchContainer) {
1598              searchContainer.classList.add('is-focused');
1599            }
1600          });
1601          input.addEventListener('blur', function (ev) {
1602            label.classList.remove('is-focused');
1603            if (searchContainer) {
1604              searchContainer.classList.remove('is-focused');
1605            }
1606          });
1607          if (input.type.toLowerCase() === 'radio' || input.type.toLowerCase() === 'checkbox') {
1608            input.addEventListener('change', function (ev) {
1609              if (input.checked) {
1610                label.classList.add('is-selected');
1611              } else {
1612                label.classList.remove('is-selected');
1613              }
1614            });
1615          }
1616          // trigger 'change' event manually in order to populate highlight states on page load
1617          if ("createEvent" in document) {
1618            var ev = document.createEvent("HTMLEvents");
1619            ev.initEvent("change", false, true);
1620            input.dispatchEvent(ev);
1621          } else {
1622            input.fireEvent("onchange");
1623          }
1624        }
1625      });
1626    };
1627
1628
1629    // add utility classes to form inputs
1630    // this makes dynamic labels possible, for example
1631    exports.addInputUtilityClasses = function addInputUtilityClasses() {
1632      var inputs = toArray(document.getElementsByTagName('input'))
1633                   .filter(isTextInput)
1634                   .concat(toArray(document.getElementsByTagName('textarea')));
1635      var currentContentClass = 'has-content';
1636      var pastContentClass = 'did-have-content';
1637
1638      _.forEach(inputs, function addClassesToInput(input) {
1639        var hasContainedContent = false;
1640
1641        // initial state
1642        if (input.value) {
1643          input.classList.add(currentContentClass);
1644          hasContainedContent = true;
1645        }
1646
1647        // changes
1648        input.addEventListener('input', function (ev) {
1649          if (input.value) {
1650            hasContainedContent = true;
1651            input.classList.remove(pastContentClass);
1652            input.classList.add(currentContentClass);
1653          } else {
1654            input.classList.remove(currentContentClass);
1655            if (hasContainedContent) {
1656              input.classList.add(pastContentClass);
1657            }
1658          }
1659        });
1660      });
1661    };
1662
1663    var isTextInput = (function () {
1664      var textInputTypes = ['color', 'date', 'email', 'number', 'password', 'search', 'tel', 'text', 'time', 'url'];
1665      return function isTextInput(el) {
1666        return textInputTypes.includes(el.getAttribute('type'));
1667      };
1668    })();
1669
1670
1671    /*
1672     * Keyboard submenu accessibility
1673     */
1674    exports.dropdownKeyboardNav = function dropdownKeyboardNav() {
1675      const activeClass = 'is-focused';
1676      const mainNavLinks = Array.from(document.querySelectorAll('.header-nav > ul > li > a'));
1677
1678      let currentDropdown;
1679
1680      // open dropdowns on spacebar/downarrow
1681      mainNavLinks.forEach(function (link) {
1682        link.addEventListener('keydown', function (ev) {
1683          if (['ArrowDown', 'Space'].indexOf(ev.code) > -1) {
1684            ev.preventDefault();
1685            const dropdown = this.parentNode.querySelector('ul');
1686            if (!dropdown) return;
1687
1688            dropdown.classList.add(activeClass);
1689            dropdown.querySelector('a').focus();
1690
1691            currentDropdown = dropdown;
1692
1693            document.body.addEventListener('click', handleClickOutside);
1694            document.body.addEventListener('keyup', handleEsc);
1695            Array.from(dropdown.querySelectorAll('a'))
1696              .forEach(link => link.addEventListener('blur', handleFocusOutside));
1697          }
1698        });
1699      });
1700
1701      // handle clicking outside a focused dropdown
1702      function handleClickOutside(ev) {
1703        if (currentDropdown.contains(ev.target)) return;
1704        closeDropdown(false);
1705      }
1706
1707      function handleEsc(ev) {
1708        if (ev.code === 'Escape') closeDropdown(true);
1709      }
1710
1711      // handle tabbing away from a focused dropdown
1712      function handleFocusOutside(ev) {
1713        const links = Array.from(currentDropdown.querySelectorAll('a'));
1714
1715        // since this is on the blur event of each dropdown link, we need to give
1716        // the browser time to actually focus the next element
1717        setTimeout(function () {
1718          if (links.indexOf(document.activeElement) > -1) return;
1719          closeDropdown(true);
1720        }, 0);
1721      }
1722
1723      function closeDropdown(focus) {
1724        currentDropdown.classList.remove(activeClass);
1725
1726        document.body.removeEventListener('click', handleClickOutside);
1727        document.body.removeEventListener('keyup', handleEsc);
1728        Array.from(currentDropdown.querySelectorAll('a'))
1729          .forEach(link => link.removeEventListener('blur', handleFocusOutside));
1730
1731        if (focus) currentDropdown.parentNode.firstElementChild.focus();
1732
1733        currentDropdown = null;
1734      }
1735    };
1736
1737
1738    // automatically create slideshow tab indicators
1739    exports.createHomeSlideshowTabs = function createHomeSlideshowTabs() {
1740      var tab = document.createElement('button');
1741      tab.className = 'home-feature__tab tabs__tab';
1742      tab.setAttribute('type', 'button');
1743      _.forEach(document.getElementsByClassName('home-feature'), function (feature) {
1744        var nav = feature.querySelector('.home-feature__nav');
1745        if (!nav) {
1746          nav = document.createElement('div');
1747          nav.className = 'home-feature__nav';
1748          feature.appendChild(nav);
1749        }
1750        _.forEach(feature.querySelectorAll('.tabs__pane'), function (pane) {
1751          nav.appendChild(tab.cloneNode(true));
1752        });
1753      });
1754    };
1755
1756
1757    // initialize ukiyo for parallax effects
1758    exports.initializeParallax = function initializeParallax() {
1759      if (typeof Ukiyo === 'function') {
1760        _.forEach(document.querySelectorAll('.use-parallax'), function (el) {
1761          new Ukiyo(el, {
1762            scale: 1.25,
1763            speed: 1.25,
1764            willChange: true,
1765            wrapperClass: 'use-parallax__wrapper',
1766          });
1767        });
1768      }
1769    };
1770
1771
1772    // add body waypoint for sticky header
1773    exports.addHeaderWaypoint = function addHeaderWaypoint() {
1774      document.body.setAttribute('data-waypoints', '1px: is-scrolled');
1775    };
1776
1777
1778    return exports;
1779
1780  }());
1781
1782
1783  /* =initialize modules
1784   * ===================
1785   */
1786
1787  var init = function () {
1788    // functions that must run before large modules
1789    // e.g. functions that output toggles that must then be initialized
1790    misc.addHeaderWaypoint();
1791    misc.addNavDrilldownButtons();
1792    misc.createHomeSlideshowTabs();
1793
1794    // large modules
1795    toggles.init();
1796    tabs.init();
1797    sharedHeights.init();
1798    waypoints.init();
1799    truncators.init();
1800    horizontalScrollers.init();
1801    sharing.init();
1802
1803    // misc modules
1804    misc.addMobileMenuToggle();
1805    misc.focusInputLabels();
1806    misc.dropdownKeyboardNav();
1807    misc.addInputUtilityClasses();
1808    misc.initializeParallax();
1809
1810    document.documentElement.classList.remove('no-js');
1811    document.documentElement.classList.add('js');
1812  };
1813
1814
1815  /* =public
1816   * =======
1817   */
1818
1819  return {
1820    xhr: xhr,
1821    toggles: toggles,
1822    tabs: tabs,
1823    sharedHeights: sharedHeights,
1824    waypoints: waypoints,
1825    sharing: sharing,
1826    init: init
1827  };
1828
1829}(window, document));
1830
1831RDJS.init();

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.