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.