PageSourceSearch

https://www.sfnet.com/Scripts/scripts.js

js sfnet.com collected 2026-10-03 20:02:54 UTC 49,303 bytes, 1,518 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  // cast to array, because _.toArray and _.castArray are inadequate when
15  // the argument could be either a node or nodeList. Also doubles as
16  // an array copier
17  function toArray(collection) {
18    var result = [];
19    if (collection) {
20      if (typeof collection.length === 'number') {
21        for (var i = 0; i < collection.length; i++) {
22          result.push(collection[i]);
23        }
24      } else {
25        result.push(collection);
26      }
27    }
28    return result;
29  }
30
31
32    /* =toggles
33   * ========
34   *
35   * a one-to-many class toggler activated on click
36   *
37   * default elements:
38   * <el data-toggle-target="[keyword|selector]" />
39   *
40   * external toggle interactions
41   * <el data-toggle-exit="esc outside" />
42   */
43
44  var toggles = (function () {
45    var toggleList = [],
46
47        togglePrototype = {
48          origin: null,
49          targets: [],
50          activeClass: 'is-active',
51          deactivateOnEsc: false,
52          deactivateOnOutside: false,
53
54          // make toggle go
55          activate: function activate(force) {
56            _.forEach([this.origin].concat(this.targets), function (el, i) {
57              if (typeof force === 'boolean') {
58                el.classList.toggle(this.activeClass, force);
59              } else {
60                el.classList.toggle(this.activeClass);
61              }
62              // if this is the origin, set aria-pressed state
63              if (i === 0) {
64                el.setAttribute('aria-pressed', (el.classList.contains(this.activeClass)).toString());
65              }
66            }.bind(this));
67            if (typeof this.onActivate === 'function') {
68              this.onActivate(this);
69            }
70            return this;
71          },
72
73          // get targets and add event listeners
74          init: function init() {
75            if (!this.targets || this.targets.length === 0) {
76              this.targets = findTargets(this.origin);
77            } else {
78              // make sure we've got an array of targets
79              this.targets = toArray(this.targets);
80            }
81            // set initial aria-pressed state
82            this.origin.setAttribute('aria-pressed', (this.origin.classList.contains(this.activeClass)).toString());
83            this.origin.addEventListener('click', function (ev) {
84              ev.preventDefault();
85              this.activate();
86            }.bind(this));
87            if (typeof this.onInit === 'function') {
88              this.onInit(this);
89            }
90            return this;
91          },
92
93          // optional callbacks
94          onActivate: null,
95          onInit: null
96        };
97
98    // private: parses a target string and returns a matching array
99    function findTargets(el) {
100      var targetString = el.getAttribute('data-toggle-target');
101      if (!targetString) {
102        return [];
103      } else if (targetString === 'next') {
104        return [el.nextElementSibling];
105      } else if (targetString === 'parent') {
106        return [el.parentNode];
107      } else if (targetString === 'parentparent') {
108        return [el.parentNode.parentNode];
109      } else if (targetString === 'parentnext') {
110        return [el.parentNode.nextElementSibling];
111      } else if (targetString === 'parentparentnext') {
112        return [el.parentNode.parentNode.nextElementSibling];
113      }
114      return toArray(document.querySelectorAll(targetString));
115    }
116
117    // public: make node or nodeList toggleable
118    function add(els, options) {
119      options = options || {};
120      els = toArray(els);
121      _.forEach(els, function (el) {
122        var exitOptions = el.getAttribute('data-toggle-exit'),
123            toggle = _.assign(
124              Object.create(togglePrototype),
125              {
126                origin: el,
127                deactivateOnEsc: !!(exitOptions && /\besc\b/.test(exitOptions)),
128                deactivateOnOutside: !!(exitOptions && /\boutside\b/.test(exitOptions))
129              },
130              options
131            );
132        toggleList.push(toggle);
133        toggle.init();
134      });
135      return list();
136    }
137
138    // public: returns an array of all toggles on the page
139    function list() {
140      return toggleList;
141    }
142
143    // public: returns all toggle objects whose origins match a given css selector
144    function find(selector) {
145      var result = [];
146      _.forEach(list(), function (toggle) {
147        if (toggle.origin.matches(selector)) {
148          result.push(toggle);
149        }
150      });
151      return result;
152    }
153
154    // public: returns a single toggle object whose origin matches a given css selector
155    function get(selector) {
156      var result = null;
157      _.forEach(list(), function (toggle) {
158        if (toggle.origin.matches(selector)) {
159          result = toggle;
160          return false;
161        }
162      });
163      return result;
164    }
165
166    // public: initialize by adding default elements
167    function init() {
168      add(document.querySelectorAll('[data-toggle-target]'));
169
170      // add global listeners
171      document.documentElement.addEventListener('keyup', function (ev) {
172        if (ev.keyCode === 27) {
173          _.forEach(list(), function (toggle) {
174            if (toggle.deactivateOnEsc) {
175              toggle.activate(false);
176            }
177          });
178        }
179      });
180      document.body.addEventListener('click', function (ev) {
181        _.forEach(list().filter(function (toggle) { return toggle.deactivateOnOutside; }), function (toggle) {
182          var deactivate = true,
183              els = [toggle.origin].concat(toggle.targets);
184          for (var i = els.length - 1; i >= 0; i--) {
185            if (els[i] === ev.target || els[i].contains(ev.target)) {
186              deactivate = false;
187              break;
188            }
189          }
190          if (deactivate) {
191            toggle.activate(false);
192          }
193        });
194      });
195    }
196
197    return {
198      add: add,
199      list: list,
200      find: find,
201      get: get,
202      init: init
203    };
204  }());
205
206
207    /* =tabs
208   * =====
209   *
210   * a class switcher wherein only one element (or pair of elements)
211   * may be active at a time
212   *
213   * default elements:
214   * <el class="tabs [is-automated]?">
215   *   <el class="tabs__tab" />
216   *   <el class="tabs__pane" />
217   * </el>
218   */
219
220
221  var tabs = (function () {
222    var tabGroupList = [],
223
224        tabGroupPrototype = {
225          // elements
226          container: null,
227          tabs: null,
228          panes: null,
229          nextTriggers: null,
230          previousTriggers: null,
231          playTriggers: null,
232          pauseTriggers: null,
233
234          // default (overrideable) selectors used in init()
235          tabSelector: '.tabs__tab',
236          paneSelector: '.tabs__pane',
237          nextSelector: '.tabs__next',
238          previousSelector: '.tabs__previous',
239          playSelector: '.tabs__play',
240          pauseSelector: '.tabs__pause',
241          automatedSelector: '.is-automated',
242
243          // parameters
244          automated: false,
245          slideDuration: 7000,
246          active: 0,
247          count: 0,
248
249          // classes
250          activeClass: 'is-active',
251          nextClass: 'is-next',
252          previousClass: 'is-previous',
253          playingClass: 'is-playing',
254
255          // utility
256          timer: null,
257
258          changeTo: function changeTo(index, interruptTimer) {
259            if (index === 'next') {
260              index = this.getNextIndex();
261            } else if (index === 'previous') {
262              index = this.getPreviousIndex();
263            } else if (typeof index !== 'number' || index < 0 || index >= this.count) {
264              console.warn('given index not recognized');
265              return this;
266            }
267
268            if (interruptTimer && this.automated) {
269              this.pause();
270            }
271
272            _.forEach(this.tabs.concat(this.panes), function (el) {
273              el.classList.remove(this.activeClass);
274              el.classList.remove(this.nextClass);
275              el.classList.remove(this.previousClass);
276            }.bind(this));
277
278            this.active = index;
279
280            if (this.tabs[index]) {
281              this.tabs[index].classList.add(this.activeClass);
282            }
283            if (this.panes[index]) {
284              this.panes[index].classList.add(this.activeClass);
285            }
286            if (this.tabs[this.getNextIndex()]) {
287              this.tabs[this.getNextIndex()].classList.add(this.nextClass);
288            }
289            if (this.panes[this.getNextIndex()]) {
290              this.panes[this.getNextIndex()].classList.add(this.nextClass);
291            }
292            if (this.tabs[this.getPreviousIndex()]) {
293              this.tabs[this.getPreviousIndex()].classList.add(this.previousClass);
294            }
295            if (this.panes[this.getPreviousIndex()]) {
296              this.panes[this.getPreviousIndex()].classList.add(this.previousClass);
297            }
298
299            if (interruptTimer && this.automated) {
300              this.play();
301            }
302            if (typeof this.onChange === 'function') {
303              this.onChange(this);
304            }
305            return this;
306          },
307
308          play: function play() {
309            this.container.classList.add(this.playingClass);
310            this.timer = setInterval(function () {
311              this.changeTo('next');
312            }.bind(this), this.slideDuration);
313            if (typeof this.onPlay === 'function') {
314              this.onPlay(this);
315            }
316            return this;
317          },
318
319          pause: function pause() {
320            this.container.classList.remove(this.playingClass);
321            clearInterval(this.timer);
322            if (typeof this.onPause === 'function') {
323              this.onPause(this);
324            }
325            return this;
326          },
327
328          getNextIndex: function getNextIndex() {
329            return (this.active + 1) % this.count;
330          },
331
332          getPreviousIndex: function getPreviousIndex() {
333            return this.active <= 0 ? this.count - 1 : this.active - 1;
334          },
335
336          init: function init() {
337            var that = this;
338
339            // default elements
340            this.tabs = toArray(this.tabs || this.container.querySelectorAll(this.tabSelector));
341            this.panes = toArray(this.panes || this.container.querySelectorAll(this.paneSelector));
342            this.nextTriggers = toArray(this.nextTriggers || this.container.querySelectorAll(this.nextSelector));
343            this.previousTriggers = toArray(this.previousTriggers || this.container.querySelectorAll(this.previousSelector));
344            this.playTriggers = toArray(this.playTriggers || this.container.querySelectorAll(this.playSelector));
345            this.pauseTriggers = toArray(this.pauseTriggers || this.container.querySelectorAll(this.pauseSelector));
346
347            // detect if we should be automated by default
348            if (this.container.matches(this.automatedSelector)) {
349              this.automated = true;
350            }
351
352            this.count = Math.max(this.tabs.length, this.panes.length);
353
354            // attach event listeners
355            _.forEach(this.tabs, function (tab, index) {
356              tab.addEventListener('click', function (ev) {
357                ev.preventDefault();
358                this.changeTo(index, true);
359              }.bind(this));
360            }.bind(this));
361            _.forEach(this.nextTriggers, function (el) {
362              el.addEventListener('click', function (ev) {
363                ev.preventDefault();
364                this.changeTo('next', true);
365              }.bind(this));
366            }.bind(this));
367            _.forEach(this.previousTriggers, function (el) {
368              el.addEventListener('click', function (ev) {
369                ev.preventDefault();
370                this.changeTo('previous', true);
371              }.bind(this));
372            }.bind(this));
373            _.forEach(this.pauseTriggers, function (el) {
374              el.addEventListener('click', function (ev) {
375                ev.preventDefault();
376                this.pause();
377              }.bind(this));
378            }.bind(this));
379            _.forEach(this.playTriggers, function (el) {
380              el.addEventListener('click', function (ev) {
381                ev.preventDefault();
382                this.play();
383              }.bind(this));
384            }.bind(this));
385
386            this.changeTo(this.active);
387            if (this.automated) {
388              this.play();
389            }
390
391            if (typeof this.onInit === 'function') {
392              this.onInit(this);
393            }
394            return this;
395          },
396
397          // optional callbacks
398          onChange: null,
399          onPlay: null,
400          onPause: null,
401          onInit: null
402        };
403
404    // public: make node or nodeList into tab groups
405    function add(els, options) {
406      options = options || {};
407      els = toArray(els);
408      _.forEach(els, function (el) {
409        var tabGroup = _.assign(Object.create(tabGroupPrototype), { container: el }, options);
410        tabGroupList.push(tabGroup);
411        tabGroup.init();
412      });
413      return list();
414    }
415
416    // public: returns an array of all tab groups on the page
417    function list() {
418      return tabGroupList;
419    }
420
421    // public: returns all tab groups whose containers match a given css selector
422    function find(selector) {
423      var result = [];
424      _.forEach(list(), function (tabGroup) {
425        if (tabGroup.container.matches(selector)) {
426          result.push(tabGroup);
427        }
428      });
429      return result;
430    }
431
432    // public: returns a single tab group whose container matches a given css selector
433    function get(selector) {
434      var result = null;
435      _.forEach(list(), function (tabGroup) {
436        if (tabGroup.container.matches(selector)) {
437          result = tabGroup;
438          return false;
439        }
440      });
441      return result;
442    }
443
444    // public: initialize by adding default elements
445    function init() {
446      add(document.getElementsByClassName('tabs'));
447    }
448
449    return {
450      add: add,
451      list: list,
452      find: find,
453      get: get,
454      init: init
455    };
456  }());
457
458
459    /* =shared height elements
460   * =======================
461   *
462   * match minimum heights across disparate elements
463   *
464   * usage:
465   * <el data-height-group="groupname" />
466   */
467
468  var sharedHeights = (function () {
469    var heightGroups = {};
470
471    // public: update all shared-height elements
472    function update() {
473      var g, thisGroup, heights, maxHeight;
474
475      function resetHeight(el) {
476        el.style.minHeight = 0;
477        heights.push(el.offsetHeight);
478      }
479      function applyHeight(el) {
480        el.style.minHeight = maxHeight + 'px';
481      }
482
483      for (g in heightGroups) {
484        if (heightGroups.hasOwnProperty(g)) {
485          thisGroup = heightGroups[g];
486          heights = [];
487          _.forEach(thisGroup, resetHeight);
488          maxHeight = Math.max.apply(null, heights);
489          _.forEach(thisGroup, applyHeight);
490        }
491      }
492    }
493
494    // public: add shared-height functionality to a node or nodelist
495    function add(els, groupName) {
496      _.forEach(toArray(els), function (el) {
497        var group = groupName || el.getAttribute('data-height-group');
498        if (!group) {
499          console.warn('No group specified for shared-height element');
500          return false;
501        }
502        heightGroups[group] = heightGroups[group] || [];
503        heightGroups[group].push(el);
504      });
505      return list();
506    }
507
508    // public: return heightGroups object
509    function list() {
510        return heightGroups;
511    }
512
513    // public: initialize with default elements
514    function init() {
515      add(document.querySelectorAll('[data-height-group]'));
516      update();
517    }
518
519    var debouncedUpdate = _.debounce(update, 75);
520    window.addEventListener('resize', debouncedUpdate);
521
522    return {
523      add: add,
524      list: list,
525      update: update,
526      init: init
527    };
528  }());
529
530
531    /* =waypoints
532   * ==========
533   *
534   * a scrolling waypoint-based class switcher, for when skrollr is overkill
535   *
536   * default elements:
537   * <el data-waypoints="waypoint[ persist]?: class" />
538   *     where waypoint is a number and a unit of measurement (%, px, em).
539   *     multiple waypoints may be separated by commas
540   */
541
542  var waypoints = (function () {
543    var waypointElementList = [],
544        waypointElementPrototype = {
545          el: null,
546          waypoints: [],
547
548          init: function () {
549            var configString = this.el.getAttribute('data-waypoints');
550            if (!configString) { return false; }
551
552            _.forEach(configString.split(','), function (config) {
553              var configParts = config.split(':'),
554                  val, cl, unit, persist = false, waypoint;
555
556              try {
557                val = configParts[0].trim();
558                cl = configParts[1].trim();
559                if (val.substr(-7) === 'persist') {
560                  persist = true;
561                  val = val.slice(0, -7).trim();
562                }
563                unit = val.replace(/\d|,/g, '') || '%';
564                val = parseFloat(val);
565                if (!cl || isNaN(val) || typeof val !== 'number') {
566                  throw 'Bad config';
567                }
568                waypoint = _.assign(Object.create(waypointPrototype), {
569                  unit: unit,
570                  val: val,
571                  cl: cl,
572                  persistent: persist
573                });
574                this.waypoints.push(waypoint);
575              } catch (e) {
576                console.warn('Bad inline configuration provided to waypoints in element ', this.el, ' at waypoint ', configParts[0]);
577              }
578            }.bind(this));
579          }
580        },
581
582        waypointPrototype = {
583          val: 50,
584          unit: '%',
585          cl: '',
586          persistent: false
587        };
588
589    function scrollListener() {
590      var viewportHeight = document.documentElement.clientHeight;
591      _.forEachRight(waypointElementList, function (waypointElement, elementIndex) {
592        var offset = waypointElement.el.getBoundingClientRect().top - viewportHe
592ight,
593            scroll = window.scrollY;
594        if (offset < 0 && window.scrollY > 0) {
595          _.forEachRight(waypointElement.waypoints, function (waypoint, waypointIndex) {
596            var test = false;
597            switch (waypoint.unit) {
598              case '%':
599                var percentage = -offset / waypointElement.el.offsetHeight * 100;
600                test = percentage > waypoint.val;
601                break;
602              case 'em':
603                var elementFontSize = parseFloat(getComputedStyle(waypointElement.el).fontSize);
604                test = -offset > elementFontSize * waypoint.val;
605                break;
606              case 'rem':
607                var documentFontSize = parseFloat(getComputedStyle(document.body).fontSize);
608                test = -offset > documentFontSize * waypoint.val;
609                break;
610              case 'vh':
611                test = -offset > viewportHeight / 100 * waypoint.val;
612                break;
613              default:  // px
614                test = -offset > waypoint.val;
615                break;
616            }
617            if (test) {
618              waypointElement.el.classList.add(waypoint.cl);
619
620              // no need to continue tracking persistent elements after activation
621              if (waypoint.persistent) {
622                waypointElement.waypoints.splice(waypointIndex, 1);
623              }
624            } else {
625              waypointElement.el.classList.remove(waypoint.cl);
626            }
627          });
628        } else {
629          _.forEach(waypointElement.waypoints, function (waypoint) {
630            waypointElement.el.classList.remove(waypoint.cl);
631          });
632        }
633
634        // prune inactive waypoint elements
635        if (waypointElement.waypoints.length === 0) {
636          waypointElementList.splice(elementIndex, 1);
637        }
638      });
639    }
640
641    // public: add elements to scroll tracking
642    function add(els, options) {
643      options = options || {};
644      els = toArray(els);
645      _.forEach(els, function (el) {
646        var waypointElement = _.assign(Object.create(waypointElementPrototype), { el: el, waypoints: [] }, options);
647        waypointElementList.push(waypointElement);
648        waypointElement.init();
649      });
650      return list();
651    }
652
653    // public: returns an array of all elements currently tracked by waypoints
654    function list() {
655      return waypointElementList;
656    }
657
658    // public: track default elements and add scroll listener
659    function init() {
660      _.forEach(document.querySelectorAll('[data-waypoints]'), add);
661      // do initial pass
662      scrollListener();
663      // debounce function and listen
664      window.addEventListener('scroll', _.throttle(scrollListener, 150));
665    }
666
667    return {
668      init: init,
669      list: list,
670      add: add
671    };
672  }());
673
674
675    /* =social share links
676   * ===================
677   *
678   * unobtrusive 'share' buttons that don't use too much bandwidth or create
679   * privacy concerns
680   *
681   * default elements:
682   * <div data-share-to="facebook twitter google linkedin reddit pinterest tumblr email"></div>
683   */
684
685   var sharing = (function () {
686
687    // glean some default parameters from the page
688    var defaultOptions = {
689      services: 'facebook twitter linkedin email',
690      url: (function () {
691        var link = document.querySelector('link[rel="canonical"]'),
692            og = document.querySelector('meta[property="og:url"]');
693        if (link) {
694          return link.getAttribute('href');
695        } else if (og) {
696          return og.getAttribute('content');
697        }
698        return window.location.href;
699      })(),
700      title: (function () {
701        var og = document.querySelector('meta[property="og:title"]');
702        if (og) {
703          return og.getAttribute('content');
704        }
705        return document.title;
706      })(),
707      abstract: (function () {
708        var og = document.querySelector('meta[property="og:description"]'),
709            meta = document.querySelector('meta[name="description"]');
710        if (og) {
711          return og.getAttribute('content');
712        } else if (meta) {
713          return meta.getAttribute('content');
714        }
715        return null;
716      })(),
717      thumbnail: (function () {
718        var og = document.querySelector('meta[property="og:image"]'),
719            apple = document.querySelector('link[rel="apple-touch-icon-precomposed"]');
720        if (og) {
721          return og.getAttribute('content');
722        } else if (apple) {
723          return apple.getAttribute('href');
724        }
725        return null;
726      })(),
727      cite: (function () {
728        var og = document.querySelector('meta[property="og:site_name"]'),
729            link = document.querySelector('link[rel="start"][title]');
730        if (og) {
731          return og.getAttribute('content');
732        } else if (link) {
733          return link.getAttribute('title');
734        }
735        return null;
736      })()
737    };
738
739    // generate services and their params using passed-in options data
740    function generateServiceData(options) {
741      options = options || {};
742      return {
743        facebook: {
744          name: 'Facebook',
745          icon: 'fab fa-facebook-square',
746          url: 'https://www.facebook.com/sharer/sharer.php',
747          params: [
748            { name: 'u', value: options.url },
749            { name: 'title', value: options.title },
750            { name: 'quote', value: options.text },
751            { name: 'description', value: options.abstract },
752            { name: 'caption', value: options.cite },
753            { name: 'picture', value: options.thumbnail }
754          ]
755        },
756        twitter: {
757          name: 'Twitter',
758          icon: 'fab fa-twitter-square',
759          url: 'https://twitter.com/intent/tweet',
760          params: [
761            { name: 'url', value: options.url },
762            { name: 'text', value: options.text }
763          ]
764        },
765        google: {
766          name: 'Google Plus',
767          icon: 'fab fa-google-plus-square',
768          url: 'https://plus.google.com/share',
769          params: [
770            { name: 'url', value: options.url }
771          ]
772        },
773        linkedin: {
774          name: 'LinkedIn',
775          icon: 'fab fa-linkedin',
776          url: 'https://www.linkedin.com/shareArticle',
777          params: [
778            { name: 'url', value: options.url },
779            { name: 'mini', value: 'true' },
780            { name: 'title', value: options.title },
781            { name: 'summary', value: options.abstract },
782            { name: 'source', value: options.cite }
783          ]
784        },
785        reddit: {
786          name: 'Reddit',
787          icon: 'fab fa-reddit-square',
788          url: 'https://www.reddit.com/submit',
789          params: [
790            { name: 'url', value: options.url },
791            { name: 'title', value: options.title }
792          ]
793        },
794        pinterest: {
795          name: 'Pinterest',
796          icon: 'fab fa-pinterest-square',
797          url: 'https://pinterest.com/pin/create/button/',
798          params: [
799            { name: 'url', value: options.url },
800            { name: 'media', value: options.thumbnail },
801            { name: 'description', value: options.title }
802          ]
803        },
804        tumblr: {
805          name: 'Tumblr',
806          icon: 'fab fa-tumblr-square',
807          url: 'https://www.tumblr.com/widgets/share/tool',
808          params: [
809            { name: 'canonicalUrl', value: options.url },
810            { name: 'title', value: options.title },
811            { name: 'caption', value: options.abstract }
812          ]
813        },
814        email: {
815          name: 'Email',
816          icon: 'fas fa-envelope-square',
817          url: 'mailto:',
818          params: [
819            { name: 'subject', value: options.title },
820            { name: 'body', value: options.url + '\n' + (options.text || '') },
821          ]
822        }
823      };
824    }
825
826    // populate our container with share buttons
827    function generateMarkup(el, options) {
828      var serviceData = generateServiceData(options),
829          button = document.createElement('a');
830      button.setAttribute('target', '_blank');
831      button.className = 'share-buttons__button';
832
833      _.forEach(options.services.split(' '), function (service) {
834        var thisButton = button.cloneNode(true),
835            thisData = serviceData[service];
836
837        if (!thisData) {
838          return false;
839        }
840
841        thisButton.innerHTML = '<span class="' + thisData.icon + '"></span>';
842        thisButton.setAttribute('title', 'Share via ' + thisData.name);
843        thisButton.href = thisData.url + '?' + thisData.params.map(function (param) {
844          if (param.value) {
845            return encodeURIComponent(param.name) + '=' + encodeURIComponent(param.value);
846          }
847          return null;
848        }).filter(function (param) {
849          return !!param;
850        }).join('&');
851
852        el.appendChild(thisButton);
853      });
854    }
855
856    // add share buttons to a container
857    function add(els, options) {
858      els = toArray(els);
859      options = options || {};
860      _.forEach(els, function (el) {
861        // look for 'data-share-to' attribute on container if no services are passed
862        var declaredServices = {};
863        if (!options.services && el.getAttribute('data-share-to')) {
864          declaredServices.services = el.getAttribute('data-share-to');
865        }
866        generateMarkup(el, _.assign(defaultOptions, options, declaredServices));
867      });
868    }
869
870    // initialize with default elements
871    function init() {
872      add(document.querySelector('[data-share-to]'));
873    }
874
875    return {
876      init: init,
877      add: add
878    };
879  })();
880
881
882    /* =truncators
883   * ===========
884   *
885   * truncate containers to number of lines, fixed height, and so on
886   *
887   * usage:
888   * <div data-truncate-to="5 lines" data-truncate-text="Show More"></div>
889   * <div data-truncate-to="100px"></div>
890   * <div data-truncate-to="2em"></div>
891   * <div data-truncate-to="3 items"></div>
892   * etc.
893   */
894
895  var truncators = (function () {
896    var buttonPrototype = document.createElement('button'),
897        truncatorList = [],
898
899        truncatorPrototype = {
900          container: null,
901          button: null,
902          buttonText: "Show More",
903          expandedButtonText: "Show Less",
904          limit: null,
905          limitUnit: null,
906          isRepeatable: false,
907          isTruncated: false,
908
909          init: function () {
910            var configString = this.container.getAttribute('data-truncate-to');
911
912            // parse units
913            if ((!this.limit || !this.limitUnit) && configString) {
914              this.limit = parseInt((configString.match(/^\d+/) || [''])[0], 10);
915              this.limitUnit = (configString.match(/[a-z]+$/i) || [''])[0].trim();
916            }
917            if (!this.limit || isNaN(this.limit) || !this.limitUnit) {
918              console.error("Bad limit or unit config provided to following truncator:", this.container);
919              return;
920            }
921
922            if (!this.button) {
923              this.button = buttonPrototype.cloneNode(true);
924            }
925
926            this.truncate();
927
928            this.button.addEventListener('click', function (ev) {
929              if (this.isTruncated) {
930                this.expand();
931              } else {
932                this.truncate();
933              }
934            }.bind(this));
935          },
936
937          truncate: function () {
938            var normalizedLimit,
939                buttonSuffix = '';
940
941            // if we're limiting by items, we can hide items instead of limiting pixel height
942            if (this.limitUnit === 'item' || this.limitUnit === 'items') {
943              var items = this.container.children,
944                  hideCount = items.length - this.limit;
945
946              // do no truncation if there aren't enough items
947              if (hideCount <= 0) {
948                return;
949              }
950
951              for (var i = this.limit; i < items.length; i++) {
952                items[i].style.display = 'none';
953              }
954
955              if (hideCount > 0) {
956                buttonSuffix = ' (' + hideCount + (hideCount > 1 ? ' items' : ' item') + ')';
957              }
958
959            // if we're not limiting by items, we'll need to calculate a max-height
960            } else {
961              this.container.style.overflow = 'hidden';
962              switch (this.limitUnit) {
963                case '%':
964                  normalizedLimit = this.container.clientHeight * this.limit / 100;
965                  break;
966
967                case 'em':
968                  // compute font size of element in px and multiply
969                  normalizedLimit = parseFloat(getComputedStyle(this.container).fontSize) * this.limit;
970                  break;
971
972                case 'rem':
973                  // compute font size of body in px and multiply
974                  normalizedLimit = parseFloat(getComputedStyle(document.body).fontSize) * this.limit;
975                  break;
976
977                case 'vh':
978                  // take a percentage of viewport height
979                  normalizedLimit = document.documentElement.clientHeight * this.limit / 100;
980                  break;
981
982                case 'vw':
983                  // take a percentage of viewport width
984                  normalizedLimit = document.documentElement.clientWidth * this.limit / 100;
985                  break;
986
987                case 'line':
988                case 'lines':
989                  // calculate pixel height of a line and multiply
990                  (function (container, limit) {
991                    var testEl = document.createElement('div'),
992                        lineHeight;
993                    testEl.textContent = 'A';
994                    container.insertBefore(testEl, container.firstElementChild);
995                    lineHeight = testEl.offsetHeight;
996                    container.removeChild(testEl);
997                    normalizedLimit = lineHeight * limit;
998                  })(this.container, this.limit);
999                  break;
1000
1001                default:  // px
1002                  normalizedLimit = this.limit;
1003                  break;
1004              }
1005
1006              this.container.style.maxHeight = normalizedLimit + 'px';
1007            }
1008
1009            this.button.textContent = this.buttonText + buttonSuffix;
1010            this.container.parentNode.insertBefore(this.button, this.container.nextElementSibling);
1011
1012            this.isTruncated = true;
1013          },
1014
1015          expand: function () {
1016            if (this.limitUnit === 'item' || this.limitUnit === 'items') {
1017              _.forEach(this.container.children, function (item) {
1018                item.style.display = '';
1019              });
1020            } else {
1021              this.container.style.overflow = '';
1022              this.container.style.maxHeight = '';
1023            }
1024
1025            if (this.isRepeatable) {
1026              this.button.textContent = this.expandedButtonText;
1027            } else {
1028              this.button.parentNode.removeChild(this.button);
1029            }
1030
1031            this.isTruncated = false;
1032          }
1033        };
1034
1035    buttonPrototype.setAttribute('type', 'button');
1036    buttonPrototype.className = 'truncator-expand';
1037
1038    function add(els, options) {
1039      els = toArray(els);
1040      options = options || {};
1041      _.forEach(els, function (el) {
1042        var truncator = _.assign(Object.create(truncatorPrototype), {container: el}, options);
1043        truncatorList.push(truncator);
1044        truncator.init();
1045      });
1046    }
1047
1048    function init() {
1049      add(document.querySelectorAll('[data-truncate-to]'));
1050    }
1051
1052    return {
1053      init: init,
1054      add: add
1055    };
1056  })();
1057
1058/* =horizontal scrollers
1059   * =====================
1060   *
1061   * Add controls for horizontal scrollers
1062   *
1063   * <section class="horizontal [horizontal--loop]?">
1064   *   <div class="horizontal__scroller">
1065   *     <div class="horizontal__contents">
1066   *       <div class="horizontal__item"> ... </div>
1067   *       ...
1068   *     </div>
1069   *   </div>
1070   * </setion>
1071   */
1072
1073var horizontalScrollers = (function () {
1074  var scrollers = [],
1075
1076      scrollerPrototype = {
1077        loop: false,
1078        el: null,
1079        scrollEl: null,
1080        contentWrapper: null,
1081        contentEls: null,
1082        leftButton: null,
1083        rightButton: null,
1084        atStartClass: 'horizontal--is-at-start',
1085        atEndClass: 'horizontal--is-at-end',
1086        margin: 20,
1087
1088        /*
1089         * Initialize this scroller, create buttons, and bind listeners
1090         */
1091        init: function init(container) {
1092          this.el = container;
1093
1094          this.scrollEl = container.querySelector('.horizontal__scroller');
1095          if (!this.scrollEl) {
1096            throw new Error('Expected scroller wrapper for horizontal scroller');
1097          }
1098
1099          this.contentWrapper = container.querySelector('.horizontal__content');
1100          if (!this.contentWrapper) {
1101            throw new Error('Expected inner container for horizontal scroller');
1102          }
1103
1104          this.contentEls = toArray(this.contentWrapper.children);
1105          if (this.contentEls.length === 0) {
1106            throw new Error('Expected inner contents for horizontal scroller');
1107          }
1108
1109          this.leftButton = this.createButton('left');
1110          this.leftButton.addEventListener('click', this.pageLeft.bind(this));
1111          this.rightButton = this.createButton('right');
1112          this.rightButton.addEventListener('click', this.pageRight.bind(this));
1113
1114          if (!this.loop) {
1115            const throttledDetectEdges = _.throttle(this.detectEdges.bind(this), 150);
1116            this.scrollEl.addEventListener('scroll', throttledDetectEdges);
1117            window.addEventListener('resize', throttledDetectEdges);
1118            this.detectEdges();
1119            setTimeout(this.detectEdges.bind(this), 1000);
1120            window.addEventListener('load', this.detectEdges.bind(this));
1121          }
1122        },
1123
1124        /*
1125         * Detects whether the scrollable area is at its very start or end
1126         * and applies css classes to match
1127         *
1128         * Throttle this function and use it as an event handler
1129         */
1130        detectEdges: function detectEdges(ev) {
1131          if (this.loop) return;
1132
1133          const pageSize = this.scrollEl.clientWidth;
1134          const contentSize = this.contentWrapper.scrollWidth;
1135
1136          if (this.scrollEl.scrollLeft < this.margin) {
1137            this.el.classList.add(this.atStartClass);
1138          } else {
1139            this.el.classList.remove(this.atStartClass);
1140          }
1141
1142          if (this.scrollEl.scrollLeft > contentSize - pageSize - this.margin) {
1143            this.el.classList.add(this.atEndClass);
1144          } else {
1145            this.el.classList.remove(this.atEndClass);
1146          }
1147        },
1148
1149        /*
1150         * Scroll this horizontal scroller to the very start
1151         */
1152        scrollToStart: function scrollToStart() {
1153          this.scrollEl.scrollLeft = 0;
1154        },
1155
1156        /*
1157         * Scroll this horizontal scroller to the far end
1158         */
1159        scrollToEnd: function scrollToEnd() {
1160          const pageSize = this.scrollEl.clientWidth;
1161          const contentSize = this.contentWrapper.scrollWidth;
1162          this.scrollEl.scrollLeft = contentSize - pageSize;
1163        },
1164
1165        /*
1166         * Scroll this area one page left, where a "page" is the width of
1167         * the visible area
1168         */
1169        pageLeft: function pageLeft() {
1170          const pageSize = this.scrollEl.clientWidth;
1171          const currentOffset = this.scrollEl.scrollLeft;
1172          const targetOffset = Math.max(currentOffset - pageSize, 0);
1173          const nearestEl = this.getNearestContentEl(targetOffset);
1174
1175          if (this.loop && currentOffset < this.margin) {
1176            this.scrollToEnd();
1177          } else {
1178            this.scrollEl.scrollLeft = this.contentEls[nearestEl].offsetLeft;
1179          }
1180        },
1181
1182        /*
1183         * Scroll this area one page right, where a "page" is the width of
1184         * the visible area
1185         */
1186        pageRight: function pageRight() {
1187          var pageSize = this.scrollEl.clientWidth;
1188          const contentSize = this.contentWrapper.scrollWidth;
1189          const currentOffset = this.scrollEl.scrollLeft;
1190          const maxOffset = contentSize - pageSize;
1191          const targetOffset = Math.min(currentOffset + pageSize, maxOffset);
1192          const nearestEl = this.getNearestContentEl(targetOffset);
1193
1194          if (this.loop && currentOffset > maxOffset - this.margin) {
1195            this.scrollToStart();
1196          } else if (targetOffset === maxOffset || this.contentEls.length <= nearestEl + 1) {
1197            this.scrollEl.scrollLeft = targetOffset;
1198          } else {
1199            this.scrollEl.scrollLeft = this.contentEls[nearestEl + 1].offsetLeft;
1200          }
1201        },
1202
1203        /*
1204         * Create a left or right control button and append it to this
1205         * scroller's container
1206         */
1207        createButton: function createButton(dir) {
1208          const result = document.createElement('button');
1209          result.setAttribute('type', 'button');
1210          result.className = 'horizontal__control horizontal__control--' + dir;
1211          result.innerHTML = `<i class="fa-regular fa-chevron-${dir}" title="Scroll ${dir}"></i>`;
1212          this.el.appendChild(result);
1213          return result;
1214        },
1215
1216        /*
1217         * Given a pixel offset from the left side of this scroller, returns
1218         * the element that lies directly underneath it. If that offset would
1219         * lie on a gap instead, returns the element to the left of the gap
1220         */
1221        getNearestContentEl: function getNearestContentEl(scrollDistance) {
1222          for (let i = 0; i < this.contentEls.length; ++i) {
1223            if (this.contentEls[i].offsetLeft > scrollDistance) {
1224              return i > 0 ? i - 1 : 0;
1225            }
1226          }
1227          return 0;
1228        },
1229      };
1230
1231  // public: return a list of scrolling sections tracked by this module
1232  function list() {
1233    return scrollers;
1234  }
1235
1236  // public: add this module's functionality to a container element
1237  // see module comment for expected structure
1238  function add(container, options) {
1239    try {
1240      const scroller = Object.assign(Object.create(scrollerPrototype), (options || {}));
1241      scroller.init(container);
1242      scrollers.push(scroller);
1243    } catch (e) {
1244      console.error(e);
1245    }
1246  }
1247
1248  // public: initialize this module
1249  function init() {
1250    _.forEach(document.querySelectorAll('.horizontal'), function (container) {
1251      add(container, {
1252        loop: container.classList.contains('horizontal--loop'),
1253      });
1254    });
1255  }
1256
1257  return {
1258    add: add,
1259    list: list,
1260    init: init,
1261  };
1262}());
1263
1264  /* =misc
1265   * =====
1266   *
1267   * miscellaneous enhancements that aren't large enough to justify a module
1268   */
1269
1270  var misc = (function () {
1271
1272    var exports = Object.create(null);
1273
1274
1275    // subnav toggles for mobile drilldown
1276    exports.addNavDrilldownButtons = function () {
1277      var toggle = document.createElement('button');
1278      toggle.className = 'header-nav__toggle';
1279      toggle.setAttribute('type', 'button');
1280      toggle.setAttribute('data-toggle-target', 'next');
1281      toggle.innerHTML = 'Expand subnavigation for previous item';
1282
1283      _.forEach(document.querySelectorAll('.header-nav a + ul'), function (sublist) {
1284        sublist.parentNode.insertBefore(toggle.cloneNode(true), sublist);
1285      });
1286    };
1287
1288
1289    // allow visual focus and selection indicators of input labels
1290    exports.focusInputLabels = function () {
1291      _.forEach(document.querySelectorAll('label[for]'), function (label) {
1292        var input = document.getElementById(label.getAttribute('for'));
1293        if (input) {
1294          input.addEventListener('focus', function (ev) {
1295            label.classList.add('is-focused');
1296          });
1297          input.addEventListener('blur', function (ev) {
1298            label.classList.remove('is-focused');
1299          });
1300          if (input.type.toLowerCase() === 'radio' || input.type.toLowerCase() === 'checkbox') {
1301            input.addEventListener('change', function (ev) {
1302              if (input.checked) {
1303                label.classList.add('is-selected');
1304              } else {
1305                label.classList.remove('is-selected');
1306              }
1307            });
1308          }
1309          // trigger 'change' event manually in order to populate highlight states on page load
1310          if ("createEvent" in document) {
1311            var ev = document.createEvent("HTMLEvents");
1312            ev.initEvent("change", false, true);
1313            input.dispatchEvent(ev);
1314          } else {
1315            input.fireEvent("onchange");
1316          }
1317        }
1318      });
1319    };
1320
1321
1322    // automatically create slideshow tab indicators
1323    exports.createHomeSlideshowTabs = function () {
1324      var tab = document.createElement('button');
1325      tab.className = 'home-feature__tab tabs__tab';
1326      tab.setAttribute('type', 'button');
1327      _.forEach(document.getElementsByClassName('home-feature'), function (feature) {
1328        var nav = feature.querySelector('.home-feature__nav');
1329        if (!nav) {
1330          nav = document.createElement('div');
1331          nav.className = 'home-feature__nav';
1332          feature.appendChild(nav);
1333        }
1334        _.forEach(feature.querySelectorAll('.tabs__pane'), function (pane) {
1335          nav.appendChild(tab.cloneNode(true));
1336        });
1337      });
1338    };
1339
1340    // combine adjacent tabs modules when placed in Sitefinity
1341    exports.combineTabs = function () {
1342      // combine under the first parent
1343      var containers = toArray(document.querySelectorAll('.sf_cols.tabs')),
1344          currentContainer,
1345          currentType,
1346          children;
1347
1348      if (containers.length > 0 && !document.body.classList.contains('sfPageEditor')) {
1349        for (var i = 0; i < containers.length; i++) {
1350          currentContainer = containers[i];
1351          currentType = currentContainer.className;
1352          while (currentContainer.nextElementSibling && currentContainer.nextElementSibling.className === currentType) {
1353            children = toArray(currentContainer.nextElementSibling.querySelectorAll('.tabs__tab, .tabs__pane'));
1354            while (children.length) {
1355              currentContainer.appendChild(children.shift());
1356            }
1357            currentContainer.parentNode.removeChild(currentContainer.nextElementSibling);
1358          }
1359        }
1360      }
1361
1362      // reorder and count children
1363      _.forEach(document.querySelectorAll('.sf_cols.tabs'), function (container) {
1364        var tabsCount = 0;
1365        _.forEach(container.querySelectorAll('.tabs__tab'), function (tab) {
1366          container.appendChild(tab);
1367          tabsCount++;
1368        });
1369        _.forEach(container.querySelectorAll('.tabs__pane'), function (pane) {
1370          container.appendChild(pane);
1371        });
1372        container.classList.add('tabs--' + tabsCount);
1373      });
1374    };
1375
1376
1377    // add indication to header utility items with children
1378    exports.markHeaderUtilityChildren = function () {
1379      _.forEach(document.querySelectorAll('.header-utility > ul > li'), function (parent) {
1380        if (parent.children.length >= 2) {
1381          parent.classList.add('has-children');
1382        }
1383      });
1384    };
1385
1386
1387    // columnize subnav items
1388    exports.columnizeNav = function () {
1389      var containers = '.header-nav > ul > li > ul, .header-nav .header-nav__sf-wrap > ul > li > ul',
1390          columnSize = 400,
1391          column = document.createElement('div'),
1392          columnClass = 'generated-column';
1393      column.className = columnClass;
1394
1395      _.forEach(document.querySelectorAll(containers), function (container) {
1396        var children = toArray(container.children).map(function (el) { return [el, el.offsetHeight]; });
1397
1398        function createColumns() {
1399          var columnCount = Math.floor(container.clientWidth / columnSize) || 1,
1400              totalHeight = children.reduce(function (acc, cur) { return acc + cur[1]; }, 0),
1401              currentHeight = 0,
1402              currentChild = 0,
1403              currentColumn;
1404
1405          // clear any existing generated column structure
1406          _.forEach(children, function (child) {
1407            container.appendChild(child[0]);
1408          });
1409          _.forEach(container.querySelectorAll('.' + columnClass), function (column) {
1410            column.parentNode.removeChild(column);
1411          });
1412
1413          if (columnCount <= 1) {
1414            return;
1415          }
1416
1417          for (var i = 1; i <= columnCount; i++) {
1418            currentColumn = column.cloneNode(true);
1419            currentColumn.classList.add(columnClass + '--1-' + columnCount);
1420            do {
1421              var child = children[currentChild];
1422              if (!child) {
1423                return;
1424              }
1425              currentColumn.appendChild(child[0]);
1426              currentHeight += child[1];
1427              currentChild++;
1428            } while (currentHeight < totalHeight / columnCount * i ||
1429                (i === columnCount && currentChild < children.length));
1430            container.appendChild(currentColumn);
1431          }
1432        }
1433
1434        createColumns();
1435        var debouncedCreateColumns = _.debounce(createColumns, 333);
1436        window.addEventListener('resize', debouncedCreateColumns);
1437      });
1438    };
1439
1440
1441    // remove empty container elements on the frontend
1442    exports.cullEmptyColumns = function () {
1443      var columns = toArray(document.getElementsByClassName('sf_colsIn'));
1444      var parents = [];
1445
1446      if (columns.length > 0 && !document.body.classList.contains('sfPageEditor')) {
1447        _.forEach(columns, function (column) {
1448          if (column.children.length === 0) {
1449            if (parents.indexOf(column.parentNode) === -1) {
1450              parents.push(column.parentNode);
1451            }
1452            column.parentNode.removeChild(column);
1453          }
1454        });
1455        _.forEach(parents, function (parent) {
1456          if (parent.children.length === 0) {
1457            parent.parentNode.removeChild(parent);
1458          }
1459        });
1460      }
1461    };
1462
1463
1464    return exports;
1465
1466  }());
1467
1468
1469  /* =initialize modules
1470   * ===================
1471   */
1472
1473  var init = function () {
1474    // functions that must run before large modules
1475    // e.g. functions that output toggles that must then be initialized
1476    misc.combineTabs();
1477    misc.addNavDrilldownButtons();
1478    misc.createHomeSlideshowTabs();
1479
1480    // large modules
1481    toggles.init();
1482    tabs.init();
1483    sharedHeights.init();
1484    waypoints.init();
1485    truncators.init();
1486    horizontalScrollers.init();
1487    sharing.init();
1488
1489    // misc modules
1490    misc.focusInputLabels();
1491    misc.markHeaderUtilityChildren();
1492    misc.cullEmptyColumns();
1493    // misc.columnizeNav();
1494
1495    document.documentElement.classList.remove('no-js');
1496    document.documentElement.classList.add('js');
1497  };
1498
1499
1500  /* =public
1501   * =======
1502   */
1503
1504  return {
1505    toggles: toggles,
1506    tabs: tabs,
1507    sharedHeights: sharedHeights,
1508    waypoints: waypoints,
1509    sharing: sharing,
1510    horizontalScrollers: horizontalScrollers,
1511    init: init
1512  };
1513
1514}(window, document));
1515
1516RDJS.init();
1517
1518//# 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.