PageSourceSearch

https://www.saem.org/ResourcePackages/SAEM/assets/scripts/scripts.js

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

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.