1/****************** 2* 3* Logs events in GoogleAnalitycs 4* 5* 6* Documentation: see `analytics.README.md` 7* 8*/ 9if (typeof FN === 'undefined') { 10 var FN = {}; 11} 12 13FN.analytics = { 14 init: function init() { 15 FN.analytics._setTriggers(); 16 }, 17 18 /** 19 Array where events are configured: { 20 'config': { 21 'selector': '.button__buy--header.analytics--click' --> CSS selector to match the desired HTML element; must also include an ".analytics--" class 22 'waitBeforeDefault': true, --> determines if 'preventDefault' should be called to wait before the page unloads (and therefore interrupts data sending) 23 'triggerOnlyOnce': true --> determines if the event handler should be removed after one execution 24 }, 25 'event': { 26 'eventCategory': 'sales', --> eventCategory string (analytics data) 27 'eventAction': 'buy-button', --> eventAction string (analytics data) 28 'eventLabelSuffix': 'header', --> eventLabelSuffix string (analytics data) - is added to an automatic label prefix string 29 } 30 } 31 */ 32 events: [], 33 34 /** 35 Adds an array of configured events to the global list of tracked events 36 Note: each page should add its own events with this method 37 */ 38 addConfiguredEvents: function addConfiguredEvents(eventsList) { 39 FN.analytics.events = FN.analytics.events.concat(eventsList); 40 }, 41 42 /** 43 Sends analytics data to "Google Analytics" 44 via Google Tag Manager's "Data Layer" object 45 Parameters: eventData { 46 'eventCategory': string, 47 'eventAction': string, 48 'eventLabelSuffix': string 49 } 50 */ 51 sendEventToGA: function sendEventToGA(eventData) { 52 var eventLabel = FN.analytics._createEventLabelString(eventData.eventLabelSuffix); 53 54 var eventObject = FN.analytics._createEventObjectForGA(eventData.eventCategory, eventData.eventAction, eventLabel); // Sends the event to Google Analytics 55 56 57 if (typeof dataLayer !== 'undefined') { 58 dataLayer.push(eventObject); 59 } 60 }, 61 62 /** 63 Sets up event handlers for click and submit events 64 Only to elements that have CSS classes of ".analytics--click" or ".analytics--submit" 65 */ 66 _setTriggers: function _setTriggers() { 67 var clickNamespace = 'click.analytics'; 68 $(document).on(clickNamespace, '.analytics--click', function (event) { 69 FN.analytics._handleEvent($(this), event, clickNamespace); 70 }); 71 var submitNamespace = 'submit.analytics'; 72 $(document).on(submitNamespace, '.analytics--submit', function (event) { 73 FN.analytics._handleEvent($(this), event, submitNamespace); 74 }); 75 }, 76 77 /** 78 Handles the events, finds the corresponding configuration and initiates data sending to analytics 79 */ 80 _handleEvent: function _handleEvent(domElement, domEvent, eventNamespace) { 81 var eventConfiguration = false; // find event configuration in FN.analytics.events 82 83 for (var i = 0; i < FN.analytics.events.length; i++) { 84 if (domElement.is(FN.analytics.events[i].config.selector)) { 85 eventConfiguration = FN.analytics.events[i]; 86 break; 87 } 88 } 89 90 if (eventConfiguration == false) return false; // no matching event configuration found 91 // send analitycs data 92 93 FN.analytics._prepareSendingToGA({ 94 'domElement': domElement, 95 'domEvent': domEvent, 96 'eventConfiguration': eventConfiguration 97 }); // remove event handler, if 'triggerOnlyOnce' is configured 98 99 100 if ('triggerOnlyOnce' in eventConfiguration.config && eventConfiguration.config.triggerOnlyOnce === true) { 101 $(domElement).off(eventNamespace); 102 } 103 }, 104 105 /** 106 Prepares sending data to "Google Analytics" 107 Parameters: options = { 108 'domElement': DOM Element, 109 'domEvent': DOM Event, 110 'eventConfiguration': event configuration from FN.analytics.events 111 } 112 */ 113 _prepareSendingToGA: function _prepareSendingToGA(options) { 114 // Links that load in the same browser window must be postponed to prevent 115 // the browser from unloading the current page before analytics data has been sent 116 if ("waitBeforeDefault" in options.eventConfiguration.config && options.eventConfiguration.config.waitBeforeDefault === true) { 117 var continueLoading = function continueLoading() { 118 if (!finishedSending) { 119 finishedSending = true; // NOTE: currently, waiting is only implemented for "click" events 120 121 if (options.domEvent.type == 'click') { 122 document.location = $(options.domElement).attr('href'); 123 } 124 } 125 }; 126 127 // Prevent the browser from unloading the current page 128 options.domEvent.preventDefault(); // Create a timeout to call `continueLoading` after one second. 129 130 setTimeout(continueLoading, 1000); // Keeps track of whether or not the data has been sent alreay. This prevents the action from being exectude twice in cases where `hitCallback` fires normally. 131 132 var finishedSending = false;
133 options.eventCallback = continueLoading; 134 } 135 136 FN.analytics.sendEventToGA({ 137 'eventCategory': options.eventConfiguration.event.eventCategory, 138 'eventAction': options.eventConfiguration.event.eventAction, 139 'eventLabelSuffix': options.eventConfiguration.event.eventLabelSuffix 140 }); 141 }, 142 143 /** 144 Creates a JS object with suitable format/keys for sending to Google Analytics / Google Tag Manager 145 */ 146 _createEventObjectForGA: function _createEventObjectForGA(category, action, label) { 147 return { 148 'event': 'customEvent', 149 'eventCategory': category, 150 'eventAction': action, 151 'eventLabel': label 152 }; 153 }, 154 155 /** 156 Create a proper event label string 157 */ 158 _createEventLabelString: function _createEventLabelString(eventLabelSuffix) { 159 var eventLabel = FN.config.analytics.siteID; 160 eventLabel += FN.config.analytics.addPlatformToEventLabel ? '/' + FN.helpers.platform() : ''; 161 eventLabel += '/' + eventLabelSuffix; 162 return eventLabel; 163 } 164}; 165$(document).ready(function () { 166 FN.analytics.init(); 167}); 168if (typeof FN == 'undefined') FN = {}; 169FN.contentDownload = { 170 selectors: { 171 'downloadButtonClass': 'content-download-link', 172 'contentDownloadOverlayBaseClass': 'overlay__container--content-download' 173 }, 174 init: function init() { 175 $(document).on('click', '.' + FN.contentDownload.selectors.downloadButtonClass, function (event) { 176 event.preventDefault(); 177 var clickedElement = this; 178 FN.contentDownload.startDownload(clickedElement); 179 FN.contentDownload.showPopup(); 180 }); 181 }, 182 startDownload: function startDownload(clickedElement) { 183 if (!$(clickedElement).is('a')) return; 184 var url = $(clickedElement).attr('href'); 185 var iFrame = '<iframe style="display: none !important" src="' + url + '">'; 186 $(iFrame).appendTo('body'); 187 }, 188 showPopup: function showPopup() { 189 var baseElementSelector = '.' + FN.contentDownload.selectors.contentDownloadOverlayBaseClass; 190 FN.overlay.init({ 191 baseElement: baseElementSelector 192 }); 193 } 194}; 195$(document).ready(function () { 196 FN.contentDownload.init(); 197}); 198function _typeof(obj) { "@babel/helpers - typeof"; if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } 199 200/*! 201 * JavaScript Cookie v2.1.0 202 * https://github.com/js-cookie/js-cookie 203 * 204 * Copyright 2006, 2015 Klaus Hartl & Fagner Brack 205 * Released under the MIT license 206 */ 207(function (factory) { 208 if (typeof define === 'function' && define.amd) { 209 define(factory); 210 } else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === 'object') { 211 module.exports = factory(); 212 } else { 213 var _OldCookies = window.Cookies; 214 var api = window.Cookies = factory(); 215 216 api.noConflict = function () { 217 window.Cookies = _OldCookies; 218 return api; 219 }; 220 } 221})(function () { 222 function extend() { 223 var i = 0; 224 var result = {}; 225 226 for (; i < arguments.length; i++) { 227 var attributes = arguments[i]; 228 229 for (var key in attributes) { 230 result[key] = attributes[key]; 231 } 232 } 233 234 return result; 235 } 236 237 function init(converter) { 238 function api(key, value, attributes) { 239 var result; // Write 240 241 if (arguments.length > 1) { 242 attributes = extend({ 243 path: '/' 244 }, api.defaults, attributes); 245 246 if (typeof attributes.expires === 'number') { 247 var expires = new Date(); 248 expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5); 249 attributes.expires = expires; 250 } 251 252 try { 253 result = JSON.stringify(value); 254 255 if (/^[\{\[]/.test(result)) { 256 value = result; 257 } 258 } catch (e) {} 259 260 if (!converter.write) { 261 value = encodeURIComponent(String(value)).replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); 262 } else { 263 value = converter.write(value, key); 264 } 265 266 key = encodeURIComponent(String(key)); 267 key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent); 268 key = key.replace(/[\(\)]/g, escape); 269 return document.cookie = [key, '=', value, attributes.expires && '; expires=' + attributes.expires.toUTCString(), // use expires attribute, max-age is not supported by IE 270 attributes.path && '; path=' + attributes.path, attributes.domain && '; domain=' + attributes.domain, attributes.secure ? '; secure' : ''].join(''); 271 } // Read 272 273 274 if (!key) { 275 result = {}; 276 } // To prevent the for loop in the first place assign an empty array 277 // in case there are no cookies at all. Also prevents odd result when 278 // calling "get()" 279 280 281 var cookies = document.cookie ? document.cookie.split('; ') : []; 282 var rdecode = /(%[0-9A-Z]{2})+/g; 283 var i = 0; 284 285 for (; i < cookies.length; i++) { 286 var parts = cookies[i].split('='); 287 var name = parts[0].replace(rdecode, decodeURIComponent); 288 var cookie = parts.slice(1).join('='); 289 290 if (cookie.charAt(0) === '"') { 291 cookie = cookie.slice(1, -1); 292 } 293 294 try { 295 cookie = converter.read ? converter.read(cookie, name) : converter(cookie, name) || cookie.replace(rdecode, decodeURIComponent); 296 297 if (this.json) { 298 try { 299 cookie = JSON.parse(cookie); 300 } catch (e) {} 301 } 302 303 if (key === name) { 304 result = cookie; 305 break; 306 } 307 308 if (!key) { 309 result[name] = cookie; 310 } 311 } catch (e) {} 312 } 313 314 return result; 315 } 316 317 api.get = api.set = api; 318 319 api.getJSON = function () { 320 return api.apply({ 321 json: true 322 }, [].slice.call(arguments)); 323 }; 324 325 api.defaults = {}; 326 327 api.remove = function (key, attributes) { 328 api(key, '', extend(attributes, { 329 expires: -1 330 })); 331 }; 332 333 api.withConverter = init; 334 return api; 335 } 336 337 return init(function () {}); 338}); 339/****************** 340* Helper functions 341*******************/ 342if (typeof FN == 'undefined') FN = {}; 343FN.helpers = { 344 /**************** 345 * Returns true if the display is HDPI 346 */ 347 isRetinaDisplay: function isRetinaDisplay() { 348 return window.matchMedia && (window.matchMedia('only screen and (min-resolution: 124dpi), only screen and (min-resolution: 1.3dppx), only screen and (min-resolution: 48.8dpcm)').matches || window.matchMedia('only screen and (-webkit-min-device-pixel-ratio: 1.3), only screen and (-o-min-device-pixel-ratio: 2.6/2), only screen and (min--moz-device-pixel-ratio: 1.3), only screen and (min-device-pixel-ratio: 1.3)').matches) || window.devicePixelRatio && window.devicePixelRatio > 1.3; 349 }, 350 351 /**************** 352 * Returns true if the device is a mobile one (iPhone, iPad, Android...) 353 */ 354 isMobileDevice: function isMobileDevice() { 355 var userAgent = navigator.userAgent || navigator.vendor || window.opera; 356 return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); 357 }, 358 359 /**************** 360 * Returns the value of a given URL parameter 361 */ 362 getURLParameterByName: function getURLParameterByName(name) { 363 name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
364 var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), 365 results = regex.exec(location.search); 366 return results === null ? "" : decodeURIComponent(results[1].replace(/\+/g, " ")); 367 }, 368 369 /**************** 370 * Returns the name of the user's Operating System 371 */ 372 platform: function platform() { 373 if ('preferredOS' in FN && (FN.preferredOS === 'mac' || FN.preferredOS === 'windows')) { 374 return FN.preferredOS; 375 } 376 377 if (navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i)) { 378 return 'mac'; 379 } else if (navigator.platform.match(/(Win)/i)) { 380 return 'windows'; 381 } else { 382 return 'other'; 383 } 384 }, 385 386 /**************** 387 * Returns true if the given element is visible in the viewport 388 * As optional parameters, a minimum amount of visible Pixels or percent can be specified 389 * options = {element: <jQuery el>, minimumVisiblePixels: <number>, minimumVisiblePercent: <number>} 390 */ 391 isVisibleInViewport: function isVisibleInViewport(options) { 392 var elementHeight = $(options.element).outerHeight(); 393 var elementTop = $(options.element).offset().top; 394 var elementBottom = elementTop + elementHeight; 395 var viewportTop = $(window).scrollTop(); 396 var viewportBottom = viewportTop + $(window).height(); 397 var minimumVisiblePixels = 0; 398 if (options.minimumVisiblePixels) minimumVisiblePixels = options.minimumVisiblePixels; 399 if (options.minimumVisiblePercent) minimumVisiblePixels = options.minimumVisiblePercent / 100 * elementHeight; 400 var alreadyOnPage = elementBottom - minimumVisiblePixels > viewportTop; 401 var notYetOffPage = elementTop + minimumVisiblePixels < viewportBottom; 402 return alreadyOnPage && notYetOffPage; 403 }, 404 405 /********** 406 * Takes a relative URL to page or asset and returns a correct URL path 407 * This happens simply by replacing the following placeholder in the given string: 408 * * "<base/>" 409 * 410 * Note: FN.config.urls.base must be set! 411 */ 412 absoluteUrl: function absoluteUrl(url) { 413 var transformedUrl = url; 414 var pattern, regex; 415 416 if ('config' in FN && 'urls' in FN.config && 'base' in FN.config.urls) { 417 pattern = "<base\\/>"; 418 regex = new RegExp(pattern, 'g'); 419 transformedUrl = transformedUrl.replace(regex, FN.config.urls.base); 420 } 421 422 return transformedUrl; 423 }, 424 425 /********* 426 * Search all defined "responsiveBreakpoints" and return the pixel size for a given breakpointName ("m", "xl"...) 427 */ 428 getResponsiveBreakpointByName: function getResponsiveBreakpointByName(breakpointName) { 429 if (!'config' in FN || !'responsiveBreakpoints' in FN.config) return false; 430 431 for (var b = 0; b < FN.config.responsiveBreakpoints.length; b++) { 432 if (FN.config.responsiveBreakpoints[b]['media'] === breakpointName) { 433 return FN.config.responsiveBreakpoints[b]; 434 } 435 } 436 }, 437 438 /********* 439 * Escape a string for use inside a double-quoted HTML attribute. Content 440 * authored in the Markdown files runs through here before being interpolated 441 * into markup, so that an apostrophe or a quote ("Git's power") cannot end 442 * the attribute early. 443 */ 444 escapeAttribute: function escapeAttribute(value) { 445 if (value === null || typeof value === 'undefined') return ''; 446 return String(value).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>'); 447 } 448}; 449/****************** 450* 451* Newsletter signup management 452* 453* Documentation: see `newsletter.README.md` 454* 455*/ 456if (typeof FN == 'undefined') FN = {}; 457FN.newsletterConfig = { 458 instances: [], 459 messages: { 460 pleaseProvideEmailAddress: "Please provide a valid email address.", 461 pleaseAcceptPrivacyPolicy: "Please read and accept our Privacy Policy.", 462 pleaseAcceptTerms: "Please read and accept our terms.", 463 pleaseCheckRequiredTags: "If you want to hear from us, please select the options above." 464 }, 465 selectors: { 466 formClass: 'newsletter-form', 467 emailInput: 'input[name="email"]', 468 tagItemClass: 'signup-tag__item', 469 consentItemClass: 'consent__item', 470 consentTextClass: 'consent__text', 471 subscriptionProfileFormID: 'newsletter-form--profile' 472 }, 473 formStatus: { 474 error: 'error', 475 loading: 'loading', 476 success: 'success' 477 } 478}; 479 480FN.newsletter = function (element) { 481 /**************** 482 * Initialization 483 */ 484 this.__init = function (element) { 485 this.baseElement = element;
486 this.startData = $(this.baseElement).serializeArray(); 487 $(this.baseElement).on('submit', $.proxy(this.submit, this)); 488 }; 489 /** 490 * Submit the form data 491 */ 492 493 494 this.submit = function (event) { 495 event.preventDefault(); 496 var formStatus = this.validateForm(); 497 498 if (formStatus.status === FN.newsletterConfig.formStatus.error) { 499 this.displayFormStatus(formStatus); 500 return false; 501 } 502 503 var requestURL = this.prepareRequestURL(); 504 var requestData = this.prepareFormData(); 505 var requestOptions = this.prepareRequestOptions(); 506 $(this.baseElement).find('input[type="submit"]').attr('disabled', 'disabled'); 507 FN.remoteRequest.sendRequest(requestURL, requestData, requestOptions); 508 }; 509 /** 510 * Validate the form before submitting 511 */ 512 513 514 this.validateForm = function () { 515 var formStatus = { 516 'status': false, 517 'messages': [] 518 }; // check email 519 520 if (!$(this.baseElement).find(FN.newsletterConfig.selectors.emailInput).val().match(/^.+@.+\..+/g)) { 521 formStatus.messages.push({ 522 'message': FN.newsletterConfig.messages.pleaseProvideEmailAddress 523 }); 524 } // check that tags are selected 525 526 527 var tagItems = $(this.baseElement).find('.' + FN.newsletterConfig.selectors.tagItemClass); 528 529 for (var i = 0; i < tagItems.length; i++) { 530 var requiredTag = typeof $(tagItems[i]).data('required') != 'undefined' ? $(tagItems[i]).data('required') : false; 531 532 if (requiredTag && !$(tagItems[i]).prop('checked')) { 533 formStatus.messages.push({ 534 'message': FN.newsletterConfig.messages.pleaseCheckRequiredTags 535 }); 536 } 537 } // check consent / privacy policy 538 539 540 var consentItems = $(this.baseElement).find('.' + FN.newsletterConfig.selectors.consentItemClass); 541 542 for (var i = 0; i < consentItems.length; i++) { 543 var requiredConsent = $(consentItems[i]).data('required'); 544 545 if (requiredConsent && !$(consentItems[i]).prop('checked')) { 546 if ($(consentItems[i]).attr('id') === 'consent--privacy') { 547 formStatus.messages.push({ 548 'message': FN.newsletterConfig.messages.pleaseAcceptPrivacyPolicy 549 }); 550 } else { 551 formStatus.messages.push({ 552 'message': FN.newsletterConfig.messages.pleaseAcceptTerms 553 }); 554 } 555 } 556 } 557 558 if (formStatus.messages.length == 0) { 559 formStatus.status = FN.newsletterConfig.formStatus.success; 560 } else { 561 formStatus.status = FN.newsletterConfig.formStatus.error; 562 } 563 564 return formStatus; 565 }; 566 /** 567 * Collect and prepare form data 568 */ 569 570 571 this.prepareFormData = function () { 572 var formData = $(this.baseElement).serializeArray(); // go through all tag items (checkboxes) and find those that are NOW UNSELECTED 573 574 $(this.baseElement).find('.' + FN.newsletterConfig.selectors.tagItemClass).each(function (index, element) { 575 var wasCheckedAtStart = $(element).attr('data-start-value') == 1; 576 var isCheckedNow = $(element).is(':checked'); 577 578 if (wasCheckedAtStart && !isCheckedNow) { 579 var tagName = $(element).attr('data-tag-name'); 580 formData.push({ 581 name: 'removeTags[]', 582 value: tagName 583 }); 584 } 585 }); // get consent text + date 586 587 var consentText = ''; 588 $(this.baseElement).find('.' + FN.newsletterConfig.selectors.consentTextClass).each(function (index, element) { 589 if ($(element).prop('checked')) { 590 if (consentText !== '') consentText += ' '; 591 consentText += $(element).closest('label').text(); 592 } 593 }); 594 595 if (consentText) { 596 formData.push({ 597 name: 'consent[date]', 598 value: new Date().toUTCString() 599 }); 600 formData.push({ 601 name: 'consent[text]', 602 value: consentText 603 }); 604 } 605 606 return formData; 607 }; 608 /** 609 * Configure request options 610 */ 611 612 613 this.prepareRequestOptions = function () { 614 var messageContainer = $(this.baseElement).find('.request-notice'); 615 var requestOptions = { 616 'responseContainer': messageContainer, 617 'onComplete': $.proxy(this.handleFormResponse, this), 618 'redirectOnSuccess': false 619 }; 620 return requestOptions; 621 }; 622 /** 623 * Choose the correct request URL 624 */ 625 626 627 this.prepareRequestURL = function () { 628 var requestURL = FN.config.urls.apiBase; 629 var subscriptionStatus = $(this.baseElement).attr('data-subscription-status'); 630 631 if (subscriptionStatus === 'subscribed') { 632 requestURL += FN.config.urls.apiContactUpdate; 633 } else { 634 requestURL += FN.config.urls.apiContactSubscribe; 635 } 636 637 return requestURL; 638 }; 639 /** 640 * Display the status of the form 641 */ 642 643 644 this.displayFormStatus = function (formStatus) { 645 var messageContainer = $(this.baseElement).find('.request-notice'); 646 var messageHtml = ''; 647 648 for (var i = 0; i < formStatus.messages.length; i++) { 649 messageHtml += '<p>' + formStatus.messages[i].message + '</p>'; 650 } 651
652 $(messageContainer).html(messageHtml).removeClass('success error loading').addClass(formStatus.status); 653 654 if (formStatus.messages.length == 0) { 655 $(messageContainer).hide(); 656 } else { 657 $(messageContainer).show(); 658 } 659 }; 660 /** 661 * Handle the request response 662 */ 663 664 665 this.handleFormResponse = function (xhr, options) { 666 $(this.baseElement).find('input[type="submit"]').removeAttr('disabled'); 667 668 if (xhr.status < 400) { 669 // trigger a custom jQuery event named "FN.newsletter:success", which other code can listen to: 670 // $(document).on("FN.newsletter:success", function(event, context){ ... 671 $(document).trigger("FN.newsletter:success", { 672 'responseData': xhr, 673 'formElement': $(this.baseElement), 674 'options': options 675 }); // Create and dispatch native event for non-jQuery listeners 676 677 var event = new CustomEvent("TowerNewsletterSubscribed", { 678 "detail": { 679 "responseData": xhr, 680 "formElement": $(this.baseElement), 681 "options": options 682 } 683 }); 684 document.dispatchEvent(event); 685 } 686 }; 687 /***************** 688 * Constructor 689 */ 690 691 692 this.__init(element); 693}; 694/****************** 695* Overlay 696* 697* 698* Creates a simple HTML container element that overlays (and possibly dims) the whole viewport. 699* Often used to show popup windows. 700* 701*******************/ 702if (typeof FN == 'undefined') FN = {}; 703FN.overlay = { 704 config: { 705 fadeDuration: 500 706 }, 707 selectors: { 708 baseElementClass: 'overlay__container', 709 closeButtonClass: 'overlay__button--close', 710 contentElementClass: 'overlay__content' 711 }, 712 init: function init(options) { 713 var baseElement = $(options.baseElement); // close button event handling 714 715 $(baseElement).find('.' + FN.overlay.selectors.closeButtonClass).on('click', FN.overlay.close); // show 716 717 FN.overlay.show(baseElement); 718 }, 719 show: function show(baseElement) { 720 $(baseElement).addClass('visible'); 721 FN.overlay.showContent(0, baseElement); 722 }, 723 showContent: function showContent(contentIndex, baseElement) { 724 if (!baseElement) baseElement = $('.' + FN.overlay.selectors.baseElementClass); 725 $(baseElement).find('.' + FN.overlay.selectors.contentElementClass).each(function (index, element) { 726 if (index === contentIndex) { 727 $(element).fadeIn(FN.overlay.config.fadeDuration).removeClass('hidden'); 728 } else { 729 $(element).fadeOut(FN.overlay.config.fadeDuration).addClass('hidden'); 730 } 731 }); 732 }, 733 close: function close(event) { 734 var baseElement = $(event.target).closest('.' + FN.overlay.selectors.baseElementClass); 735 var contentElements = $(baseElement).find('.' + FN.overlay.selectors.contentElementClass); 736 $(contentElements).fadeOut(FN.overlay.config.fadeDuration, function () { 737 $(baseElement).removeClass('visible'); 738 }); 739 event.preventDefault(); 740 } 741}; 742/*********** 743* Usage 744*/ 745 746/* 747var overlay = FN.overlay.init({ 748 baseElement: '.overlay-wrapper' 749}); 750*/ 751/****************** 752* Create and insert a PopUp (for getting newsletter signups etc.) 753* 754* 755* Documentation: see `popups.README.md` 756* 757*/ 758if (typeof FN == 'undefined') FN = {}; 759FN.popupConfig = { 760 configuredItems: {}, 761 instances: {}, 762 763 /** Search configuredItems for a popup for _this_ page and then create/init it */ 764 initConfiguredItems: function initConfiguredItems() { 765 FN.popupConfig.instances = {}; 766 767 var popupsForCurrentPage = FN.popupConfig._findActivePopupsForCurrentPage(); 768 769 for (var p = 0; p < popupsForCurrentPage.length; p++) { 770 FN.popupConfig.createPopupWithConfig(popupsForCurrentPage[p]); 771 } 772 }, 773 774 /** Create a new FN.popup instance and add it */ 775 createPopupWithConfig: function createPopupWithConfig(popupConfig) { 776 if (popupConfig.id in FN.popupConfig.instances) return; 777 FN.popupConfig.instances[popupConfig.id] = new FN.popup(popupConfig); 778 }, 779 780 /** adds a popup configuration */ 781 addPopupConfig: function addPopupConfig(config) { 782 if (!config) return; 783 FN.popupConfig.configuredItems[config.id] = config; 784 }, 785 786 /** 787 Search FN.popupConfig.configuredItems for an active popup configuration for this page 788 Note: - 'active' must be true 789 - 'pagePaths' must contain a regEx that matches the current window.location.pathname 790 */ 791 _findActivePopupsForCurrentPage: function _findActivePopupsForCurrentPage() { 792 var popupsForCurrentPage = []; 793 794 for (var key in FN.popupConfig.configuredItems) { 795 if (!FN.popupConfig.configuredItems[key].active) continue; 796 797 for (var p = 0; p < FN.popupConfig.configuredItems[key].pagePaths.length; p++) { 798 var re = new RegExp(FN.popupConfig.configuredItems[key].pagePaths[p]); 799 var match = re.exec(window.location.pathname); 800 if (match) popupsForCurrentPage.push(FN.popupConfig.configuredItems[key]); 801 } 802 } 803 804 return popupsForCurrentPage; 805 } 806}; 807 808FN.popup = function (config) { 809 /** 810 Create and initialize a configured popup 811 */ 812 this._initWithConfig = function (popupConfig) { 813 if (!popupConfig) return false;
814 this.id = popupConfig.id; 815 this.config = popupConfig; 816 if (this._checkHasBeenClosedBefore()) return false; 817 818 var html = this._generateHtmlContent(); 819 820 html = this._wrapHtmlContent(html); 821 if (!html) return false; 822 823 this._insertPopupHtml(html); 824 825 this._attachEventHandlers(); 826 827 this._manageFirstDisplay(); // add analytics event listeners 828 829 830 var popupAnalyticsEvents = [{ 831 'config': { 832 'selector': '#' + this.id + ' .fn-popup__newsletter-form.analytics--submit', 833 'triggerOnlyOnce': true 834 }, 835 'event': { 836 'eventCategory': 'newsletter', 837 'eventAction': 'signup', 838 'eventLabelSuffix': this.id 839 } 840 }, { 841 'config': { 842 'selector': '#' + this.id + ' .fn-popup__button--link.analytics--click' 843 }, 844 'event': { 845 'eventCategory': 'page-interaction', 846 'eventAction': 'landing-page', 847 'eventLabelSuffix': this.id 848 } 849 }, { 850 'config': { 851 'selector': '#' + this.id + ' .fn-popup__button--tweet.analytics--click' 852 }, 853 'event': { 854 'eventCategory': 'social', 855 'eventAction': 'share', 856 'eventLabelSuffix': this.id 857 } 858 }]; 859 if ('analytics' in FN) FN.analytics.addConfiguredEvents(popupAnalyticsEvents); 860 }; 861 /** 862 Insert the generated HTML into the DOM 863 */ 864 865 866 this._insertPopupHtml = function (html) { 867 switch (this.config.type) { 868 case 'sidebar': 869 case 'modal': 870 case 'welcome-mat': 871 $('body').append(html); 872 break; 873 874 case 'inline': 875 var popupPlaceholders = $('.popup-placeholder'); 876 877 for (var i = 0; i < popupPlaceholders.length; i++) { 878 if ($(popupPlaceholders[i]).data('popupId') === this.id) { 879 $(popupPlaceholders[i]).replaceWith(html); 880 } 881 } 882 883 break; 884 } 885 }; 886 /** 887 Wraps the HTML content blocks in HTML containers 888 */ 889 890 891 this._wrapHtmlContent = function (contentHtml) { 892 var html = ''; 893 html += '<div id="' + this.id + '" class="fn-popup fn-popup--' + this.config.type + ' fn-popup--hidden">'; 894 895 switch (this.config.type) { 896 case 'sidebar': 897 html += '<div class="fn-popup__content-container">'; 898 html += ' <div class="fn-popup__close"></div>'; 899 html += contentHtml; 900 html += '</div>'; 901 break; 902 903 case 'inline': 904 html += '<div class="fn-popup__content-container">'; 905 html += ' <div class="fn-popup__close"></div>'; 906 html += contentHtml; 907 html += '</div>'; 908 break; 909 910 case 'modal': 911 html += '<div class="fn-popup__overlay"></div>'; 912 html += '<div class="fn-popup__content-container">'; 913 html += ' <div class="fn-popup__close"></div>'; 914 html += contentHtml; 915 html += '</div>'; 916 break; 917 918 case 'welcome-mat': 919 html += '<div class="fn-popup__overlay"></div>'; 920 html += '<div class="fn-popup__close"></div>'; 921 html += '<div class="fn-popup__content-container">' + contentHtml + '</div>'; 922 break; 923 } 924 925 return html; 926 }; 927 /** 928 Generate appropriate HTML 929 */ 930 931 932 this._generateHtmlContent = function () { 933 var html = ''; 934 935 for (var i = 0; i < this.config.content.length; i++) { 936 var content = this.config.content[i]; 937 html += '<div class="fn-popup__content fn-popup__content--' + (i + 1) + '">'; 938 939 switch (content.type) { 940 case 'content': 941 html += '<div class="fn-popup__text">' + content['text'] + '</div>'; 942 html += '<a class="fn-popup__button fn-popup__button--next">' + content['action-button'] + '</a>'; 943 break; 944 945 case 'link': 946 html += '<div class="fn-popup__text">' + content['text'] + '</div>'; 947 html += '<a class="fn-popup__button fn-popup__button--link analytics--click">' + content['action-button'] + '</a>'; 948 break; 949 950 case 'tweet': 951 html += '<div class="fn-popup__text">' + content['text'] + '</div>'; 952 html += '<a class="fn-popup__button fn-popup__button--tweet analytics--click">' + content['action-button'] + '</a>'; 953 954 if ('footer' in content) { 955 html += '<div class="fn-popup__footer">' + content['footer'] + '</div>'; 956 } 957 958 break; 959 960 case 'newsletter-signup': 961 html += '<div class="fn-popup__text">' + content['text'] + '</div>'; 962 html += '<div class="fn-popup__input">'; 963 html += ' <form class="fn-popup__newsletter-form analytics--submit" autocomplete="off">'; 964 html += ' <input type="hidden" name="action" value="subscribe" />'; 965 html += ' <input type="hidden" name="format" value="json" />'; 966 html += ' <input type="hidden" name="listName" value="' + (content['list-name'] ? content['list-name'] : 'tower') + '" />'; 967 html += ' <input type="hidden" name="signup[medium]" value="' + content['signup-medium'] + '" />'; 968 html += ' <input type="hidden" name="signup[URI]" value="' + content['signup-URI'] + '" />'; 969 html += ' <input type="hidden" name="signup[OS]" value="' + FN.helpers.platform() + '" />'; 970 971 if ('signup-form' in content) { 972 html += ' <input type="hidden" name="signupForm" value="' + content['signup-form'] + '" />'; 973 } 974 975 if ('tags' in content) { 976 html += '<div class="newsletter-form__groups">'; 977 978 for (var t = 0; t < content['tags'].length; t++) { 979 var hidden = 'hidden' in content['tags'][t] && content['tags'][t]['hidden'] ? ' style="display:none;" ' : ''; 980 var defaultChecked = 'default' in content['tags'][t] && content['tags'][t]['default'] ? ' checked="checked" ' : ''; 981 var requiredTag = 'required' in content['tags'][t] ? content['tags'][t]['required'] : ''; 982 var cssClass = 'signup-tag__item' + ('collectConsent' in content['tags'][t] && content['tags'][t]['collectConsent'] ? ' consent__text' : ''); 983 html += '<div class="row"' + hidden + '><label>'; 984 html += '<input class="' + cssClass + '" type="checkbox" name="tags[]" value="' + content['tags'][t]['id'] + '" id="signup-tag__' + content['tags'][t]['id'] + '"' + defaultChecked + ' data-required="' + requiredTag
984+ '" />'; 985 html += 'label' in content['tags'][t] ? content['tags'][t]['label'] : ''; 986 html += '</label></div>'; 987 } 988 989 html += '</div>'; 990 } 991 992 if ('consent' in content) { 993 html += '<div class="newsletter-form__consent">'; 994 995 for (var c = 0; c < content['consent'].length; c++) { 996 html += '<label><input type="checkbox" id="consent--' + content['consent'][c]['id'] + '" class="consent__item consent__text" data-required="' + content['consent'][c]['required'] + '" />' + content['consent'][c]['text'] + '</label>'; 997 } 998 999 html += '</div>'; 1000 } 1001 1002 html += '<div class="newsletter-form__submit">'; 1003 html += '<input type="text" name="email" placeholder="Email Address" />'; 1004 html += '<input type="submit" value="' + content['action-button'] + '" class="fn-popup__button--signup" />'; 1005 1006 if ('close-button' in content) { 1007 html += ' <a class="fn-popup__button fn-popup__button--close analytics--click">' + content['close-button'] + '</a>'; 1008 } 1009 1010 html += '</div>'; 1011 html += ' <div class="request-notice"></div>'; 1012 html += ' </form>'; 1013 html += '</div>'; 1014 break; 1015 } 1016 1017 html += '</div>'; 1018 } 1019 1020 return html; 1021 }; 1022 /** 1023 Manages the first display of the popup (either when reaching a certain scrolling position or directly after loading the page) 1024 */ 1025 1026 1027 this._manageFirstDisplay = function () { 1028 if (this.config.display.event === 'onScroll') { 1029 $(window).on('scroll.fn-popup', $.proxy(this._showOnScroll, this)); 1030 } else if (this.config.display.event === 'onLoad') { 1031 this._showOnLoad(); 1032 } else if (this.config.display.event === 'onClick') { 1033 $(document).on('click', this.config.display.selector, $.proxy(this._showOnClick, this)); 1034 } 1035 }; 1036 1037 this._showOnScroll = function () { 1038 var scrollingPosition = $(document).scrollTop() + $(window).height(); 1039 1040 if ("beforeElement" in this.config.display) { 1041 /* 'display': { 1042 'event': 'onScroll', // must be 'onScroll' 1043 'beforeElement': '#chapter ~', // CSS selector of the element that the popup should display when reaching 1044 'withDistance': '500' } // optional amount of pixels that the event should fire before reaching the element 1045 */ 1046 var beforeElement = $(this.config.display.beforeElement); 1047 var offsetTop = beforeElement.offset().top; 1048 if ("withDistance" in this.config.display) offsetTop -= this.config.display.withDistance; 1049 1050 if (scrollingPosition > offsetTop) { 1051 this._revealPopup(); 1052 1053 $(window).off('scroll.fn-popup'); 1054 } 1055 } else if ("percent" in this.config.display) { 1056 /* 'display': { 1057 'event': 'onScroll', // must be 'onScroll' 1058 'percent': '0.7' } // scrolling position, 0 to 1 (bottom of page) 1059 */ 1060 var documentHeight = $(document).height(); 1061 1062 if (scrollingPosition / documentHeight > this.config.display.percent) { 1063 this._revealPopup(); 1064 1065 $(window).off('scroll.fn-popup'); 1066 } 1067 } 1068 }; 1069 1070 this._showOnLoad = function () { 1071 /* 'display': { 1072 'event': 'onLoad', // must be 'onLoad' 1073 'delay': 500 } // delay in miliseconds 1074 */ 1075 var thePopup = this; 1076 setTimeout(function () { 1077 thePopup._revealPopup(); 1078 }, thePopup.config.display.delay); 1079 }; 1080 1081 this._showOnClick = function (event) { 1082 /* 'display': { 1083 'event': 'onClick', // must be 'onClick' 1084 'selector': '.button' } // CSS selector of the element(s) that should trigger the popup on click 1085 */ 1086 event.preventDefault(); 1087 var clickedElement = $(event.target); 1088 1089 this._revealPopup(); 1090 }; 1091 /** 1092 Reveal (display for the first time) an already created popup 1093 */ 1094 1095 1096 this._revealPopup = function () { 1097 var thePopup = this; 1098 1099 if (this.config.type === 'modal') { 1100 $('#' + this.config.id).removeClass('fn-popup--hidden'); 1101 $('#' + this.config.id + ' .fn-popup__content-container').fadeIn(); 1102 } else if (this.config.type === 'welcome-mat') { 1103 $('#' + this.config.id).removeClass('fn-popup--hidden'); 1104 } else if (this.config.type === 'sidebar') { 1105 if ('startSmall' in this.config.display) { 1106 $('#' + this.config.id).addClass('fn-popup--squashed'); 1107 } 1108 1109 $('#' + this.config.id).removeClass('fn-popup--hidden'); 1110 setTimeout(function () { 1111 $('#' + thePopup.id + ' .fn-popup__content--1').fadeIn(); 1112 }, 600); 1113 } else if (this.config.type === 'inline') { 1114 $('#' + this.config.id).removeClass('fn-popup--hidden'); 1115 } 1116 }; 1117 /** 1118 Attach event handlers 1119 */ 1120 1121 1122 this._attachEventHandlers = function () { 1123 // --- close popup 1124 $('#' + this.id + ' .fn-popup__close, #' + this.id + ' .fn-popup__button--close').on('click', $.proxy(this.close, this)); // --- show next content, if any 1125 1126 $('#' + this.id + ' .fn-popup__button--next').on('click', $.proxy(function (event) { 1127 var currentContentOnClick = this._currentContentNumberOnClick($(event.target)); 1128 1129 this.nextOrClose(currentContentOnClick); 1130 }, this)); // --- open link in new window 1131 1132 $('.fn-popup__button--link').on('click', $.proxy(function (event) { 1133 var currentContentOnClick = this._currentContentNumberOnClick($(event.target)); 1134 1135 window.open(this.config.content[currentContentOnClick - 1]['url'], '_blank'); 1136 this.nextOrClose(currentContentOnClick); 1137 }, this)); // --- open tweet window 1138 1139 $('.fn-popup__button--tweet').on('click', $.proxy(function (event) { 1140 var currentContentOnClick = this._currentContentNumberOnClick($(event.target)); 1141 1142 var tweetOptions = this.config.content[currentContentOnClick - 1]['tweet']; 1143 FN.socialShare(tweetOptions.platform, tweetOptions.url, tweetOptions.text); 1144 this.nextOrClose(currentContentOnClick); 1145 }, this)); // --- register newsletter element 1146 1147 var newsletterItem = $('#' + this.id).find('.fn-popup__newsletter-form'); 1148 1149 if (newsletterItem.length > 0) { 1150 FN.newsletterConfig.instances.push(new FN.newsletter(newsletterItem)); 1151 $(document).on("FN.newsletter:success", $.proxy(function (event, context) { 1152 if ($(context.options.responseContainer).parents("#" + this.id).length == 1) { 1153 this._newsletterSubmissionResponse(context.responseData, context.options); 1154 } 1155 }, this)); 1156 } 1157 }; 1158 /** 1159 Shows next content (of multi-step popup contents) 1160 */ 1161 1162 1163 this._showNextContent = function (currentContentNo) { 1164 var thePopup = this; // hide current content 1165 1166 $('#' + this.id + ' .fn-popup__content--' + currentContentNo).hide(); // if configured to grow after the first interaction... 1167 1168 if ('startSmall' in this.config.display && this.config.display['startSmall'] == true) { 1169 if ($('#' + this.id).hasClass('fn-popup--squashed')) { 1170 $('#' + this.id).removeClass('fn-popup--squashed').addClass('fn-popup--stretched'); 1171 } 1172 } // show next content 1173 1174 1175 if ($('#' + this.id + ' .fn-popup__content--' + (parseInt(currentContentNo) + 1))) { 1176 setTimeout(function () { 1177 $('#' + thePopup.id + ' .fn-popup__content--' + (parseInt(currentContentNo) + 1)).fadeIn(); 1178 }, 500); 1179 } 1180 }; 1181 /** 1182 Returns the number (id) of the currently shown content (typicall, either 1, 2, or 3) 1183 */ 1184 1185 1186 this._currentContentNumberOnClick = function (clickedItem) {
1187 var contentContainer = $(clickedItem).closest('.fn-popup__content'); 1188 var contentId = contentContainer.attr('class').match(/fn-popup__content--(\d){1}/i); 1189 1190 if (contentId && contentId[1]) { 1191 return contentId[1]; 1192 } else { 1193 return false; 1194 } 1195 }; 1196 /** 1197 Check if a the popup has been closed on a previous pageview 1198 */ 1199 1200 1201 this._checkHasBeenClosedBefore = function () { 1202 var re = new RegExp(this.id + "=([^;]+)"); 1203 var value = re.exec(document.cookie); 1204 return value !== null ? true : false; 1205 }; 1206 /** 1207 Save a cookie if the user has closed the popup (so he doesn't see it again on his next visit) 1208 */ 1209 1210 1211 this._setPreventReopenCookie = function () { 1212 document.cookie = this.id + '=closed; path=/; expires=Tue, 07 Nov 2084 12:00:00 GMT'; 1213 }; 1214 /** 1215 Callback for async newsletter requests 1216 */ 1217 1218 1219 this._newsletterSubmissionResponse = function (data, options) { 1220 if (data.status === 200) { 1221 var currentContentOnClick = this._currentContentNumberOnClick(options.responseContainer); 1222 1223 this.nextOrClose(currentContentOnClick); 1224 } 1225 }, 1226 /** 1227 Close any open popups 1228 */ 1229 this.close = function () { 1230 $('#' + this.id).addClass('fn-popup--hidden'); 1231 1232 if ('showOnlyOnce' in this.config.display && this.config.display.showOnlyOnce === false) {// don't set a cookie 1233 } else { 1234 this._setPreventReopenCookie(); 1235 } 1236 }; 1237 /** 1238 Show next content or close the popup (if there is no "next" content...) 1239 */ 1240 1241 this.nextOrClose = function (currentContentNo) { 1242 var numberOfContents = this.config.content.length; 1243 1244 if (numberOfContents > currentContentNo) { 1245 this._showNextContent(currentContentNo); 1246 } else if (numberOfContents == currentContentNo) { 1247 this.close(); 1248 } 1249 }; 1250 /***************** 1251 * Constructor 1252 */ 1253 1254 1255 this._initWithConfig(config); 1256}; // load configured popups - but only once the page is completely loaded 1257 1258 1259$(window).on('load', function () { 1260 FN.popupConfig.initConfiguredItems(); 1261}); 1262/****************** 1263* Send and process a remote request (e.g. for newsletter subscription) 1264*******************/ 1265if (typeof FN == 'undefined') FN = {}; 1266FN.remoteRequest = { 1267 sendRequest: function sendRequest(url, requestData, options) { 1268 $(options.responseContainer).html('Your request is being submitted...').removeClass('error').removeClass('success').addClass('info'); 1269 $.ajax({ 1270 type: options.httpMethod ? options.httpMethod : 'POST', 1271 url: url, 1272 data: requestData, 1273 dataType: 'json', 1274 complete: function complete(xhr, textStatus) { 1275 var message; 1276 1277 if (xhr.status >= 200 && xhr.status < 400) { 1278 message = xhr.responseJSON && 'message' in xhr.responseJSON ? xhr.responseJSON.message : "Your request was successfully sent."; 1279 } else { 1280 message = xhr.responseJSON && 'message' in xhr.responseJSON ? xhr.responseJSON.message : "An error occured. Please contact support via email at [email protected]"; 1281 } 1282 1283 FN.remoteRequest.showResult(xhr.status, message, options); 1284 1285 if (options.onComplete) { 1286 if (typeof options.onComplete === "function") { 1287 options.onComplete(xhr, options); 1288 } 1289 } 1290 } 1291 }); 1292 }, 1293 showResult: function showResult(status, message, options) { 1294 var successful = status >= 200 && status < 400; 1295 1296 if (successful && options.disableButtonOnSuccess) { 1297 $(options.disableButtonOnSuccess).attr('disabled', 'disabled'); 1298 } 1299 1300 if (successful && options.disableButtonOnFailure) { 1301 $(options.disableButtonOnFailure).attr('disabled', 'disabled'); 1302 } 1303 1304 if (options.responseContainer) { 1305 $(options.responseContainer).html(message).removeClass('info').addClass(successful ? 'success' : 'error'); 1306 } 1307 } 1308}; 1309/***** Usage example: 1310 1311$(document).ready(function(){ 1312 $('.newsletter-form').on('submit', function(event){ 1313 var requestData = $(this).serialize(); 1314 var messageContainer = $(this).find('.request-notice'); 1315 FN.remoteRequest.sendRequest(FN.remoteRequest.urls.newsletter, requestData, {'responseContainer':message
1315Container}); 1316 event.preventDefault(); 1317 }); 1318}); 1319 1320*/ 1321/****************** 1322* Sharing Sidebar 1323*******************/ 1324if (typeof FN == 'undefined') FN = {}; 1325FN.sharingBar = { 1326 config: { 1327 containerCssSelector: false, 1328 topThresholdCssSelector: false 1329 }, 1330 visible: false, 1331 topThreshold: 0, 1332 init: function init(config) { 1333 FN.sharingBar.config = config; 1334 if ($(FN.sharingBar.config.containerCssSelector).length == 0) return; 1335 FN.sharingBar.resetTopThreshold(); 1336 $(window).on('resize', function () { 1337 FN.sharingBar.resetTopThreshold(); 1338 }); 1339 $(window).on('scroll', function () { 1340 var scrollingPosition = $(window).scrollTop(); 1341 FN.sharingBar.toggle(scrollingPosition > FN.sharingBar.topThreshold); 1342 }); 1343 }, 1344 toggle: function toggle(show) { 1345 if (show === FN.sharingBar.visible) { 1346 return; 1347 } 1348 1349 var newFloatingSidebarPos = show ? '-6px' : '-70px'; 1350 $(FN.sharingBar.config.containerCssSelector).css({ 1351 right: newFloatingSidebarPos 1352 }); 1353 FN.sharingBar.visible = show; 1354 }, 1355 resetTopThreshold: function resetTopThreshold() { 1356 var topThresholdElement = $(FN.sharingBar.config.topThresholdCssSelector); 1357 if (topThresholdElement.length == 0) return; 1358 FN.sharingBar.topThreshold = $(topThresholdElement).position().top; 1359 } 1360}; 1361if (window.FN == undefined) { 1362 FN = {}; 1363} // This function is basically a direct translation of the PHP version in 1364// siteURL/index.php. 1365 1366 1367FN.siteURL = function (site, path) { 1368 var url; 1369 var trailingSlash, leadingSlash; 1370 1371 if (!FN.siteURLs || !FN.siteURLs[site]) { 1372 return ''; 1373 } 1374 1375 url = FN.siteURLs[site]; 1376 trailingSlash = url[url.length - 1] == '/'; 1377 leadingSlash = Boolean(path) && path[0] == '/'; 1378 1379 if (!trailingSlash && !leadingSlash && path) { 1380 url = url + '/' + path; 1381 } else if (trailingSlash && leadingSlash) { 1382 url = url + path.slice(1); 1383 } else if (path) { 1384 url = url + path; 1385 } 1386 1387 return url; 1388}; 1389/****************** 1390* Share on Twitter / Google+ 1391*******************/ 1392if (typeof FN == 'undefined') FN = {}; 1393 1394FN.socialShare = function (service, shareUrl, shareText) { 1395 var serviceUrl; 1396 1397 if (!shareText) { 1398 shareText = window.document.title; 1399 } 1400 1401 if (!shareUrl) { 1402 shareUrl = window.location.href; 1403 } 1404 1405 if (service === 'twitter') { 1406 serviceUrl = 'https://x.com/intent/tweet'; 1407 serviceUrl += '?text=' + encodeURIComponent(shareText); 1408 serviceUrl += '&url=' + encodeURIComponent(shareUrl); 1409 window.open(serviceUrl, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'); 1410 } else if (service === 'facebook') { 1411 serviceUrl = 'https://www.facebook.com/sharer/sharer.php'; 1412 serviceUrl += '?u=' + encodeURIComponent(shareUrl); 1413 window.open(serviceUrl, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'); 1414 } else if (service === 'linkedin') { 1415 serviceUrl = 'http://www.linkedin.com/shareArticle'; 1416 serviceUrl += '?mini=true'; 1417 serviceUrl += '&url=' + encodeURIComponent(shareUrl); 1418 serviceUrl += '&title=' + encodeURIComponent(shareText); 1419 window.open(serviceUrl, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'); 1420 } else if (service === 'buffer') { 1421 serviceUrl = 'https://bufferapp.com/add'; 1422 serviceUrl += '?url=' + encodeURIComponent(shareUrl); 1423 serviceUrl += '&text=' + encodeURIComponent(shareText); 1424 window.open(serviceUrl, '', 'menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=600,width=600'); 1425 } 1426}; 1427/****************** 1428* Create and insert a little Tooltip / Popover window 1429* 1430* 1431* Documentation 1432* See the repository's Wiki on GitHub.com: 1433* https://github.com/fournova/kirby-toolbox/wiki/javascript:-tooltips.js 1434* 1435*/ 1436if (typeof FN == 'undefined') FN = {}; 1437FN.tooltips = { 1438 config: { 1439 dimensions: { 1440 pointerYDistance: 2, 1441 pointerHeightPixels: 14, 1442 pointerWidthPixels: 32, 1443 pointerHorizontalInsetPixels: 6, 1444 boxBorderWidth: 1 1445 }, 1446 selectors: { 1447 tooltipIndicatorClass: 'tooltip-indicator', 1448 tooltipId: 'tooltip', 1449 tooltipContentDataAttr: "data-content", 1450 tooltipPositionDataAttr: "data-pointer-position" 1451 } 1452 }, 1453 1454 /** 1455 Init 1456 */
1457 init: function init() { 1458 $('.' + FN.tooltips.config.selectors.tooltipIndicatorClass).on('mouseenter', function () { 1459 FN.tooltips.showTooltip(this); 1460 }); 1461 $('.' + FN.tooltips.config.selectors.tooltipIndicatorClass).on('mouseleave', function () { 1462 FN.tooltips.hideTooltips(); 1463 }); 1464 }, 1465 1466 /** 1467 Create and show a tooltip 1468 */ 1469 showTooltip: function showTooltip(indicatorItem) { 1470 FN.tooltips.hideTooltips(); // remove all existing tooltips (just to be sure...) 1471 1472 var tooltipText = $(indicatorItem).attr(FN.tooltips.config.selectors.tooltipContentDataAttr); 1473 var tooltipPointerPosition = $(indicatorItem).attr(FN.tooltips.config.selectors.tooltipPositionDataAttr).split('-'); 1474 tooltipPointerPosition = { 1475 y: tooltipPointerPosition[0], 1476 x: tooltipPointerPosition[1] 1477 }; 1478 var html = '<div id="' + FN.tooltips.config.selectors.tooltipId + '">'; 1479 html += '<div class="tooltip__pointer tooltip__pointer--' + tooltipPointerPosition.x + ' tooltip__pointer--' + tooltipPointerPosition.y + '"></div>'; 1480 html += '<div class="tooltip__text">' + tooltipText + '</div>'; 1481 html += '</div>'; 1482 $(indicatorItem).append(html); 1483 var indicatorItemWidth = $(indicatorItem).width(); 1484 var indicatorItemHeight = $(indicatorItem).height(); 1485 var tooltipHeight = $('#' + FN.tooltips.config.selectors.tooltipId).height(); 1486 var tooltipWidth = $('#' + FN.tooltips.config.selectors.tooltipId).width() + 2 * FN.tooltips.config.dimensions.boxBorderWidth; 1487 var tooltipPosition = {}; 1488 1489 if (tooltipPointerPosition.x === 'left') { 1490 tooltipPosition.left = indicatorItemWidth / 2 - FN.tooltips.config.dimensions.pointerWidthPixels / 2 - FN.tooltips.config.dimensions.pointerHorizontalInsetPixels; 1491 } else if (tooltipPointerPosition.x === 'right') { 1492 tooltipPosition.right = indicatorItemWidth / 2 - FN.tooltips.config.dimensions.pointerWidthPixels / 2 - FN.tooltips.config.dimensions.pointerHorizontalInsetPixels; 1493 } else { 1494 // 'center' and default 1495 tooltipPosition.left = indicatorItemWidth / 2 - tooltipWidth / 2; 1496 } 1497 1498 if (tooltipPointerPosition.y === 'top') { 1499 tooltipPosition.top = indicatorItemHeight + FN.tooltips.config.dimensions.pointerHeightPixels + FN.tooltips.config.dimensions.pointerYDistance; 1500 } else { 1501 // 'bottom' and default 1502 tooltipPosition.bottom = indicatorItemHeight + FN.tooltips.config.dimensions.pointerHeightPixels + FN.tooltips.config.dimensions.pointerYDistance; 1503 } 1504 1505 var cssAttr = {}; 1506 if ('left' in tooltipPosition) cssAttr['left'] = tooltipPosition.left + 'px'; 1507 if ('right' in tooltipPosition) cssAttr['right'] = tooltipPosition.right + 'px'; 1508 if ('top' in tooltipPosition) cssAttr['top'] = tooltipPosition.top + 'px'; 1509 if ('bottom' in tooltipPosition) cssAttr['bottom'] = tooltipPosition.bottom + 'px'; 1510 $('#' + FN.tooltips.config.selectors.tooltipId).css(cssAttr).show(); 1511 }, 1512 1513 /** 1514 Hide all tooltips 1515 */ 1516 hideTooltips: function hideTooltips() { 1517 $('#' + FN.tooltips.config.selectors.tooltipId).remove(); 1518 } 1519}; // load configured tooltips - but only once the page is completely loaded 1520 1521$(window).on('load', function () { 1522 FN.tooltips.init(); 1523}); 1524if (typeof FN == 'undefined') FN = {}; 1525FN.topBar = { 1526 active: false, 1527 visible: false, 1528 topThreshold: 0, 1529 selectors: { 1530 baseElementClass: 'top-bar', 1531 topThresholdElement: '.text-with-media:last-of-type' 1532 }, 1533 init: function init() { 1534 FN.topBar.active = true; 1535 FN.topBar.resetTopThreshold(); 1536 $(window).resize(function () { 1537 FN.topBar.resetTopThreshold(); 1538 }); 1539 $(window).scroll(function () { 1540 var scrollingPosition = $(window).scrollTop(); 1541 FN.topBar.toggle(scrollingPosition > FN.topBar.topThreshold); 1542 }); 1543 }, 1544 resetTopThreshold: function resetTopThreshold() { 1545 if ($(FN.topBar.selectors.topThresholdElement).length < 1) return; 1546 FN.topBar.topThreshold = $(FN.topBar.selectors.topThresholdElement).position().top; 1547 }, 1548 toggle: function toggle(show) { 1549 if (FN.topBar.active === false) { 1550 return; 1551 } 1552 1553 if (show === FN.topBar.visible) { 1554 return; 1555 } 1556
1557 var newTopBarPos = show ? '0px' : '-10em'; 1558 $('.' + FN.topBar.selectors.baseElementClass).stop().animate({ 1559 top: newTopBarPos 1560 }); 1561 FN.topBar.visible = show; 1562 } 1563}; 1564if (typeof FN == 'undefined') FN = {}; 1565FN.videoAutoplayerConfig = {}; 1566FN.videoAutoplayer = { 1567 _observer: null, 1568 init: function init() { 1569 if ('IntersectionObserver' in window) { 1570 FN.videoAutoplayer._observer = new IntersectionObserver(FN.videoAutoplayer._onIntersection, { 1571 threshold: 0.4 1572 }); 1573 FN.videoAutoplayer.observeAll(); 1574 } else { 1575 // fallback for browsers without IntersectionObserver 1576 FN.videoAutoplayer.playOrPauseAllVideos(); 1577 $(window).on('scroll', FN.videoAutoplayer.playOrPauseAllVideos); 1578 } 1579 }, 1580 1581 /******************** 1582 * (Re-)observe all videos currently in the DOM. 1583 * Call after any dynamic DOM change that adds or replaces <video> elements. 1584 */ 1585 observeAll: function observeAll() { 1586 if (!FN.videoAutoplayer._observer) return; 1587 1588 FN.videoAutoplayer._observer.disconnect(); 1589 1590 $('video').each(function (i, video) { 1591 FN.videoAutoplayer._observer.observe(video); 1592 }); 1593 }, 1594 _onIntersection: function _onIntersection(entries) { 1595 entries.forEach(function (entry) { 1596 if (entry.isIntersecting) { 1597 // guard against non-active rotator slides (opacity:0 in CSS) 1598 if (FN.videoAutoplayer.isVideoVisible(entry.target)) { 1599 FN.videoAutoplayer.startPlayback(entry.target); 1600 } 1601 } else { 1602 FN.videoAutoplayer.stopPlayback(entry.target); 1603 } 1604 }); 1605 }, 1606 1607 /******************** 1608 * Iterate through all videos on the page and 1609 * play or pause them depending on their visibility. 1610 * Used as the scroll-based fallback path. 1611 */ 1612 playOrPauseAllVideos: function playOrPauseAllVideos() { 1613 $('video').each(function (i, item) { 1614 var isVideoVisible = FN.videoAutoplayer.isVideoVisible(item); 1615 FN.videoAutoplayer.playOrPause(item, isVideoVisible); 1616 }); 1617 }, 1618 1619 /******************** 1620 * Returns true if the given video item is both 1621 * visible in the viewport and visible through CSS 1622 */ 1623 isVideoVisible: function isVideoVisible(item) { 1624 // is the video currently visible on the page / viewport? 1625 var visibleInViewport = FN.helpers.isVisibleInViewport({ 1626 element: item, 1627 minimumVisiblePercent: 40 1628 }); // is the video currently visible by CSS properties (note that we have to check the _parent_ element) 1629 1630 var visibleInCSS = $(item).parent().css(['opacity', 'visibility', 'display']); 1631 1632 if (visibleInViewport && visibleInCSS.opacity > 0 && visibleInCSS.visibility != 'hidden' && visibleInCSS.display != 'none') { 1633 return true; 1634 } else { 1635 return false; 1636 } 1637 }, 1638 playOrPause: function playOrPause(item, isVideoVisible) { 1639 if (isVideoVisible) { 1640 FN.videoAutoplayer.startPlayback(item); 1641 } else { 1642 FN.videoAutoplayer.stopPlayback(item); 1643 } 1644 }, 1645 startPlayback: function startPlayback(videoElement) { 1646 var videoDomElement = $(videoElement).get(0); // with preload="none", trigger a load before playing so sources are resolved 1647 1648 if (videoDomElement.preload === 'none' && videoDomElement.readyState === 0) { 1649 videoDomElement.load(); 1650 } 1651 1652 var isPlaying = videoDomElement.currentTime > 0 && !videoDomElement.paused && !videoDomElement.ended && videoDomElement.readyState > 2; 1653 1654 if (!isPlaying) { 1655 var playPromise = videoDomElement.play(); 1656 1657 if (playPromise !== undefined) { 1658 playPromise.then(function () {// Autoplay started! 1659 })["catch"](function (error) { 1660 // Only show controls if autoplay was explicitly blocked by the browser. 1661 // AbortError is a transient interruption (e.g. load/play race), not a user-facing issue. 1662 if (error.name === 'NotAllowedError') { 1663 $(videoDomElement).prop('controls', true); 1664 } 1665 }); 1666 } 1667 1668 return true; 1669 } 1670 }, 1671 stopPlayback: function stopPlayback(videoElement) { 1672 var videoDomElement = $(videoElement).get(0); 1673 var isPlaying = videoDomElement.currentTime > 0 && !videoDomElement.paused && !videoDomElement.ended && videoDomElement.readyState > 2; 1674 1675 if (isPlaying) { 1676 videoDomElement.pause(); 1677 return true; 1678 } 1679 }, 1680 rewindToStart: function rewindToStart(videoElement) { 1681 var videoDomElement = $(videoElement).get(0); 1682 videoDomElement.currentTime = 0; 1683 return true; 1684 } 1685}; 1686/******************** 1687* --USAGE-- 1688* Typically on pageReady, you'll simply want to call "init()" on the videoAutoplayer as follows: 1689*-------------------- 1690 1691$(document).ready(function(){ 1692 FN.videoAutoplayer.init(); 1693}); 1694 1695**********************/ 1696if (typeof FN === 'undefined') { 1697 var FN = {}; 1698} 1699 1700FN.videoPlayer = { 1701 videos: [{ 1702 'name': 'tower-mac', 1703 'videoId': 'JRSxr-PU0wk', 1704 'ratio': 1.7777, 1705 'width': 1280, 1706 'isPlaying': false 1707 }, { 1708 'name': 'tower-windows',
1709 'videoId': '9rqsPFdW0gc', 1710 'ratio': 1.7777, 1711 'width': 1280, 1712 'isPlaying': false 1713 }], 1714 playerMargin: 70, 1715 selectors: { 1716 'overlayWrapperClass': 'overlay__container--screencast', 1717 'playerWrapperClass': 'screencast__wrapper', 1718 'playerID': 'screencast__player', 1719 'closeButtonClass': 'overlay__button--close' 1720 }, 1721 player: false, 1722 init: function init() { 1723 $.getScript("https://www.youtube.com/iframe_api"); 1724 $('.button--play-screencast').on('click', function (event) { 1725 FN.videoPlayer.play('tower-mac', event); 1726 }); 1727 $('.visible-on--windows .button-big--screencast').on('click', function (event) { 1728 FN.videoPlayer.play('tower-windows', event); 1729 }); 1730 $('.' + FN.videoPlayer.selectors.overlayWrapperClass + ' .' + FN.videoPlayer.selectors.closeButtonClass).on('click', FN.videoPlayer.close); 1731 }, 1732 play: function play(videoName, event) { 1733 event.preventDefault(); 1734 1735 FN.videoPlayer._setActiveVideo(videoName); 1736 1737 FN.videoPlayer._showPlayer(); 1738 1739 FN.videoPlayer._initVideo(); 1740 }, 1741 close: function close() { 1742 $('#' + FN.videoPlayer.selectors.playerID).html(''); 1743 $('.' + FN.videoPlayer.selectors.overlayWrapperClass).fadeOut(0.1); 1744 $(document).off('keyup.screencast-closer'); 1745 FN.videoPlayer.player.stopVideo(); 1746 FN.videoPlayer.player.clearVideo(); 1747 $('#' + FN.videoPlayer.selectors.playerID).remove(); 1748 return false; 1749 }, 1750 _showPlayer: function _showPlayer() { 1751 var activeVideo = FN.videoPlayer._getActiveVideo(); 1752 1753 var width = $(window).width() > activeVideo.width ? activeVideo.width : $(window).width() - 2 * FN.videoPlayer.playerMargin; 1754 var height = width / activeVideo.ratio; 1755 1756 if (height > $(window).height() - 2 * FN.videoPlayer.playerMargin) { 1757 height = $(window).height() - 2 * FN.videoPlayer.playerMargin; 1758 width = height * activeVideo.ratio; 1759 } 1760 1761 $('.' + FN.videoPlayer.selectors.playerWrapperClass).append('<div id="screencast__player"></div>'); 1762 $('#' + FN.videoPlayer.selectors.playerID).css({ 1763 'width': width, 1764 'height': height 1765 }); 1766 $('.' + FN.videoPlayer.selectors.overlayWrapperClass).addClass('visible').fadeIn(0.1); 1767 $(document).on('keyup.screencast-closer', function (e) { 1768 if (e.keyCode === 27) { 1769 FN.videoPlayer.close(); 1770 } 1771 }); 1772 }, 1773 _setActiveVideo: function _setActiveVideo(videoName) { 1774 for (var i = 0; i < FN.videoPlayer.videos.length; i++) { 1775 if (videoName === FN.videoPlayer.videos[i].name) { 1776 FN.videoPlayer.videos[i].isPlaying = true; 1777 } else { 1778 FN.videoPlayer.videos[i].isPlaying = false; 1779 } 1780 } 1781 }, 1782 _getActiveVideo: function _getActiveVideo() { 1783 for (var i = 0; i < FN.videoPlayer.videos.length; i++) { 1784 if (FN.videoPlayer.videos[i].isPlaying == true) { 1785 return FN.videoPlayer.videos[i]; 1786 } 1787 } 1788 1789 return false; 1790 }, 1791 _initVideo: function _initVideo() { 1792 var activeVideo = FN.videoPlayer._getActiveVideo(); 1793 1794 FN.videoPlayer.player = new YT.Player(FN.videoPlayer.selectors.playerID, { 1795 videoId: activeVideo.videoId, 1796 playerVars: { 1797 autohide: 1, 1798 // Values: 2 (default), 1, and 0. This parameter indicates whether the video controls will automatically hide after a video begins playing 1799 autoplay: 1, 1800 // Values: 0 or 1. Default is 0. Sets whether or not the initial video will autoplay when the player loads. 1801 controls: 1, 1802 // Values: 0 (do not display), 1, or 2. Default is 1. This parameter indicates whether the video player controls will display 1803 enablejsapi: 0, 1804 // Values: 0 or 1. Default is 0. Setting this to 1 will enable the Javascript API 1805 fs: 1, 1806 // Values: 0 or 1. The default value is 1, which causes the fullscreen button to display. Setting this parameter to 0 prevents the fullscreen button from displaying 1807 hl: 'en', 1808 // Sets the player's interface language 1809 iv_load_policy: 3, 1810 // Values: 1 or 3. Default is 1. Setting to 1 will cause video annotations to be shown by default, whereas setting to 3 will cause video annotations to not be shown by default 1811 modestbranding: 1, 1812 // This parameter lets you use a YouTube player that does not show a YouTube logo. Set the parameter value to 1 to prevent the YouTube logo from displaying in the control bar 1813 origin: '', 1814 // This parameter provides an extra security measure for the IFrame API and is only supported for IFrame embeds. If you are using the IFrame API, which means you are setting the enablejsapi parameter value to 1, you should always specify your domain as the origin parameter value 1815 playsinline: 0, 1816 // This parameter controls whether videos play inline or fullscreen in an HTML5 player on iOS 1817 rel: 0, 1818 // Values: 0 or 1. Default is 1. This parameter indicates whether the player should show related videos when playback of the initial video ends 1819 showinfo: 0, 1820 // Values: 0 or 1. The parameter's default value is 1. If you set the parameter value to 0, then the player will not display information like the video title and uploader before the video starts playing 1821 theme: 'dark' // This parameter indicates whether the embedded player will display player controls (like a play button or volume control) within a dark or light control bar. Valid parameter values are dark and light 1822 // full reference: https://developers.google.com/youtube/player_parameters.html?playerVersion=HTML5 1823 1824 }, 1825 events: { 1826 'onReady': function onReady(event) { 1827 event.target.playVideo(); 1828 } 1829 } 1830 }); 1831 } 1832}; 1833/****************** 1834* 1835* Allows to mark content that should be displayed only on specific environments. 1836* 1837* HTML content can be marked using the following CSS classes: 1838* - .visible-on--mobile 1839* - .visible-on--desktop 1840* - .visible-on--mac 1841* - .visible-on--windows 1842* 1843* This way, content can be written/included for multiple platforms; 1844* on page load, any content that doesn't match the actual platform of the user will be hidden. 1845*/ 1846if (typeof FN == 'undefined') FN = {}; 1847FN.visibleOn = { 1848 defaultOS: 'mac', 1849 currentOS: false, 1850 otherOS: false, 1851 isMobile: false, 1852 mobileBreakpoint: '760px', 1853 selectors: '.visible-on--mobile, .visible-on--desktop, .visible-on--mac, .visible-on--windows', 1854 toggleContent: function toggleContent() { 1855 // determine current OS and device width 1856 var currentOS = FN.visibleOn.defaultOS; 1857 1858 if ('osSwitch' in FN) { 1859 FN.visibleOn.currentOS = FN.osSwitch.detectPlatform(); 1860 } else { 1861 FN.visibleOn.currentOS = FN.helpers.platform(); 1862 } 1863 1864 FN.visibleOn.otherOS = FN.visibleOn.currentOS == 'mac' ? 'windows' : 'mac'; 1865 FN.visibleOn.isMobile = window.matchMedia('(min-width: ' + FN.visibleOn.mobileBreakpoint + ')').matches ? false : true; // hiding criteria 1866 1867 var hideOnOS = 'visible-on--' + FN.visibleOn.otherOS; 1868 var hideOnDevice = FN.visibleOn.isMobile ? 'visible-on--desktop' : 'visible-on--mobile'; // iterate through all items with at least one of the corresponding CSS selectors 1869 1870 $(FN.visibleOn.selectors).each(function (index, element) { 1871 var elementClass = $(element).attr('class'); // if at least one hiding criteria is met, the element must be hidden (independent of other criteria) 1872 1873 if (elementClass.indexOf(hideOnOS) > -1 || elementClass.indexOf(hideOnDevice) > -1) { 1874 $(element).hide(); 1875 } else { 1876 $(element).show(); 1877 } 1878 }); // add or update class on body 1879 1880 document.body.classList.remove('current-os-' + FN.visibleOn.otherOS); 1881 document.body.classList.add('current-os-' + FN.visibleOn.currentOS); 1882 } 1883}; 1884$(document).ready(function () { 1885 FN.visibleOn.toggleContent(); 1886}); 1887if (typeof FN == 'undefined') FN = {}; 1888FN.osSwitch = { 1889 defaultPlatform: 'mac', 1890 currentPlatform: false, 1891 otherPlatform: false,
1892 init: function init() { 1893 var platform = FN.osSwitch.detectPlatform(); 1894 1895 FN.osSwitch._setPlatform(platform); // order os-switch buttons (current platform appears first) 1896 1897 1898 FN.osSwitch.orderButtons(); // attach event handler to os-switch buttons 1899 1900 $(document).on('click', '.os-switch', function () { 1901 FN.osSwitch.switchPlatform(this); 1902 }); 1903 }, 1904 toggleContent: function toggleContent() { 1905 $('.visible-on--' + FN.osSwitch.otherPlatform).hide(); 1906 $('.visible-on--' + FN.osSwitch.currentPlatform).show(); // add or update class on body 1907 1908 document.body.classList.remove('current-os-' + FN.osSwitch.otherPlatform); 1909 document.body.classList.add('current-os-' + FN.osSwitch.currentPlatform); 1910 }, 1911 detectPlatform: function detectPlatform(forcePlatform) { 1912 var platform = false; 1913 1914 var platformFromCookie = FN.osSwitch._getPersistedPlatformChoice(); 1915 1916 var platformFromBrowser = FN.helpers.platform(); 1917 1918 if (forcePlatform) { 1919 // set the platform to a given value (without looking at the user's machine) 1920 platform = forcePlatform; 1921 } else if (platformFromCookie) { 1922 // if a Cookie from a previous visit is set, use this value 1923 platform = platformFromCookie; 1924 } else if (platformFromBrowser !== 'other') { 1925 // set it by looking at the user's browser / machine 1926 platform = platformFromBrowser; 1927 } else { 1928 // use a default 1929 platform = FN.osSwitch.defaultPlatform; 1930 } 1931 1932 return platform; 1933 }, 1934 switchPlatform: function switchPlatform(clickedSwitch) { 1935 var chosenPlatform = $(clickedSwitch).hasClass('os-switch--mac') ? 'mac' : 'windows'; 1936 1937 FN.osSwitch._setPlatform(chosenPlatform); 1938 1939 $(clickedSwitch).siblings().removeClass('selected').addClass('selectable'); 1940 $(clickedSwitch).addClass('selected').removeClass('selectable'); 1941 FN.osSwitch.toggleContent(); // trigger a custom jQuery event named "FN.osSwitch:switched", which other code can listen to: 1942 // $(document).on("FN.osSwitch:switched", function(event, context){ ... 1943 1944 $(document).trigger("FN.osSwitch:switched", { 1945 'newCurrentPlatform': chosenPlatform 1946 }); 1947 }, 1948 orderButtons: function orderButtons() { 1949 // only order buttons when 'both' OS are being displayed 1950 $('.os-selection--both').each(function (index, element) { 1951 var otherPlatformButton = $(element).find('.os-switch--' + FN.osSwitch.otherPlatform); 1952 var currentPlatformButton = $(element).find('.os-switch--' + FN.osSwitch.currentPlatform); 1953 var connectingWord = $(element).find('.os-switch__connector'); 1954 var prefixText = $(element).find('.os-switch__prefix'); 1955 $(currentPlatformButton).addClass('selected').removeClass('selectable'); 1956 $(otherPlatformButton).addClass('selectable').removeClass('selected'); 1957 $(element).html('').append(prefixText).append(currentPlatformButton).append(connectingWord).append(otherPlatformButton); 1958 }); 1959 }, 1960 _setPlatform: function _setPlatform(platform) { 1961 FN.osSwitch.currentPlatform = platform; 1962 FN.osSwitch.otherPlatform = platform === 'mac' ? 'windows' : 'mac'; 1963 1964 FN.osSwitch._persistPlatformChoice(); 1965 }, 1966 _persistPlatformChoice: function _persistPlatformChoice() { 1967 Cookies.set('platform_choice', FN.osSwitch.currentPlatform); 1968 }, 1969 _getPersistedPlatformChoice: function _getPersistedPlatformChoice() { 1970 if (Cookies.get('platform_choice')) { 1971 return Cookies.get('platform_choice'); 1972 } else { 1973 return false; 1974 } 1975 } 1976}; 1977if (typeof FN == 'undefined') FN = {}; 1978FN.architectureDetector = { 1979 cache: null, 1980 1981 /** 1982 * Detect the user's processor architecture 1983 * @returns {Promise<string>} Returns 'arm', 'x86', or 'unknown' 1984 */ 1985 detect: function detect() { 1986 // Return cached result if available 1987 if (FN.architectureDetector.cache !== null) { 1988 return Promise.resolve(FN.architectureDetector.cache); 1989 } // Try modern API first (Chrome/Edge 90+) 1990 1991 1992 return FN.architectureDetector.detectModern().then(function (architecture) { 1993 // If modern method failed, try fallback 1994 if (architecture === 'unknown') { 1995 architecture = FN.architectureDetector.detectFromUserAgent(); 1996 } // Normalize and cache the result 1997 1998 1999 architecture = FN.architectureDetector.normalizeArchitecture(architecture); 2000 FN.architectureDetector.cache = architecture; 2001 return architecture; 2002 })["catch"](function (error) { 2003 console.warn('Architecture detection failed, using fallback:', error); // Use fallback method 2004 2005 var architecture = FN.architectureDetector.detectFromUserAgent(); 2006 architecture = FN.architectureDetector.normalizeArchitecture(architecture); 2007 FN.architectureDetector.cache = architecture; 2008 return architecture; 2009 }); 2010 }, 2011 2012 /** 2013 * Detect architecture using the modern navigator.userAgentData API 2014 * @returns {Promise<string>} Architecture string or 'unknown' 2015 */ 2016 detectModern: function detectModern() { 2017 if (!('userAgentData' in navigator) || !navigator.userAgentData.getHighEntropyValues) { 2018 return Promise.resolve('unknown'); 2019 } 2020 2021 return navigator.userAgentData.getHighEntropyValues(['architecture']).then(function (uaData) { 2022 return uaData.architecture || 'unknown'; 2023 })["catch"](function (error) { 2024 console.warn('Modern architecture detection failed:', error); 2025 return 'unknown'; 2026 }); 2027 }, 2028 2029 /** 2030 * Detect architecture from user agent string (fallback method) 2031 * @returns {string} Architecture string or 'unknown' 2032 */ 2033 detectFromUserAgent: function detectFromUserAgent() { 2034 var userAgent = navigator.userAgent.toLowerCase(); // Check for ARM indicators 2035 2036 if (userAgent.indexOf('arm') !== -1 || userAgent.indexOf('aarch64') !== -1 || userAgent.indexOf('apple silicon') !== -1) { 2037 return 'arm'; 2038 } // Check for x86/x64 indicators 2039 2040 2041 if (userAgent.indexOf('x86') !== -1 || userAgent.indexOf('x64') !== -1 || userAgent.indexOf('amd64') !== -1 || userAgent.indexOf('intel') !== -1) { 2042 return 'x86'; 2043 } // Additional checks for specific platforms 2044 2045 2046 if (userAgent.indexOf('mac') !== -1 && userAgent.indexOf('intel') !== -1) { 2047 return 'x86'; 2048 } 2049 2050 return 'unknown'; 2051 }, 2052 2053 /** 2054 * Normalize architecture string to standard values 2055 * @param {string} arch - Raw architecture string 2056 * @returns {string} Normalized architecture: 'arm', 'x86', or 'unknown' 2057 */ 2058 normalizeArchitecture: function normalizeArchitecture(arch) { 2059 if (!arch || arch === 'unknown') { 2060 return 'unknown'; 2061 } 2062 2063 var normalized = arch.toLowerCase(); // ARM variants 2064 2065 if (normalized.indexOf('arm') !== -1 || normalized.indexOf('aarch64') !== -1 || normalized.indexOf('arm64') !== -1) { 2066 return 'arm'; 2067 } // x86 variants 2068 2069 2070 if (normalized.indexOf('x86') !== -1 || normalized.indexOf('x64') !== -1 || normalized.indexOf('amd64') !== -1 || normalized.indexOf('i386') !== -1 || normalized.indexOf('i686') !== -1) { 2071 return 'x86'; 2072 } 2073 2074 return normalized; 2075 }, 2076 2077 /** 2078 * Check if the current architecture is ARM 2079 * @returns {Promise<boolean>} True if ARM architecture 2080 */ 2081 isARM: function isARM() { 2082 return FN.architectureDetector.detect().then(function (arch) { 2083 return arch === 'arm'; 2084 }); 2085 }, 2086 2087 /** 2088 * Check if the current architecture is x86 2089 * @returns {Promise<boolean>} True if x86 architecture 2090 */ 2091 isx86: function isx86() { 2092 return FN.architectureDetector.detect().then(function (arch) { 2093 return arch === 'x86'; 2094 }); 2095 }, 2096 2097 /** 2098 * Clear the cached architecture detection result 2099 */ 2100 clearCache: function clearCache() { 2101 FN.architectureDetector.cache = null; 2102 } 2103}; 2104if (typeof FN == 'undefined') FN = {}; 2105FN.downloadLink = {
2106 selector: '.download-link, .button--start-trial', 2107 init: function init() { 2108 $(window).on('click', FN.downloadLink.selector, function (event) { 2109 event.preventDefault(); 2110 var clickedElement = $(event.target); 2111 FN.downloadLink.startDownload(clickedElement); 2112 }); 2113 }, 2114 startDownload: function startDownload(clickedElement) { 2115 if (!$(clickedElement).is('a')) return; 2116 var url = $(clickedElement).attr('href'); // Check if this is a Windows download and if we need to add ARM architecture parameter 2117 2118 FN.downloadLink.processDownloadURL(url).then(function (processedUrl) { 2119 var iFrame = '<iframe style="display: none !important" src="' + processedUrl + '">'; 2120 $(iFrame).appendTo('body'); 2121 })["catch"](function (error) { 2122 console.warn('Download URL processing failed, using original URL:', error); 2123 var iFrame = '<iframe style="display: none !important" src="' + url + '">'; 2124 $(iFrame).appendTo('body'); 2125 }); 2126 }, 2127 2128 /** 2129 * Process download URL to add architecture parameters if needed 2130 * @param {string} url - Original download URL 2131 * @returns {Promise<string>} - Processed URL with architecture parameter if applicable 2132 */ 2133 processDownloadURL: function processDownloadURL(url) { 2134 // Check if this is a Windows download URL 2135 if (!FN.downloadLink.isWindowsDownloadURL(url)) { 2136 return Promise.resolve(url); 2137 } // Check if architecture detector is available 2138 2139 2140 if (!FN.architectureDetector || typeof FN.architectureDetector.detect !== 'function') { 2141 console.warn('Architecture detector not available, proceeding with standard download'); 2142 return Promise.resolve(url); 2143 } 2144 2145 return FN.architectureDetector.detect().then(function (architecture) { 2146 // Only add parameter for ARM architecture 2147 if (architecture === 'arm') { 2148 return FN.downloadLink.addArchParameter(url, 'arm64'); 2149 } 2150 2151 return url; 2152 })["catch"](function (error) { 2153 console.warn('Failed to detect architecture:', error); 2154 return url; 2155 }); 2156 }, 2157 2158 /** 2159 * Check if URL is a Windows download URL 2160 * @param {string} url - URL to check 2161 * @returns {boolean} - True if this is a Windows download URL 2162 */ 2163 isWindowsDownloadURL: function isWindowsDownloadURL(url) { 2164 return url.indexOf('/download/windows') !== -1 || url.indexOf('tower3-win') !== -1 || url.indexOf('windows') !== -1; 2165 }, 2166 2167 /** 2168 * Add architecture parameter to download URL 2169 * @param {string} url - Original URL 2170 * @param {string} arch - Architecture parameter value (e.g., 'arm64') 2171 * @returns {string} - URL with architecture parameter added 2172 */ 2173 addArchParameter: function addArchParameter(url, arch) { 2174 try { 2175 // Try modern URL API first 2176 if (typeof URL !== 'undefined' && URL.prototype.searchParams) { 2177 var urlObj = new URL(url, window.location.origin); 2178 urlObj.searchParams.set('arch', arch); 2179 return urlObj.toString(); 2180 } 2181 } catch (error) {// Fall through to fallback method 2182 } // Fallback for browsers that don't support URL constructor 2183 2184 2185 var separator = url.indexOf('?') !== -1 ? '&' : '?'; 2186 return url + separator + 'arch=' + encodeURIComponent(arch); 2187 }, 2188 2189 /** 2190 * Debug function to log architecture detection results 2191 * @returns {Promise<void>} 2192 */ 2193 debugArchitecture: function debugArchitecture() { 2194 if (!FN.architectureDetector) { 2195 console.log('Architecture detector not available'); 2196 return Promise.resolve(); 2197 } 2198 2199 return FN.architectureDetector.detect().then(function (architecture) { 2200 console.log('Detected architecture:', architecture); 2201 console.log('Is ARM:', architecture === 'arm'); 2202 console.log('Is x86:', architecture === 'x86'); 2203 })["catch"](function (error) { 2204 console.error('Architecture detection failed:', error); 2205 }); 2206 } 2207}; 2208$(document).on('ready', function () { 2209 FN.downloadLink.init(); 2210}); 2211if (typeof FN == 'undefined') FN = {}; 2212FN.announcementBar = { 2213 config: { 2214 selectors: { 2215 'wrapperClass': 'announcement-bar' 2216 } 2217 }, 2218 init: function init() { 2219 var announcementBarEl = $('.' + FN.announcementBar.config.selectors.wrapperClass); 2220 if (announcementBarEl.length != 1) return; 2221 $(announcementBarEl).find('.inner-container').addClass('visible'); 2222 } 2223}; 2224$(window).on('load', FN.announcementBar.init); 2225if (typeof FN == 'undefined') FN = {}; 2226FN.couponParam = { 2227 config: { 2228 urlParamName: 'coupon' 2229 },
2230 init: function init() { 2231 var couponParamValue = FN.helpers.getURLParameterByName(FN.couponParam.config.urlParamName); 2232 2233 if (couponParamValue) { 2234 Cookies.set(FN.couponParam.config.urlParamName, couponParamValue, { 2235 expires: 7 2236 }); 2237 } 2238 } 2239}; 2240$(window).on('load', FN.couponParam.init); 2241if (typeof FN == 'undefined') FN = {}; 2242FN.campaignParam = { 2243 config: { 2244 urlParamName: 'campaign' 2245 }, 2246 init: function init() { 2247 var campaignParamValue = FN.helpers.getURLParameterByName(FN.campaignParam.config.urlParamName); 2248 2249 if (campaignParamValue) { 2250 Cookies.set(FN.campaignParam.config.urlParamName, campaignParamValue, { 2251 expires: 30, 2252 path: '/', 2253 sameSite: 'Lax' 2254 }); 2255 } 2256 } 2257}; 2258$(window).on('load', FN.campaignParam.init); 2259document.addEventListener('DOMContentLoaded', function () { 2260 var nav = document.querySelector('.nav-top-common'); 2261 var showLink = document.querySelector('.nav-top-show'); 2262 var primaryLinks = Array.from(document.querySelectorAll('.nav-top-primary > li > a')); 2263 showLink.addEventListener('click', function () { 2264 nav.classList.toggle('active'); 2265 var isExpanded = nav.classList.contains('active'); 2266 showLink.setAttribute('aria-expanded', isExpanded ? 'true' : 'false'); 2267 showLink.setAttribute('aria-label', isExpanded ? 'Close navigation menu' : 'Open navigation menu'); 2268 }); 2269 primaryLinks.forEach(function (link) { 2270 var secodaryList = link.nextElementSibling; 2271 var hasSecondaryList = secodaryList && secodaryList.classList.contains('nav-top-secondary'); 2272 if (!hasSecondaryList) return; 2273 link.parentElement.addEventListener('mouseover', function () { 2274 link.dataset.overAt = Date.now(); 2275 link.setAttribute('aria-expanded', 'true'); 2276 }); 2277 link.parentElement.addEventListener('mouseleave', function () { 2278 link.setAttribute('aria-expanded', 'false'); 2279 }); 2280 link.addEventListener('click', function (event) { 2281 var isMobile = window.matchMedia('(max-width: 900px)').matches; 2282 if (isMobile) return; // Don't follow the link if it's tapped when the list is just opened. This 2283 // is required for touch interfaces which handle `mouseover` and `click` 2284 // together, therefore not allowing to view and select items on the 2285 // secondary list. 2286 2287 if (link.dataset.overAt && Date.now() - link.dataset.overAt < 100) event.preventDefault(); 2288 }); 2289 }); 2290 nav.towerLastScrollPosition = 0; 2291 nav.towerScrollOffset = 0; 2292 document.addEventListener('scroll', function () { 2293 var isOnMobile = window.matchMedia('(max-width: 900px)').matches; 2294 if (isOnMobile) return; // Always show nav when near the top of the page (within 30px) 2295 2296 if (window.scrollY <= 30) { 2297 nav.towerScrollOffset = 0; 2298 nav.style.top = '0px'; 2299 nav.style.transition = 'top 0.2s'; 2300 nav.towerLastScrollPosition = window.scrollY; 2301 return; 2302 } 2303 2304 var paralaxFactor = 0.8; 2305 var style = window.getComputedStyle(nav); 2306 var shadow = 20; // The size of the navigation pill's shadow 2307 2308 var limit = (parseFloat(style.height) + parseFloat(style.marginTop) + shadow) / paralaxFactor; 2309 var scrollThreshold = 70; // Only show nav after scrolling up this many pixels 2310 2311 var diff = nav.towerLastScrollPosition - window.scrollY; 2312 2313 if (diff < 0 && window.scrollY > 0) { 2314 // Scrolling down - hide nav 2315 var offset = Math.max(-limit, Math.min(0, nav.towerScrollOffset + diff)); 2316 nav.towerScrollOffset = offset; 2317 nav.style.top = "".concat(Math.round(offset * paralaxFactor), "px"); 2318 nav.style.transition = 'none'; 2319 } else if (diff > scrollThreshold) { 2320 // Scrolling up past threshold - show nav 2321 nav.towerScrollOffset = 0; 2322 nav.style.top = '0px'; 2323 nav.style.transition = 'top 0.2s'; 2324 } // If scrolling up but not past threshold, keep nav in current position 2325 2326 2327 nav.towerLastScrollPosition = window.scrollY; 2328 }); 2329 nav.addEventListener('mouseover', function () { 2330 nav.towerScrollOffset = 0; 2331 nav.style.top = '0px'; 2332 nav.style.transition = 'top 0.2s'; 2333 }); // Set order of primary items so that they can be reordered on mobile 2334 2335 var primaryItems = Array.from(document.querySelectorAll('.nav-top-primary > li'));
2336 primaryItems.forEach(function (item, index) { 2337 item.style.order = index; 2338 }); // Footer accordion 2339 2340 document.querySelectorAll('.footer-group__label').forEach(function (button) { 2341 button.addEventListener('click', function () { 2342 var isExpanded = button.getAttribute('aria-expanded') === 'true'; 2343 button.setAttribute('aria-expanded', isExpanded ? 'false' : 'true'); 2344 var list = document.getElementById(button.getAttribute('aria-controls')); 2345 list.classList.toggle('footer-group__list--collapsed', isExpanded); 2346 }); 2347 }); 2348}); 2349if (typeof FN == 'undefined') FN = {}; 2350 2351FN.contentTextWithMedia = function (itemConfig) { 2352 /**************** 2353 * Initialization 2354 */ 2355 this.__init = function (itemConfig) { 2356 this.platform = typeof FN.osSwitch == 'undefined' || !FN.osSwitch.currentPlatform ? FN.helpers.platform() : FN.osSwitch.currentPlatform; 2357 this.itemConfig = itemConfig; // build + insert HTML 2358 2359 this.__buildItemHtml(); 2360 2361 if ('navigation' in this.itemConfig && this.itemConfig['navigation'] === true) { 2362 this.__buildNavigationHtml(); 2363 } // find + save DOM elements 2364 2365 2366 this.baseElement = $('#' + this.itemConfig['id']); 2367 this.mediaItems = $(this.baseElement).find('.' + FN.contentTextWithMedia["static"].selectors.mediaItemClass); 2368 this.textItems = $(this.baseElement).find('.' + FN.contentTextWithMedia["static"].selectors.textItemClass); 2369 this.navigationItems = $(this.baseElement).find('.' + FN.contentTextWithMedia["static"].selectors.navigationItemClass); 2370 this.activeItemIndex = this.__findActiveItemIndex(); // set up event handlers 2371 2372 $(this.textItems).on('click', $.proxy(this.itemWasSelected, this)); 2373 $('video', this.baseElement).on('ended', $.proxy(this.videoFinishedPlaying, this)); 2374 $('video', this.baseElement).on('loadeddata', $.proxy(this.videoHasLoadedData, this)); 2375 2376 if ('navigation' in this.itemConfig && this.itemConfig['navigation'] === true) { 2377 $(this.navigationItems).on('click', $.proxy(this.itemWasSelected, this)); 2378 } // initialize with current selection 2379 2380 2381 this.__displayActiveItem(); 2382 }; 2383 /***************** 2384 * Build HTML structure of item 2385 */ 2386 2387 2388 this.__buildItemHtml = function () { 2389 var html = ''; 2390 2391 var contentConfig = this.__contentConfig(); 2392 2393 var containerClass = 'width' in this.itemConfig ? 'inner-container--' + this.itemConfig['width'] : 'inner-container--fullwidth'; 2394 html += '<div class="inner-container ' + containerClass + '">'; 2395 2396 for (var key in contentConfig) { 2397 if (key === 'text') { 2398 var textContent = contentConfig['text']; 2399 html += this.__buildItemHtmlForTextContent(textContent); 2400 } else if (key === 'media') { 2401 var mediaContent = contentConfig['media']; 2402 var mediaItems = mediaContent['items']; 2403 html += this.__buildItemHtmlForMediaContent(mediaContent, mediaItems); 2404 } 2405 } 2406 2407 html += '</div>'; 2408 $('#' + this.itemConfig['id']).html(html); 2409 }; 2410 2411 this.__buildItemHtmlForTextContent = function (textContent) { 2412 html = '<div class="text-with-media__text">'; 2413 html += ' <div class="text__container">' + FN.helpers.absoluteUrl(textContent) + '</div>'; 2414 html += '</div>'; 2415 return html; 2416 }; 2417 2418 this.__buildItemHtmlForMediaContent = function (mediaContent, mediaItems) { 2419 html = '<div class="text-with-media__media">'; 2420 html += ' <div class="media__container">'; // placeholder images 2421 2422 html += '<div class="media__placeholder">'; 2423 html += this.__buildItemHtmlForPlaceholder(); 2424 html += '</div>'; // media items 2425 2426 for (var m = 0; m < mediaItems.length; m++) { 2427 var mediaItem = mediaItems[m]; 2428 2429 var mediaItemSource = this.__bestItemSourceForCurrentScreen(mediaItem); 2430 2431 if (mediaItemSource['type'] === 'video') { 2432 html += this.__buildItemHtmlForVideo(mediaItemSource, mediaItem); 2433 } else if (mediaItemSource['type'] === 'picture') { 2434 html += this.__buildItemHtmlForPicture(mediaItemSource, mediaItem); 2435 } 2436 } 2437 2438 html += '</div><!-- END media__container-->'; 2439 html += '</div><!-- END media-->'; 2440 return html; 2441 }; 2442 2443 this.__buildItemHtmlForPlaceholder = function () { 2444 var placeholder = this.__placeholderForCurrentScreen(); 2445 2446 if (!placeholder) return ''; // The placeholder is a blank spacer that reserves the media box, so it is 2447 // decorative and deliberately carries an empty alt. 2448 2449 return '<img src="' + placeholder['url'] + '"' + ' width="' + placeholder['width'] + '"' + ' height="' + placeholder['height'] + '"' + ' alt="" />'; 2450 }; // Describes what the media shows, so a screenshot or the still that stands in 2451 // for a video is not an unlabelled image. Set per media item via `alt:` in the 2452 // content; use `alt: ''` for a purely decorative illustration. A media item 2453 // with no `alt:` at all gets no attribute, so it stays visible to an 2454 // accessibility audit instead of silently claiming to be decorative. 2455 2456 2457 this.__altAttribute = function (mediaItem) { 2458 if (!('alt' in mediaItem)) return ''; 2459 return ' alt="' + FN.helpers.escapeAttribute(mediaItem['alt']) + '"'; 2460 }; 2461 2462 this.__buildItemHtmlForVideo = function (mediaItemSource, mediaItem) { 2463 var itemClass = FN.contentTextWithMedia["static"].selectors.mediaItemClass; 2464 2465 var alt = this.__altAttribute(mediaItem); 2466 2467 html = '<div class="' + itemClass + '" data-source-media="' + mediaItemSource['media'] + '">'; 2468 html += '<video preload="none" playsinline="playsinline" muted="muted" fetchpriority="low" disableremoteplayback disablepictureinpicture class="loading">'; // main video source 2469 2470 if ('src' in mediaItemSource) { 2471 var mainSourceUrl = FN.helpers.absoluteUrl(mediaItemSource['src']); 2472 html += '<source src="' + mainSourceUrl + '" type="video/mp4">'; 2473 } // fallback sources 2474 2475 2476 if ('fallback' in mediaItemSource) { 2477 for (var f = 0; f < mediaItemSource['fallback'].length; f++) { 2478 var fallbackUrl = FN.helpers.absoluteUrl(mediaItemSource['fallback'][f]); 2479 2480 if (mediaItemSource['fallback'][f].match(".mp4$")) { 2481 html += '<source src="' + fallbackUrl + '" type="video/mp4">'; 2482 } else { 2483 switch (mediaItemSource.media) { 2484 case "l": 2485 html += '<img src="' + fallbackUrl + '" width="1500" height="1200" loading="lazy"' + alt + ' />'; 2486 break; 2487 2488 case "m": 2489 html += '<img src="' + fallbackUrl + '" width="2000" height="1200" loading="lazy"' + alt + ' />'; 2490 break; 2491 2492 default: 2493 html += '<img src="' + fallbackUrl + '" loading="lazy"' + alt + ' />'; 2494 } 2495 } 2496 } 2497 } 2498 2499 html += '</video>'; 2500 html += '</div>'; 2501 return html; 2502 }; 2503 2504 this.__buildItemHtmlForPicture = function (mediaItemSource, mediaItem) { 2505 var itemClass = FN.contentTextWithMedia["static"].selectors.mediaItemClass; 2506 var srcSet = 'srcset' in mediaItemSource ? FN.helpers.absoluteUrl(mediaItemSource['srcset']) : ''; 2507 var imgSrc = 'src' in mediaItemSource ? FN.helpers.absoluteUrl(mediaItemSource['src']) : ''; 2508 2509 var alt = this.__altAttribute(mediaItem); 2510 2511 html = '<div class="' + itemClass + '" data-source-media="' + mediaItemSource['media'] + '">'; 2512 html += '<picture>'; 2513 html += ' <source srcset="' + srcSet + '" />'; 2514 html += ' <img src="' + imgSrc + '"' + alt + ' />'; 2515 html += '</picture>'; 2516 html += '</div>'; 2517 return html; 2518 }; 2519 2520 this.__buildNavigationHtml = function () { 2521 var contentConfig = this.__contentConfig(); 2522 2523 var mediaContent = contentConfig['media']['items']; 2524 var html = '<div class="media__navigation">'; 2525 2526 for (var m = 0; m < mediaContent.length; m++) { 2527 html += '<a class="' + FN.contentTextWithMedia["static"].selectors.navigationItemClass + '"></a>'; 2528 } 2529 2530 html += '</div>'; 2531 $('#' + this.itemConfig['id'] + ' .media__container').append(html); 2532 }; 2533 2534 this.__contentConfig = function () { 2535 // find the corresponding content config - depending on the OS / platform and the fact if "content_all" is defined
2536 if ('content_all' in this.itemConfig) { 2537 return this.itemConfig['content_all']; 2538 } else if ('content_' + this.platform in this.itemConfig) { 2539 return this.itemConfig['content_' + this.platform]; 2540 } else { 2541 return false; 2542 } 2543 }; 2544 /***************** 2545 * Event handling: item was selected 2546 */ 2547 2548 2549 this.itemWasSelected = function (event) { 2550 // deactivate the automatic timer 2551 clearTimeout(this.activeItemRotationTimer); 2552 this.activeItemRotationTimer = false; 2553 this.itemConfig['autorotate'] = false; // mark active item 2554 2555 var newSelectionIndex = false; 2556 var selectedItem = event.currentTarget; 2557 var selectedItemClass = $(event.currentTarget).hasClass(FN.contentTextWithMedia["static"].selectors.navigationItemClass) ? FN.contentTextWithMedia["static"].selectors.navigationItemClass : FN.contentTextWithMedia["static"].selectors.textItemClass; 2558 var items = $(this.baseElement).find('.' + selectedItemClass); 2559 2560 for (var i = 0; i < items.length; i++) { 2561 if ($(items[i]).is(selectedItem)) { 2562 if (this.activeItemIndex === i) { 2563 // current highlight was re-selected 2564 return; 2565 } else { 2566 this.activeItemIndex = i; 2567 } 2568 2569 break; 2570 } 2571 } 2572 2573 this.__hideActiveItem(); 2574 2575 this.__displayActiveItem(); 2576 2577 event.preventDefault(); 2578 }; 2579 /***************** 2580 * Video finished playing 2581 */ 2582 2583 2584 this.videoFinishedPlaying = function (event) { 2585 if ('autorotate' in this.itemConfig && this.itemConfig['autorotate'] === true) { 2586 // show next item 2587 this.activateNextItem(); 2588 } else { 2589 // replay video 2590 var videoElement = event.target; 2591 FN.videoAutoplayer.startPlayback(videoElement); 2592 } 2593 }; 2594 /***************** 2595 * Video has loaded 2596 */ 2597 2598 2599 this.videoHasLoadedData = function (event) { 2600 // display the video 2601 var videoElement = event.target; 2602 $(videoElement).removeClass('loading').addClass('ready'); 2603 }; 2604 /***************** 2605 * Mark the "next" item as active 2606 */ 2607 2608 2609 this.activateNextItem = function () { 2610 this.__hideActiveItem(); 2611 2612 if (this.activeItemIndex + 1 < this.textItems.length) { 2613 this.activeItemIndex = this.activeItemIndex + 1; 2614 } else { 2615 this.activeItemIndex = 0; 2616 } 2617 2618 this.__displayActiveItem(); 2619 }; 2620 /***************** 2621 * Check if we need to re-init a media item because a different breakpoint size 2622 * would be more suitable (possibly the case after the window was resized) 2623 */ 2624 2625 2626 this.__checkMediaItemSizes = function (event) { 2627 var contentConfig = this.__contentConfig(); 2628 2629 if (!contentConfig) return; 2630 var mediaItemsConfig = contentConfig['media']['items']; 2631 var mediaItemConfig, mediaItemNode, currentItemSource, bestItemSourceWithCurrentScreenSize; 2632 2633 for (var i = 0; i < mediaItemsConfig.length; i++) { 2634 mediaItemConfig = mediaItemsConfig[i]; 2635 mediaItemNode = this.mediaItems[i]; 2636 currentItemSource = $(mediaItemNode).data('source-media'); 2637 bestItemSourceWithCurrentScreenSize = this.__bestItemSourceForCurrentScreen(mediaItemConfig); 2638 2639 if (currentItemSource && bestItemSourceWithCurrentScreenSize && currentItemSource != bestItemSourceWithCurrentScreenSize['media']) { 2640 // re-init, in order to replace with more appropriate source 2641 this.__init(this.itemConfig); 2642 2643 return; 2644 } 2645 } 2646 }; 2647 /***************** 2648 * Find the currently active item 2649 */ 2650 2651 2652 this.__findActiveItemIndex = function () { 2653 for (var i = 0; i < this.textItems.length; i++) { 2654 if ($(this.textItems[i]).hasClass(FN.contentTextWithMedia["static"].selectors.activeItemClass)) { 2655 return i; 2656 } 2657 } 2658 2659 return 0; 2660 }; 2661 /***************** 2662 * From the sources defined for this item, find the one that matches the current 2663 * screen / breakpoint best 2664 */ 2665 2666 2667 this.__bestItemSourceForCurrentScreen = function (mediaItem) { 2668 for (var s = 0; s < mediaItem['sources'].length; s++) { 2669 var breakpoint = FN.helpers.getResponsiveBreakpointByName(mediaItem['sources'][s]['media']); 2670 2671 if (window.matchMedia('(min-width: ' + breakpoint['width'] + ')').matches) { 2672 return mediaItem['sources'][s]; 2673 } 2674 } 2675 2676 return false;
2677 }; 2678 /***************** 2679 * Determines the correct placeholder for the current breakpoint and returns its 2680 * URL together with the dimensions needed to reserve its box. 2681 */ 2682 2683 2684 this.__placeholderForCurrentScreen = function () { 2685 var currentBreakpoint = this.__findMatchingMediaBreakpoint(); 2686 2687 var placeholderSize; 2688 2689 for (var p = 0; p < FN.contentTextWithMedia["static"].placeholderSizes.length; p++) { 2690 if (FN.contentTextWithMedia["static"].placeholderSizes[p]['media'] == currentBreakpoint['media']) { 2691 placeholderSize = FN.contentTextWithMedia["static"].placeholderSizes[p]; 2692 break; 2693 } 2694 } 2695 2696 if (!placeholderSize) return false; 2697 return { 2698 'url': FN.helpers.absoluteUrl('<base/>assets/img/screenshots/' + placeholderSize['image']), 2699 'width': placeholderSize['width'], 2700 'height': placeholderSize['height'] 2701 }; 2702 }; 2703 /***************** 2704 * Displays / marks the currently active item 2705 */ 2706 2707 2708 this.__displayActiveItem = function () { 2709 $(this.textItems[this.activeItemIndex]).addClass(FN.contentTextWithMedia["static"].selectors.activeItemClass); 2710 $(this.mediaItems[this.activeItemIndex]).addClass(FN.contentTextWithMedia["static"].selectors.activeItemClass); 2711 $(this.navigationItems[this.activeItemIndex]).addClass(FN.contentTextWithMedia["static"].selectors.activeItemClass); 2712 2713 var contentConfig = this.__contentConfig(); 2714 2715 if (!contentConfig || !('media' in contentConfig)) return; 2716 var activeMediaItem = contentConfig['media']['items'][this.activeItemIndex]; 2717 var videoElement = $(this.mediaItems[this.activeItemIndex]).find('video'); 2718 2719 if (videoElement.length == 1) { 2720 var visibleInViewport = FN.helpers.isVisibleInViewport({ 2721 element: videoElement, 2722 minimumVisiblePercent: 40 2723 }); 2724 if (visibleInViewport) FN.videoAutoplayer.startPlayback(videoElement); 2725 } 2726 2727 this.__startActiveItemRotation(); 2728 }; 2729 /***************** 2730 * Hides the currently active item before switching to the next (now 'active') item 2731 */ 2732 2733 2734 this.__hideActiveItem = function () { 2735 var contentConfig = this.__contentConfig(); 2736 2737 var activeMediaItem = contentConfig['media']['items'][this.activeItemIndex]; 2738 var videoElement = $(this.mediaItems[this.activeItemIndex]).find('video'); 2739 2740 if (videoElement.length == 1) { 2741 FN.videoAutoplayer.stopPlayback(videoElement); 2742 FN.videoAutoplayer.rewindToStart(videoElement); 2743 } 2744 2745 $(this.textItems).removeClass(FN.contentTextWithMedia["static"].selectors.activeItemClass); 2746 $(this.mediaItems).removeClass(FN.contentTextWithMedia["static"].selectors.activeItemClass); 2747 $(this.navigationItems).removeClass(FN.contentTextWithMedia["static"].selectors.activeItemClass); 2748 }; 2749 /***************** 2750 * Activate the timer to rotate active items 2751 */ 2752 2753 2754 this.__startActiveItemRotation = function () { 2755 // start autorotation timer (if configured) 2756 if (!'autorotate' in this.itemConfig || this.itemConfig['autorotate'] !== true) return; 2757 var instance = this; 2758 var rotationTiming = false; 2759 2760 var contentConfig = this.__contentConfig(); 2761 2762 var activeMediaItem = contentConfig['media']['items'][this.activeItemIndex]; 2763 var videoElement = $(this.mediaItems[this.activeItemIndex]).find('video'); 2764 2765 if (videoElement.length == 1) { 2766 // videos rotate through the "ended" event listener 2767 return; 2768 } else { 2769 rotationTiming = FN.contentTextWithMedia["static"].rotationTiming; 2770 } 2771 2772 this.activeItemRotationTimer = setTimeout(function () { 2773 instance.activateNextItem(); 2774 }, rotationTiming); 2775 }; 2776 /***************** 2777 * Find the responsive breakpoint that matches the current screen width 2778 */ 2779 2780 2781 this.__findMatchingMediaBreakpoint = function () { 2782 var currentBreakpoint = false; 2783 2784 for (var b = 0; b < FN.config.responsiveBreakpoints.length; b++) { 2785 if (window.matchMedia('(min-width: ' + FN.config.responsiveBreakpoints[b]['width'] + ')').matches) { 2786 currentBreakpoint = FN.config.responsiveBreakpoints[b]; 2787 break; 2788 } 2789 } 2790 2791 return currentBreakpoint; 2792 }; 2793 /***************** 2794 * Constructor 2795 */ 2796 2797 2798 this.__init(itemConfig); 2799}; 2800/************** 2801* Static Variables 2802*/ 2803 2804 2805FN.contentTextWithMedia["static"] = { 2806 itemInstances: [], 2807 rotationTiming: 4000, 2808 selectors: { 2809 itemWrapperClass: 'text-with-media', 2810 mediaItemClass: 'media__item', 2811 textItemClass: 'text__item', 2812 navigationItemClass: 'navigation__item',
2813 activeItemClass: 'active' 2814 }, 2815 // The placeholder is the only element of a media item that is in the document 2816 // flow — `.media__item` is absolutely positioned — so it alone determines the 2817 // height of the container. Its dimensions are carried here, and always 2818 // rendered, so the browser can reserve the right box before the image loads. 2819 placeholderSizes: [{ 2820 'media': 'xl', 2821 'image': '[email protected]', 2822 'width': 1500, 2823 'height': 1200 2824 }, { 2825 'media': 'l', 2826 'image': '[email protected]', 2827 'width': 1500, 2828 'height': 1200 2829 }, { 2830 'media': 'm', 2831 'image': '[email protected]', 2832 'width': 2000, 2833 'height': 1200 2834 }, { 2835 'media': 's', 2836 'image': '[email protected]', 2837 'width': 400, 2838 'height': 500 2839 }, { 2840 'media': 'xs', 2841 'image': '[email protected]', 2842 'width': 400, 2843 'height': 500 2844 }], 2845 initAllItems: function initAllItems() { 2846 if ('contentTextWithMediaItems' in FN) { 2847 for (var i = 0; i < FN.contentTextWithMediaItems.length; i++) { 2848 var itemConfig = FN.contentTextWithMediaItems[i]; 2849 FN.contentTextWithMedia["static"].itemInstances.push(new FN.contentTextWithMedia(itemConfig)); 2850 } 2851 } // register all videos with the autoplayer (no-op until videoAutoplayer.init() runs) 2852 2853 2854 if (typeof FN.videoAutoplayer != 'undefined') FN.videoAutoplayer.observeAll(); // replace testimonial placeholders 2855 2856 if (typeof FN.testimonials != 'undefined') FN.testimonials.init(); // react on window resize 2857 2858 $(window).on('resize', FN.contentTextWithMedia["static"].windowWasResized); 2859 }, 2860 windowWasResized: function windowWasResized(event) { 2861 for (var i = 0; i < FN.contentTextWithMedia["static"].itemInstances.length; i++) { 2862 var contentTextWithMediaItem = FN.contentTextWithMedia["static"].itemInstances[i]; 2863 2864 contentTextWithMediaItem.__checkMediaItemSizes(); 2865 } // re-observe after resize in case video elements were replaced for a new breakpoint 2866 2867 2868 if (typeof FN.videoAutoplayer != 'undefined') FN.videoAutoplayer.observeAll(); 2869 if (typeof FN.testimonials != 'undefined') FN.testimonials.init(); 2870 } 2871}; 2872if (typeof FN == 'undefined') FN = {}; 2873 2874FN.contentTextWithContent = function (itemConfig) { 2875 /**************** 2876 * Initialization 2877 */ 2878 this.__init = function (itemConfig) { 2879 this.platform = typeof FN.osSwitch == 'undefined' || !FN.osSwitch.currentPlatform ? FN.helpers.platform() : FN.osSwitch.currentPlatform; 2880 this.itemConfig = itemConfig; // build + insert HTML 2881 2882 this.__buildItemHtml(); // find + save DOM elements 2883 2884 2885 this.baseElement = $('#' + this.itemConfig['id']); // event handlers 2886 2887 $('video', this.baseElement).on('loadeddata', $.proxy(this.videoHasLoadedData, this)); 2888 }; 2889 /***************** 2890 * Build HTML structure of item 2891 */ 2892 2893 2894 this.__buildItemHtml = function () { 2895 var html = '<div class="inner-container inner-container--maxwidth">'; 2896 2897 var contentConfig = this.__contentConfig(); 2898 2899 for (var key in contentConfig) { 2900 if (key === 'text') { 2901 html += this.__buildItemHtmlForText(contentConfig['text']); 2902 } else if (key === 'content') { 2903 html += this.__buildItemHtmlForContent(contentConfig['content']); 2904 } 2905 } 2906 2907 html += '</div>'; 2908 $('#' + this.itemConfig['id']).html(html); 2909 }; 2910 2911 this.__buildItemHtmlForText = function (contentItem) { 2912 var html = '<div class="text-with-content__text">'; 2913 2914 if (contentItem['headline']) { 2915 html += '<h2 class="font--white">' + FN.helpers.absoluteUrl(contentItem['headline']) + '</h2>'; 2916 } 2917 2918 if (contentItem['subline']) { 2919 html += '<p>' + FN.helpers.absoluteUrl(contentItem['subline']) + '</p>'; 2920 } 2921 2922 html += '</div>'; 2923 return html; 2924 }; 2925 2926 this.__buildItemHtmlForContent = function (contentItem) { 2927 var contentItemSource = this.__bestItemSourceForCurrentScreen(contentItem); 2928 2929 var itemClass = 'text-with-content__content text-with-content__content--' + contentItemSource['type']; 2930 if (contentItemSource['class']) itemClass += ' text-with-content__content--' + contentItemSource['class']; 2931 var html = '<div class="' + itemClass + '" data-source-media="' + contentItemSource['media'] + '">'; 2932 2933 if (contentItemSource['type'] === 'video') { 2934 html += this.__buildItemHtmlForVideo(contentItemSource); 2935 } else if (contentItemSource['type'] === 'picture') { 2936 html += this.__buildItemHtmlForPicture(contentItemSource); 2937 } 2938 2939 html += '</div>'; 2940 return html; 2941 }; // Describes what the media shows, so a screenshot or the still that stands in 2942 // for a video is not an unlabelled image. Set via `alt:` on the source in the 2943 // content; use `alt: ''` for a purely decorative illustration. A source with 2944 // no `alt:` at all gets no attribute, so it stays visible to an accessibility 2945 // audit instead of silently claiming to be decorative. 2946 2947 2948 this.__altAttribute = function (contentItemSource) { 2949 if (!('alt' in contentItemSource)) return ''; 2950 return ' alt="' + FN.helpers.escapeAttribute(contentItemSource['alt']) + '"'; 2951 }; 2952 2953 this.__buildItemHtmlForVideo = function (contentItemSource) { 2954 var alt = this.__altAttribute(contentItemSource); 2955 2956 html = '<video preload="none" playsinline="playsinline" loop="loop" muted="muted" fetchpriority="low" disableremoteplayback disablepictureinpicture class="loading">'; // main video source 2957 2958 if ('src' in contentItemSource) { 2959 var mainSourceUrl = FN.helpers.absoluteUrl(contentItemSource['src']); 2960 html += '<source src="' + mainSourceUrl + '" type="video/mp4">'; 2961 } // fallback sources 2962 2963 2964 if ('fallback' in contentItemSource) { 2965 for (var f = 0; f < contentItemSource['fallback'].length; f++) { 2966 var fallbackUrl = FN.helpers.absoluteUrl(contentItemSource['fallback'][f]); 2967 2968 if (contentItemSource['fallback'][f].match(".mp4$")) { 2969 html += '<source src="' + fallbackUrl + '" type="video/mp4">'; 2970 } else { 2971 html += '<img src="' + fallbackUrl + '" loading="lazy"' + alt + ' />'; 2972 } 2973 } 2974 } 2975 2976 html += '</video>'; 2977 return html; 2978 }; 2979 2980 this.__buildItemHtmlForPicture = function (contentItemSource) { 2981 var srcSet = 'srcset' in contentItemSource ? FN.helpers.absoluteUrl(contentItemSource['srcset']) : ''; 2982 var imgSrc = 'src' in contentItemSource ? FN.helpers.absoluteUrl(contentItemSource['src']) : ''; 2983 2984 var alt = this.__altAttribute(contentItemSource); 2985 2986 html = '<picture>'; 2987 html += ' <source srcset="' + srcSet + '" />'; 2988 html += ' <img src="' + imgSrc + '"' + alt + ' />'; 2989 html += '</picture>'; 2990 return html; 2991 }; 2992 2993 this.__contentConfig = function () { 2994 // find the corresponding content config - depending on the OS / platform and the fact if "content_all" is defined
2995 if ('content_all' in this.itemConfig) { 2996 return this.itemConfig['content_all']; 2997 } else if ('content_' + this.platform in this.itemConfig) { 2998 return this.itemConfig['content_' + this.platform]; 2999 } else { 3000 return false; 3001 } 3002 }; 3003 /***************** 3004 * From the sources defined for this item, find the one that matches the current 3005 * screen / breakpoint best 3006 */ 3007 3008 3009 this.__bestItemSourceForCurrentScreen = function (contentItemMedia) { 3010 for (var s = 0; s < contentItemMedia.length; s++) { 3011 var breakpoint = FN.helpers.getResponsiveBreakpointByName(contentItemMedia[s]['media']); 3012 3013 if (window.matchMedia('(min-width: ' + breakpoint['width'] + ')').matches) { 3014 return contentItemMedia[s]; 3015 } 3016 } 3017 3018 return false; 3019 }; 3020 /***************** 3021 * Video has loaded 3022 */ 3023 3024 3025 this.videoHasLoadedData = function (event) { 3026 // display the video 3027 var videoElement = event.target; 3028 $(videoElement).removeClass('loading').addClass('ready'); 3029 }; 3030 /***************** 3031 * Check if we need to re-init a media item because a different breakpoint size 3032 * would be more suitable (possibly the case after the window was resized) 3033 */ 3034 3035 3036 this.__checkContentItemSizes = function (event) { 3037 var contentConfig = this.__contentConfig(); 3038 3039 if (!contentConfig) return false; 3040 var currentItemSource = $(this.baseElement).find('.' + FN.contentTextWithContent["static"].selectors.contentItemClass).data('source-media'); 3041 3042 var bestItemSourceWithCurrentScreenSize = this.__bestItemSourceForCurrentScreen(contentConfig['content']); 3043 3044 if (currentItemSource && bestItemSourceWithCurrentScreenSize && currentItemSource != bestItemSourceWithCurrentScreenSize['media']) { 3045 // re-init, in order to replace with more appropriate source 3046 this.__init(this.itemConfig); 3047 3048 return; 3049 } 3050 }; 3051 /***************** 3052 * Constructor 3053 */ 3054 3055 3056 this.__init(itemConfig); 3057}; 3058/************** 3059* Static Variables 3060*/ 3061 3062 3063FN.contentTextWithContent["static"] = { 3064 itemInstances: [], 3065 selectors: { 3066 itemWrapperClass: 'text-with-content', 3067 contentItemClass: 'text-with-content__content', 3068 textItemClass: 'text-with-content__text' 3069 }, 3070 initAllItems: function initAllItems() {
3071 if ('contentTextWithContentItems' in FN) { 3072 for (var i = 0; i < FN.contentTextWithContentItems.length; i++) { 3073 var itemConfig = FN.contentTextWithContentItems[i]; 3074 FN.contentTextWithContent["static"].itemInstances.push(new FN.contentTextWithContent(itemConfig)); 3075 } 3076 } // register all videos with the autoplayer (no-op until videoAutoplayer.init() runs) 3077 3078 3079 if (typeof FN.videoAutoplayer != 'undefined') FN.videoAutoplayer.observeAll(); // replace testimonial placeholders 3080 3081 if (typeof FN.testimonials != 'undefined') FN.testimonials.init(); // react on window resize 3082 3083 $(window).on('resize', FN.contentTextWithContent["static"].windowWasResized); 3084 }, 3085 windowWasResized: function windowWasResized(event) { 3086 for (var i = 0; i < FN.contentTextWithContent["static"].itemInstances.length; i++) { 3087 var contentTextWithContentItem = FN.contentTextWithContent["static"].itemInstances[i]; 3088 3089 contentTextWithContentItem.__checkContentItemSizes(); 3090 } 3091 3092 if (typeof FN.testimonials != 'undefined') FN.testimonials.init(); 3093 } 3094}; 3095if (typeof FN == 'undefined') FN = {}; 3096 3097FN.contentSlider = function (baseElement) { 3098 /**************** 3099 * Initialization 3100 */ 3101 this.__init = function (baseElement) { 3102 this.platform = FN.helpers.platform(); 3103 this.baseElement = baseElement; // find + save elements 3104 3105 this.allItems = $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.itemClass); 3106 this.activeItem = $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.activeItemClass); 3107 if (this.activeItem.length != 1) this.__makeItemActive(this.allItems[0]); // build + insert controls HTML 3108 3109 this.__buildControlsHtml(); 3110 3111 this.__buildNavigationHtml(); 3112 3113 this.__markActiveNavigationItem(); // set up event triggers 3114 3115 3116 $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.controlPreviousClass).on('click', $.proxy(this.__showPreviousItemClicked, this)); 3117 $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.controlNextClass).on('click', $.proxy(this.__showNextItemClicked, this)); 3118 $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.navigationItemClass).on('click', $.proxy(this.__navigationItemClicked, this)); // start autorotation 3119 3120 this.__startActiveItemRotation(); 3121 }; 3122 /***************** 3123 * Build next/previous controls HTML structure 3124 */ 3125 3126 3127 this.__buildControlsHtml = function () { 3128 var html = ''; // add controls 3129 3130 html += '<div class="content-slider__control--previous"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 25"><path d="M13 0a1 1 0 0 1 .7.29 1 1 0 0 1 0 1.39L2.81 12.5l10.9 10.82a1 1 0 0 1 0 1.39 1 1 0 0 1-1.41 0L0 12.5 12.3.29A1 1 0 0 1 13 0" fill="#51bbf1" /></svg></div>'; 3131 html += '<div class="content-slider__control--next"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 25"><path d="M1 25a1 1 0 0 1-.7-.29 1 1 0 0 1 0-1.39L11.19 12.5.29 1.68a1 1 0 0 1 0-1.39 1 1 0 0 1 1.41 0L14 12.5 1.7 24.71A1 1 0 0 1 1 25" fill="#51bbf1" /></svg></div>'; 3132 $(this.baseElement).prepend(html); 3133 }; 3134 /***************** 3135 * Build navigation HTML structure 3136 */ 3137 3138 3139 this.__buildNavigationHtml = function () { 3140 var html = ''; // add navigation, one for each item 3141 3142 html += '<div class="' + FN.contentSlider["static"].selectors.navigationWrapperClass + '">'; 3143 3144 for (var i = 0; i < this.allItems.length; i++) { 3145 html += '<a class="' + FN.contentSlider["static"].selectors.navigationItemClass + '"></a>'; 3146 } 3147 3148 html += '</div>'; 3149 $(this.baseElement).append(html); 3150 }; 3151 /***************** 3152 * Next item button was clicked 3153 */ 3154 3155 3156 this.__showNextItemClicked = function (event) { 3157 clearInterval(this.autorotateTimer); 3158 3159 this.__showNextItem(event); 3160 }; 3161 /***************** 3162 * Previous item button was clicked 3163 */ 3164 3165 3166 this.__showPreviousItemClicked = function (event) { 3167 clearInterval(this.autorotateTimer); 3168 3169 this.__showPreviousItem(event); 3170 }; 3171 /***************** 3172 * Navigation item was clicked 3173 */ 3174 3175 3176 this.__navigationItemClicked = function (event) { 3177 clearInterval(this.autorotateTimer); 3178 var selectedItemIndex, direction; 3179 3180 var activeItemIndex = this.__activeItemIndex(); 3181 3182 var selectedItem = event.target; 3183 var navigationItems = $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.navigationItemClass); 3184 3185 for (var i = 0; i < navigationItems.length; i++) { 3186 if ($(navigationItems[i]).is(selectedItem)) { 3187 selectedItemIndex = i; 3188 direction = selectedItemIndex > activeItemIndex ? 'next' : 'previous'; 3189 break; 3190 } 3191 } 3192 3193 this.__makeItemActive(this.allItems[selectedItemIndex], direction); 3194 }; 3195 /***************** 3196 * Shows next item 3197 */ 3198 3199 3200 this.__showNextItem = function (event) { 3201 var nextItemIndex = false;
3202 3203 var currentItemIndex = this.__activeItemIndex(); 3204 3205 if (currentItemIndex >= this.allItems.length - 1) { 3206 nextItemIndex = 0; 3207 } else { 3208 nextItemIndex = currentItemIndex + 1; 3209 } 3210 3211 this.__makeItemActive(this.allItems[nextItemIndex], 'next'); 3212 }; 3213 /***************** 3214 * Shows previous item 3215 */ 3216 3217 3218 this.__showPreviousItem = function (event) { 3219 var previousItemIndex = false; 3220 3221 var currentItemIndex = this.__activeItemIndex(); 3222 3223 if (currentItemIndex == 0) { 3224 previousItemIndex = this.allItems.length - 1; 3225 } else { 3226 previousItemIndex = currentItemIndex - 1; 3227 } 3228 3229 this.__makeItemActive(this.allItems[previousItemIndex], 'previous'); 3230 }; 3231 /***************** 3232 * Make an item active 3233 */ 3234 3235 3236 this.__makeItemActive = function (newActiveItem, direction) { 3237 var hideClass = direction === 'next' ? FN.contentSlider["static"].selectors.hideRightItemClass : FN.contentSlider["static"].selectors.hideLeftItemClass; 3238 $(this.activeItem).removeClass(FN.contentSlider["static"].selectors.activeItemClass).addClass(hideClass); 3239 $(newActiveItem).removeClass(FN.contentSlider["static"].selectors.hideLeftItemClass + ' ' + FN.contentSlider["static"].selectors.hideRightItemClass).addClass(FN.contentSlider["static"].selectors.activeItemClass); 3240 this.activeItem = $(newActiveItem); 3241 3242 this.__markActiveNavigationItem(); 3243 }; 3244 /***************** 3245 * Returns the index of the currently active item 3246 */ 3247 3248 3249 this.__activeItemIndex = function () { 3250 for (var i = 0; i < this.allItems.length; i++) { 3251 if ($(this.allItems[i]).is(this.activeItem)) { 3252 return i; 3253 } 3254 } 3255 3256 return false; 3257 }; 3258 /***************** 3259 * Marks the currently active navigation item 3260 */ 3261 3262 3263 this.__markActiveNavigationItem = function () { 3264 var activeItemIndex = this.__activeItemIndex(); 3265 3266 var navigationItems = $(this.baseElement).find('.' + FN.contentSlider["static"].selectors.navigationItemClass); 3267 3268 for (var i = 0; i < navigationItems.length; i++) { 3269 if (i === activeItemIndex) { 3270 $(navigationItems[i]).addClass('active'); 3271 } else { 3272 $(navigationItems[i]).removeClass('active'); 3273 } 3274 } 3275 }; 3276 /***************** 3277 * Activate the timer/interval to rotate items automatically 3278 */ 3279 3280 3281 this.__startActiveItemRotation = function () { 3282 // start autorotation timer (if configured) 3283 var autorotationConfig = $(this.baseElement).data('autorotate'); 3284 if (!autorotationConfig || isNaN(autorotationConfig)) return; 3285 var instance = this; 3286 this.autorotateTimer = setInterval(function () { 3287 instance.__showNextItem(); 3288 }, parseInt(autorotationConfig)); 3289 }; 3290 /***************** 3291 * Constructor 3292 */ 3293 3294 3295 this.__init(baseElement); 3296}; 3297/************** 3298* Static Variables 3299*/ 3300 3301 3302FN.contentSlider["static"] = { 3303 itemInstances: [], 3304 selectors: { 3305 wrapperClass: 'content-slider', 3306 itemClass: 'content-slider__item', 3307 controlPreviousClass: 'content-slider__control--previous', 3308 controlNextClass: 'content-slider__control--next', 3309 navigationWrapperClass: 'content-slider__navigation', 3310 navigationItemClass: 'content-slider__navigation-item', 3311 activeItemClass: 'active', 3312 hideLeftItemClass: 'hidden-left', 3313 hideRightItemClass: 'hidden-right' 3314 } 3315}; 3316$(document).ready(function () { 3317 $('.' + FN.contentSlider["static"].selectors.wrapperClass).each(function (index, element) { 3318 FN.contentSlider["static"].itemInstances.push(new FN.contentSlider(element)); 3319 }); 3320}); 3321document.addEventListener('DOMContentLoaded', function () { 3322 var closeButtons = document.querySelectorAll(".modal-close"); 3323 closeButtons.forEach(function (closeButton) { 3324 closeButton.addEventListener("click", function () { 3325 var modal = closeButton.closest(".modal-view"); 3326 modal.classList.remove("modal-view-reveal"); 3327 modal.classList.add("modal-view-hide"); 3328 setTimeout(function () { 3329 modal.close(); 3330 modal.classList.remove("modal-view-hide"); 3331 }, 300); 3332 }); 3333 }); 3334 var modalAppIconImage = document.querySelector(".modal-app-icon img"); 3335 3336 if (modalAppIconImage) { 3337 if (modalAppIconImage.complete) { 3338 modalAppIconImage.closest(".modal-app-icon").classList.add("modal-app-icon-loaded"); 3339 } else { 3340 modalAppIconImage.onload = function () { 3341 modalAppIconImage.closest(".modal-app-icon").classList.add("modal-app-icon-loaded"); 3342 }; 3343 } 3344 } 3345 3346 var afterDownloadModal = document.getElementById("after-download-modal"); // There are multiple buttons, one for each platform 3347 3348 var downloadButtons = document.querySelectorAll(".nav-top-download a, .hero-download a"); 3349 downloadButtons.forEach(function (downloadButton) { 3350 downloadButton.addEventListener("click", function (event) { 3351 event.preventDefault(); 3352 var isMobile = window.matchMedia("(max-width: 900px)").matches; 3353 3354 if (isMobile) { 3355 var nav = document.querySelector(".nav-top-common"); 3356 nav.classList.remove("active"); 3357 var trialViaEmailModal = document.getElementById("trial-via-email"); 3358 trialViaEmailModal.showModal(); 3359 } else { 3360 var body = document.getElementsByTagName("body")[0]; 3361 var iframe = document.createElement("iframe"); 3362 iframe.src = downloadButton.href; 3363 iframe.style.display = "none"; 3364 body.appendChild(iframe); 3365 afterDownloadModal.showModal(); 3366 afterDownloadModal.classList.add("modal-view-reveal"); 3367 } 3368 }); 3369 }); 3370 var afterSubscribeModal = document.getElementById("after-subscribe-modal"); 3371 document.addEventListener("TowerNewsletterSubscribed", function (event) { 3372 if (afterDownloadModal.open) { 3373 afterDownloadModal.close(); 3374 afterSubscribeModal.showModal(); 3375 afterSubscribeModal.classList.add("modal-view-reveal"); 3376 } 3377 }); 3378}); 3379document.addEventListener("DOMContentLoaded", function () { 3380 var trackTrial = function trackTrial() { 3381 // Get GA client ID from 'ga' object, get campaign query parameters 3382 // 3383 // If the 'ga' object isn't available, or extraction of the client ID 3384 // otherwise fails, we should get an exception which is caught by the 'try' 3385 // block and no request is made. 3386 try { 3387 var url = FN.config.urls.licensingBase + 'trial_trackings'; 3388 var tracker = ga.getAll()[0]; 3389 var clientId = tracker.get('clientId'); 3390 var data = { 3391 ga_client_id: clientId 3392 }; 3393 var query = window.location.search;
3394 var validCampaignParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'utm_id', 'utm_mt', 'utm_device', 'lpurl', 'gclid', 'fbclid', 'msclkid']; 3395 var campaignParams, campaignParam; 3396 var i; // Extract campaign params from query string into [key, value] arrays 3397 3398 campaignParams = query.slice(1).split('&').map(function (pair) { 3399 return pair.split('='); 3400 }); 3401 3402 for (i = 0; i < campaignParams.length; i++) { 3403 var campaignParam = campaignParams[i]; 3404 3405 if (validCampaignParams.indexOf(campaignParam[0]) > -1) { 3406 data[campaignParam[0]] = campaignParam[1]; 3407 } 3408 } 3409 3410 data = JSON.stringify(data); 3411 fetch(url, { 3412 method: 'POST', 3413 headers: { 3414 'Content-Type': 'application/json' 3415 }, 3416 body: data 3417 }); 3418 } catch (_unused) {} 3419 }; 3420 3421 var trialButtons = document.querySelectorAll(".button--start-trial"); 3422 trialButtons.forEach(function (trialButton) { 3423 trialButton.addEventListener("click", function () { 3424 trackTrial(); 3425 }); 3426 }); 3427}); 3428if (typeof FN == 'undefined') FN = {}; 3429FN.downloadDropdown = { 3430 instances: [], 3431 init: function init() { 3432 var dropdownContainers = document.querySelectorAll('.button--split-dropdown'); 3433 dropdownContainers.forEach(function (container) { 3434 var instance = new FN.downloadDropdown.Instance(container); 3435 FN.downloadDropdown.instances.push(instance); 3436 }); 3437 }, 3438 Instance: function Instance(container) { 3439 this.container = container; 3440 this.trigger = container.querySelector('.button__dropdown-trigger'); 3441 this.dropdown = container.querySelector('.download-dropdown'); 3442 this.isOpen = false; 3443 this.init(); 3444 } 3445}; 3446FN.downloadDropdown.Instance.prototype = { 3447 init: function init() { 3448 if (!this.trigger || !this.dropdown) { 3449 console.warn('Download dropdown: Missing trigger or dropdown element'); 3450 return; 3451 } 3452 3453 this.bindEvents(); 3454 }, 3455 bindEvents: function bindEvents() { 3456 var self = this; // Toggle dropdown on trigger click 3457 3458 this.trigger.addEventListener('click', function (e) { 3459 e.preventDefault(); 3460 e.stopPropagation(); 3461 self.toggle(); 3462 }); // Close dropdown when clicking outside 3463 3464 document.addEventListener('click', function (e) { 3465 if (!self.container.contains(e.target)) { 3466 self.close(); 3467 } 3468 }); // Handle keyboard navigation 3469 3470 this.trigger.addEventListener('keydown', function (e) { 3471 if (e.key === 'Enter' || e.key === ' ') { 3472 e.preventDefault(); 3473 self.toggle(); 3474 } else if (e.key === 'ArrowDown' && self.isOpen) { 3475 e.preventDefault(); 3476 self.focusFirstItem(); 3477 } else if (e.key === 'Escape') { 3478 self.close(); 3479 self.trigger.focus(); 3480 } 3481 }); // Handle dropdown item keyboard navigation 3482 3483 var dropdownItems = this.dropdown.querySelectorAll('.download-dropdown__item'); 3484 dropdownItems.forEach(function (item, index) { 3485 item.addEventListener('keydown', function (e) { 3486 if (e.key === 'ArrowDown') { 3487 e.preventDefault(); 3488 var nextItem = dropdownItems[index + 1]; 3489 3490 if (nextItem) { 3491 nextItem.focus(); 3492 } else { 3493 dropdownItems[0].focus(); // Loop to first 3494 } 3495 } else if (e.key === 'ArrowUp') { 3496 e.preventDefault(); 3497 var prevItem = dropdownItems[index - 1]; 3498 3499 if (prevItem) { 3500 prevItem.focus(); 3501 } else { 3502 dropdownItems[dropdownItems.length - 1].focus(); // Loop to last 3503 } 3504 } else if (e.key === 'Escape') { 3505 self.close(); 3506 self.trigger.focus(); 3507 } else if (e.key === 'Tab' && !e.shiftKey && index === dropdownItems.length - 1) { 3508 // Close dropdown when tabbing out of last item 3509 self.close(); 3510 } else if (e.key === 'Tab' && e.shiftKey && index === 0) { 3511 // Close dropdown when shift-tabbing out of first item 3512 self.close(); 3513 } 3514 }); // Close dropdown when item is clicked 3515 3516 item.addEventListener('click', function () { 3517 self.close(); 3518 }); 3519 }); 3520 },
3521 toggle: function toggle() { 3522 if (this.isOpen) { 3523 this.close(); 3524 } else { 3525 this.open(); 3526 } 3527 }, 3528 open: function open() { 3529 if (this.isOpen) return; // Close other open dropdowns 3530 3531 FN.downloadDropdown.instances.forEach(function (instance) { 3532 if (instance !== this && instance.isOpen) { 3533 instance.close(); 3534 } 3535 }.bind(this)); 3536 this.isOpen = true; 3537 this.trigger.classList.add('dropdown--open'); 3538 this.dropdown.classList.add('dropdown--open'); 3539 this.trigger.setAttribute('aria-expanded', 'true'); // Set focus to first item after a brief delay to ensure dropdown is visible 3540 3541 setTimeout(function () { 3542 this.focusFirstItem(); 3543 }.bind(this), 50); 3544 }, 3545 close: function close() { 3546 if (!this.isOpen) return; 3547 this.isOpen = false; 3548 this.trigger.classList.remove('dropdown--open'); 3549 this.dropdown.classList.remove('dropdown--open'); 3550 this.trigger.setAttribute('aria-expanded', 'false'); 3551 }, 3552 focusFirstItem: function focusFirstItem() { 3553 var firstItem = this.dropdown.querySelector('.download-dropdown__item'); 3554 3555 if (firstItem) { 3556 firstItem.focus(); 3557 } 3558 } 3559}; // Initialize on DOM ready 3560 3561$(document).ready(function () { 3562 FN.downloadDropdown.init(); 3563}); 3564if (typeof FN == 'undefined') FN = {}; 3565FN.architectureAdaptiveButton = { 3566 instances: [], 3567 init: function init() { 3568 // Handle split dropdown buttons 3569 var adaptiveButtons = document.querySelectorAll('[data-architecture-adaptive="true"]'); 3570 adaptiveButtons.forEach(function (button) { 3571 var instance = new FN.architectureAdaptiveButton.Instance(button); 3572 FN.architectureAdaptiveButton.instances.push(instance); 3573 }); // Handle single adaptive links 3574 3575 var adaptiveLinks = document.querySelectorAll('[data-architecture-adaptive-link="true"]'); 3576 adaptiveLinks.forEach(function (link) { 3577 var instance = new FN.architectureAdaptiveButton.LinkInstance(link); 3578 FN.architectureAdaptiveButton.instances.push(instance); 3579 }); 3580 }, 3581 Instance: function Instance(container) { 3582 this.container = container; 3583 this.mainButton = container.querySelector('.button__main'); 3584 this.buttonTextSpan = container.querySelector('.button-text'); 3585 this.dropdownItem = container.querySelector('.download-dropdown__item'); 3586 this.dropdownTextSpan = container.querySelector('.dropdown-item-text'); // Store original values 3587 3588 this.originalMainText = this.buttonTextSpan ? this.buttonTextSpan.getAttribute('data-default-text') : null; 3589 this.originalMainHref = this.mainButton ? this.mainButton.getAttribute('data-default-href') : null; 3590 this.originalDropdownText = this.dropdownTextSpan ? this.dropdownTextSpan.getAttribute('data-default-text') : null; 3591 this.armUrl = this.container.getAttribute('data-arm-url'); 3592 this.init(); 3593 }, 3594 LinkInstance: function LinkInstance(link) { 3595 this.link = link; // Store original values 3596 3597 this.originalHref = link.getAttribute('data-default-href'); 3598 this.armHref = link.getAttribute('data-arm-href'); 3599 this.init(); 3600 } 3601}; 3602FN.architectureAdaptiveButton.Instance.prototype = { 3603 init: function init() { 3604 if (!this.mainButton || !this.buttonTextSpan || !this.dropdownItem || !this.dropdownTextSpan) { 3605 console.warn('Architecture adaptive button: Missing required elements'); 3606 return; 3607 } 3608 3609 this.detectAndUpdateButton(); 3610 }, 3611 detectAndUpdateButton: function detectAndUpdateButton() { 3612 var self = this; // Use architecture detector to determine user's architecture 3613 3614 FN.architectureDetector.detect().then(function (architecture) { 3615 self.updateButtonForArchitecture(architecture); 3616 })["catch"](function (error) { 3617 console.warn('Architecture detection failed, using default (x86):', error); // Fallback to x86 as default - no changes needed since template defaults to x86 3618 3619 self.updateButtonForArchitecture('x86'); 3620 }); 3621 }, 3622 updateButtonForArchitecture: function updateButtonForArchitecture(architecture) { 3623 if (architecture === 'arm') { 3624 this.configureForARM(); 3625 } else { 3626 // Default to x86 for 'x86' or 'unknown' 3627 this.configureForx86(); 3628 } 3629 }, 3630 configureForARM: function configureForARM() { 3631 // User is on ARM, so show ARM as main button and x86 in dropdown 3632 var armText = this.originalMainText.replace('(x64)', '(ARM)'); 3633 var x86Text = this.originalDropdownText.replace('(ARM)', '(x64)'); // Update main button 3634 3635 this.buttonTextSpan.textContent = armText; // Set ARM download URL for main button 3636 3637 this.mainButton.href = this.armUrl; // Update dropdown item 3638 3639 this.dropdownTextSpan.textContent = x86Text; 3640 this.dropdownItem.href = this.originalMainHref; // x86 version goes to dropdown 3641 3642 this.dropdownItem.setAttribute('data-track', 'x86'); // Update aria label 3643 3644 var dropdownTrigger = this.container.querySelector('.button__dropdown-trigger'); 3645 3646 if (dropdownTrigger) { 3647 dropdownTrigger.setAttribute('aria-label', 'More download options (x86 version available)'); 3648 } 3649 }, 3650 configureForx86: function configureForx86() { 3651 // User is on x86, keep default configuration (x86 main, ARM in dropdown) 3652 // This is already the default state from the template, but we'll be explicit 3653 // Ensure main button shows x86 3654 this.buttonTextSpan.textContent = this.originalMainText; 3655 this.mainButton.href = this.originalMainHref; // Ensure dropdown shows ARM 3656 3657 this.dropdownTextSpan.textContent = this.originalDropdownText; 3658 this.dropdownItem.href = this.armUrl; // ARM download URL 3659 3660 this.dropdownItem.setAttribute('data-track', 'arm'); // Update aria label 3661 3662 var dropdownTrigger = this.container.querySelector('.button__dropdown-trigger'); 3663 3664 if (dropdownTrigger) { 3665 dropdownTrigger.setAttribute('aria-label', 'More download options (ARM version available)'); 3666 } 3667 } 3668}; 3669FN.architectureAdaptiveButton.LinkInstance.prototype = {
3670 init: function init() { 3671 if (!this.originalHref || !this.armHref) { 3672 console.warn('Architecture adaptive link: Missing required href attributes'); 3673 return; 3674 } 3675 3676 this.detectAndUpdateLink(); 3677 }, 3678 detectAndUpdateLink: function detectAndUpdateLink() { 3679 if (!FN.architectureDetector || typeof FN.architectureDetector.detect !== 'function') { 3680 console.warn('Architecture detector not available, using default href'); 3681 return; 3682 } 3683 3684 FN.architectureDetector.detect().then(function (architecture) { 3685 if (architecture === 'arm') { 3686 this.configureForARM(); 3687 } else { 3688 this.configureForx86(); 3689 } 3690 }.bind(this))["catch"](function (error) { 3691 console.warn('Failed to detect architecture for adaptive link:', error); 3692 }); 3693 }, 3694 configureForARM: function configureForARM() { 3695 // User is on ARM, update href to ARM version 3696 this.link.href = this.armHref; 3697 }, 3698 configureForx86: function configureForx86() { 3699 // User is on x86, use default href 3700 this.link.href = this.originalHref; 3701 } 3702}; // Initialize on DOM ready, but after architecture detector 3703 3704$(document).ready(function () { 3705 // Small delay to ensure architecture detector is available 3706 setTimeout(function () { 3707 if (typeof FN.architectureDetector !== 'undefined') { 3708 FN.architectureAdaptiveButton.init(); 3709 } else { 3710 console.warn('Architecture detector not available, adaptive buttons will use defaults'); 3711 } 3712 }, 100); 3713}); 3714if (typeof FN == 'undefined') FN = {}; 3715FN.config = { 3716 urls: { 3717 base: false, 3718 apiBase: false, 3719 licensingBase: false,
3720 checkoutApi: false, 3721 enterpriseInvoiceCheckoutApi: false, 3722 apiContactSubscribe: 'contacts/subscribe', 3723 apiContactUpdate: 'contacts/update', 3724 apiEnterpriseContact: 'deal/add', 3725 apiJobApplication: 'emails/job-application', 3726 apiTicketSupport: 'tickets', 3727 apiEnterpriseLead: 'sales_leads/enterprise', 3728 apiTeamLead: 'sales_leads/team', 3729 ticketSupportSuccess: 'support/contact/message-sent', 3730 helpSearchMac: 'https://www.git-tower.com/help/search/mac', 3731 helpSearchWindows: 'https://www.git-tower.com/help/search/windows' 3732 }, 3733 analytics: { 3734 siteID: 'tower-website', 3735 addPlatformToEventLabel: true 3736 }, 3737 responsiveBreakpoints: [// NOTE: values here must be the same as in _variables.scss!! 3738 { 3739 'media': 'xl', 3740 'width': '1400px' 3741 }, { 3742 'media': 'l', 3743 'width': '1100px' 3744 }, { 3745 'media': 'm', 3746 'width': '760px' 3747 }, { 3748 'media': 's', 3749 'width': '440px' 3750 }, { 3751 'media': 'xs', 3752 'width': '1px' 3753 }] 3754}; 3755 3756FN.config.setEnvironmentUrls = function (type) { 3757 var urls = { 3758 'base': false, 3759 'apiBase': false 3760 }; 3761 var host = window.location.host; 3762 3763 if (host.indexOf('website-tower') === 0) { 3764 // development 3765 urls.base = '/'; 3766 urls.apiBase = 'http://website-api.test/'; 3767 urls.licensingBase = 'https://staging.account.git-tower.com/api/'; 3768 urls.checkoutApi = 'http://website-tower.test/store/api/'; 3769 urls.enterpriseInvoiceCheckoutApi = 'http://website-tower.test/store/api/enterprise-invoice-checkout/'; 3770 urls.educationApi = 'http://website-tower.test/a/education-api/'; 3771 } else if (host.indexOf('staging.git-tower.com') === 0) { 3772 // staging 3773 urls.base = '/'; 3774 urls.apiBase = 'https://staging.git-tower.com/api/'; 3775 urls.licensingBase = 'https://staging.account.git-tower.com/api/'; 3776 urls.checkoutApi = 'https://staging.git-tower.com/store/api/'; 3777 urls.enterpriseInvoiceCheckoutApi = 'https://staging.git-tower.com/store/api/enterprise-invoice-checkout/'; 3778 urls.educationApi = 'https://staging.git-tower.com/a/education-api/'; 3779 } else { 3780 // production 3781 urls.base = '/'; 3782 urls.apiBase = 'https://www.git-tower.com/api/'; 3783 urls.licensingBase = 'https://account.git-tower.com/api/'; 3784 urls.checkoutApi = 'https://www.git-tower.com/store/api/'; 3785 urls.enterpriseInvoiceCheckoutApi = 'https://www.git-tower.com/store/api/enterprise-invoice-checkout/'; 3786 urls.educationApi = 'https://www.git-tower.com/a/education-api/'; 3787 } 3788 3789 FN.config.urls.base = urls.base; 3790 FN.config.urls.apiBase = urls.apiBase; 3791 FN.config.urls.licensingBase = urls.licensingBase; 3792 FN.config.urls.checkoutApi = urls.checkoutApi; 3793 FN.config.urls.enterpriseInvoiceCheckoutApi = urls.enterpriseInvoiceCheckoutApi; 3794 FN.config.urls.educationApi = urls.educationApi; 3795}; 3796 3797FN.config.setEnvironmentUrls(); 3798/*********** Configure Analytics Event Tracking */ 3799 3800FN.config.analyticsEvents = [ 3801/* Newsletter Subscription */ 3802{ 3803 'config': { 3804 'selector': '.overlay__container--download .newsletter-form.analytics--submit', 3805 'triggerOnlyOnce': true 3806 }, 3807 'event': { 3808 'eventCategory': 'newsletter', 3809 'eventAction': 'signup', 3810 'eventLabelSuffix': 'trial-download' 3811 } 3812}, { 3813 'config': { 3814 'selector': '#after-download-modal .newsletter-form.analytics--submit', 3815 'triggerOnlyOnce': true 3816 }, 3817 'event': { 3818 'eventCategory': 'newsletter', 3819 'eventAction': 'signup', 3820 'eventLabelSuffix': 'trial-download' 3821 } 3822}, { 3823 'config': { 3824 'selector': '.overlay__container--trial-via-email .newsletter-form.analytics--submit', 3825 'triggerOnlyOnce': true 3826 }, 3827 'event': { 3828 'eventCategory': 'newsletter', 3829 'eventAction': 'signup', 3830 'eventLabelSuffix': 'trial-via-email' 3831 } 3832}, { 3833 'config': { 3834 'selector': '#trial-via-email .newsletter-form.analytics--submit', 3835 'triggerOnlyOnce': true 3836 }, 3837 'event': { 3838 'eventCategory': 'newsletter', 3839 'eventAction': 'signup', 3840 'eventLabelSuffix': 'trial-via-email' 3841 } 3842}, 3843/* Trial Download Buttons */ 3844{ 3845 'config': { 3846 'selector': '.nav-top--standard .visible-on--mac .button--start-trial.analytics--click' 3847 }, 3848 'event': { 3849 'eventCategory': 'sales', 3850 'eventAction': 'trial-download-mac', 3851 'eventLabelSuffix': 'top-navi' 3852 } 3853}, { 3854 'config': { 3855 'selector': '.nav-top-download .visible-on--mac .button--start-trial.analytics--click' 3856 }, 3857 'event': { 3858 'eventCategory': 'sales', 3859 'eventAction': 'trial-download-mac', 3860 'eventLabelSuffix': 'top-navi' 3861 } 3862}, { 3863 'config': { 3864 'selector': '.nav-top--standard .visible-on--windows .button--start-trial.analytics--click' 3865 }, 3866 'event': { 3867 'eventCategory': 'sales', 3868 'eventAction': 'trial-download-windows', 3869 'eventLabelSuffix': 'top-navi' 3870 } 3871}, { 3872 'config': { 3873 'selector': '.nav-top-download .visible-on--windows .button--start-trial.analytics--click' 3874 }, 3875 'event': { 3876 'eventCategory': 'sales', 3877 'eventAction': 'trial-download-windows', 3878 'eventLabelSuffix': 'top-navi' 3879 } 3880}, { 3881 'config': { 3882 'selector': 'header .visible-on--mac .button--start-trial.analytics--click' 3883 }, 3884 'event': { 3885 'eventCategory': 'sales', 3886 'eventAction': 'trial-download-mac', 3887 'eventLabelSuffix': 'header' 3888 } 3889}, { 3890 'config': { 3891 'selector': 'header .visible-on--windows .button--start-trial.analytics--click' 3892 }, 3893 'event': { 3894 'eventCategory': 'sales', 3895 'eventAction': 'trial-download-windows', 3896 'eventLabelSuffix': 'header' 3897 } 3898}, { 3899 'config': { 3900 'selector': '#content_button-bar--middle .visible-on--mac .button--start-trial.analytics--click' 3901 }, 3902 'event': { 3903 'eventCategory': 'sales', 3904 'eventAction': 'trial-download-mac', 3905 'eventLabelSuffix': 'mid-page' 3906 } 3907}, { 3908 'config': { 3909 'selector': '#content_button-bar--middle .visible-on--windows .button--start-trial.analytics--click' 3910 }, 3911 'event': { 3912 'eventCategory': 'sales', 3913 'eventAction': 'trial-download-windows', 3914 'eventLabelSuffix': 'mid-page' 3915 } 3916}, { 3917 'config': { 3918 'selector': '#content_button-bar--footer .visible-on--mac .button--start-trial.analytics--click' 3919 }, 3920 'event': { 3921 'eventCategory': 'sales', 3922 'eventAction': 'trial-download-mac', 3923 'eventLabelSuffix': 'footer' 3924 } 3925}, { 3926 'config': { 3927 'selector': '#content_button-bar--footer .visible-on--windows .button--start-trial.analytics--click' 3928 }, 3929 'event': { 3930 'eventCategory': 'sales', 3931 'eventAction': 'trial-download-windows', 3932 'eventLabelSuffix': 'footer' 3933 } 3934}, 3935/* Trial via Email Buttons */ 3936{ 3937 'config': { 3938 'selector': '.nav-top--mobile .button--start-trial.analytics--click' 3939 }, 3940 'event': { 3941 'eventCategory': 'sales', 3942 'eventAction': 'trial-via-email', 3943 'eventLabelSuffix': 'top-navi' 3944 } 3945}, 3946/* Twitter */ 3947{ 3948 'config': { 3949 'selector': '.overlay__container--download .button--twitter.analytics--click' 3950 }, 3951 'event': { 3952 'eventCategory': 'social', 3953 'eventAction': 'share', 3954 'eventLabelSuffix': 'twitter' 3955 } 3956}, { 3957 'config': { 3958 'selector': '#after-subscribe-modal .button--twitter.analytics--click' 3959 }, 3960 'event': { 3961 'eventCategory': 'social', 3962 'eventAction': 'share', 3963 'eventLabelSuffix': 'twitter' 3964 } 3965}, 3966/* Page Interactions */ 3967{ 3968 'config': { 3969 'selector': '.announcement-bar--introductory-pricing .button.analytics--click', 3970 'waitBeforeDefault': true 3971 }, 3972 'event': { 3973 'eventCategory': 'sales', 3974 'eventAction': 'discount-button', 3975 'eventLabelSuffix': 'introductory-pricing' 3976 } 3977}, { 3978 'config': { 3979 'selector': '.referrer-content--none .button.analytics--click', 3980 'waitBeforeDefault': true 3981 }, 3982 'event': { 3983 'eventCategory': 'page-interaction', 3984 'eventAction': 'button', 3985 'eventLabelSuffix': 'show-new-features' 3986 } 3987}, 3988/* Enterprise/team contact forms */ 3989{ 3990 'config': { 3991 'selector': '#enterprise-contact__form.analytics--submit', 3992 'triggerOnlyOnce': true 3993 }, 3994 'event': { 3995 'eventCategory': 'sales', 3996 'eventAction': 'enterprise-request', 3997 'eventLabelSuffix': 'contact-form' 3998 } 3999}, { 4000 'config': { 4001 'selector': '#team-contact__form.analytics--submit', 4002 'triggerOnlyOnce': true 4003 }, 4004 'event': { 4005 'eventCategory': 'sales', 4006 'eventAction': 'enterprise-request', 4007 'eventLabelSuffix': 'contact-form' 4008 } 4009}, 4010/* Alternative Architecture Downloads */ 4011{ 4012 'config': { 4013 'selector': '.download-dropdown__item[data-track="arm"]' 4014 }, 4015 'event': { 4016 'eventCategory': 'sales', 4017 'eventAction': 'trial-download-windows-arm', 4018 'eventLabelSuffix': 'dropdown' 4019 } 4020}, { 4021 'config': { 4022 'selector': '.download-dropdown__item[data-track="x86"]' 4023 }, 4024 'event': { 4025 'eventCategory': 'sales', 4026 'eventAction': 'trial-download-windows-x86', 4027 'eventLabelSuffix': 'dropdown' 4028 } 4029}]; // Quick'n'dirty Fathom events for downloads (Mac, Windows and "all") as well as 4030// for reaching the "order completed" page 4031// TODO: Should probably be extracted into a separate place 4032 4033window.addEventListener('DOMContentLoaded', function () { 4034 var macDownloadSelectors = ['.nav-top--standard .visible-on--mac .button--start-trial.analytics--click', 'header .visible-on--mac .button--start-trial.analytics--click', '#content_button-bar--middle .visible-on--mac .button--start-trial.analytics--click', '#content_button-bar--footer .visible-on--mac .button--start-trial.analytics--click']; 4035 4036 function handleMacDownload() { 4037 if (window.fathom) { 4038 // "Download Trial — all"
4039 window.fathom.trackGoal('CEZJGYMP', 0); // "Download Trial — Mac" 4040 4041 window.fathom.trackGoal('OOICDQYE', 0); 4042 } 4043 } 4044 4045 macDownloadSelectors.forEach(function (selector) { 4046 var macDownloadButtons = document.querySelectorAll(selector); 4047 4048 if (macDownloadButtons.length > 0) { 4049 macDownloadButtons.forEach(function (button) { 4050 button.addEventListener('click', handleMacDownload); 4051 }); 4052 } 4053 }); 4054 var windowsDownloadSelectors = ['.nav-top--standard .visible-on--windows .button--start-trial.analytics--click', 'header .visible-on--windows .button--start-trial.analytics--click', '#content_button-bar--middle .visible-on--windows .button--start-trial.analytics--click', '#content_button-bar--footer .visible-on--windows .button--start-trial.analytics--click']; 4055 4056 function handleWindowsDownload() { 4057 if (window.fathom) { 4058 // "Download Trial — all" 4059 window.fathom.trackGoal('CEZJGYMP', 0); // "Download Trial — Windows" 4060 4061 window.fathom.trackGoal('GRHVWPYL', 0); 4062 } 4063 } 4064 4065 windowsDownloadSelectors.forEach(function (selector) { 4066 var windowsDownloadButtons = document.querySelectorAll(selector); 4067 4068 if (windowsDownloadButtons.length > 0) { 4069 windowsDownloadButtons.forEach(function (button) { 4070 button.addEventListener('click', handleWindowsDownload); 4071 }); 4072 } 4073 }); // Alternative architecture download tracking 4074 4075 function handleAlternativeArchDownload(architecture) { 4076 if (window.fathom) { 4077 // "Download Trial — all" 4078 window.fathom.trackGoal('CEZJGYMP', 0); // Architecture-specific goals (you may need to create these goals) 4079 4080 if (architecture === 'arm') {// window.fathom.trackGoal('ARMVERSION', 0); 4081 } else if (architecture === 'x86') {// window.fathom.trackGoal('X86VERSION', 0); 4082 } 4083 } 4084 } // Add event listeners for alternative architecture download buttons 4085 4086 4087 document.addEventListener('click', function (e) { 4088 if (e.target.classList.contains('download-dropdown__item')) { 4089 var trackType = e.target.getAttribute('data-track'); 4090 4091 if (trackType === 'arm' || trackType === 'x86') { 4092 handleAlternativeArchDownload(trackType); 4093 } 4094 } 4095 }); 4096 var orderCompletedSelector = "#page--store__order-completed"; 4097 4098 function handleOrderCompleted() { 4099 if (window.fathom) { 4100 // "Purchase" 4101 window.fathom.trackGoal('3IQFXA38', 0); 4102 } 4103 } // Are we on the "order completed" page? 4104 4105 4106 if (document.querySelector(orderCompletedSelector)) { 4107 handleOrderCompleted(); 4108 } 4109}); 4110$(document).ready(function () { 4111 /*********** Toggle Mac / Windows Specific Content */ 4112 FN.osSwitch.init(); 4113 $(document).on("FN.osSwitch:switched", function (event, context) { 4114 FN.contentTextWithMedia["static"].initAllItems(); 4115 FN.contentTextWithContent["static"].initAllItems(); 4116 }); 4117 /*********** init analytics */ 4118 4119 FN.analytics.addConfiguredEvents(FN.config.analyticsEvents); 4120 /*********** prefill "source[OS]" value in newsletter forms */ 4121 4122 if ($('input[name="signup[OS]"]').length > 0) { 4123 $('input[name="signup[OS]"]').val(FN.helpers.platform()); 4124 } 4125 /*********** Init "text-with-media" items */ 4126 4127 4128 if (typeof FN.contentTextWithMedia !== 'undefined') FN.contentTextWithMedia["static"].initAllItems(); 4129 /*********** Init "text-with-content" items */ 4130 4131 if (typeof FN.contentTextWithContent !== 'undefined') FN.contentTextWithContent["static"].initAllItems(); 4132 /*********** Start autoplay videos */ 4133 4134 if (typeof FN.videoAutoplayer !== 'undefined') FN.videoAutoplayer.init(); 4135 /********** Screencast Video Player */ 4136 4137 if (typeof FN.videoPlayer !== 'undefined') FN.videoPlayer.init(); 4138 /********** Newsletter signup */ 4139 4140 $('.' + FN.newsletterConfig.selectors.formClass).each(function (index, element) { 4141 FN.newsletterConfig.instances.push(new FN.newsletter(element)); 4142 }); 4143}); 4144if (typeof FN == 'undefined') FN = {}; 4145FN.testimonials = { 4146 config: { 4147 selectors: { 4148 'testimonialPlaceholderClass': 'testimonial-placeholder' 4149 } 4150 }, 4151 init: function init() { 4152 if (typeof FN.testimonialData == 'undefined') return; // replace testimonial placeholders with real data 4153 4154 $('.' + FN.testimonials.config.selectors.testimonialPlaceholderClass).each(function (index, item) { 4155 var testimonialId = $(item).data('id'); 4156 var testimonialData = FN.testimonials.findTestimonialDataById(testimonialId); 4157 if (!testimonialData) return; 4158 var testimonialLayout = $(item).data('layout'); 4159 if (!testimonialLayout) testimonialLayout = 'mini'; 4160 var testimonialHtml = FN.testimonials.buildHtml(testimonialData, testimonialLayout); 4161 $(item).replaceWith(testimonialHtml); 4162 }); 4163 }, 4164 findTestimonialDataById: function findTestimonialDataById(id) { 4165 var testimonialData = false;
4166 4167 for (var i = 0; i < FN.testimonialData.length; i++) { 4168 if (id == FN.testimonialData[i]['id']) { 4169 testimonialData = FN.testimonialData[i]; 4170 break; 4171 } 4172 } 4173 4174 return testimonialData; 4175 }, 4176 buildHtml: function buildHtml(testimonial, layout) { 4177 var html = '<div class="testimonial testimonial--' + layout + '">'; 4178 html += ' <div class="testimonial__image"><img src="' + FN.helpers.absoluteUrl(testimonial['image']) + '@140w.jpg" width="140" height="140" alt="' + testimonial['name'] + '" /></div>'; 4179 html += ' <p class="testimonial__text">' + testimonial['text'] + '</p>'; 4180 html += ' <p class="testimonial__person">'; 4181 4182 if (testimonial['link'] !== '') { 4183 html += '<a href=' + testimonial['link'] + ' target="_blank" rel="noopener noreferrer"><span class="testimonial__name">' + testimonial['name'] + '</span></a>'; 4184 } else { 4185 html += '<span class="testimonial__name">' + testimonial['name'] + '</span>'; 4186 } 4187 4188 if (testimonial['company'] !== '') { 4189 html += '<span class="testimonial__company">' + testimonial['company'] + '</span>'; 4190 } 4191 4192 html += '</p></div>'; 4193 return html; 4194 } 4195}; 4196$(document).ready(function () { 4197 FN.testimonials.init(); 4198}); 4199if (typeof FN == 'undefined') FN = {}; 4200FN.releaseNotesConfig = { 4201 selectors: { 4202 naviLinksSelector: '.navi a', 4203 naviLinkActiveClass: 'navi--active', 4204 naviLinkHighlightClass: 'navi--highlights', 4205 naviLinkReleaseNotesClass: 'navi--release-notes', 4206 contentHighlightsClass: 'release__highlights', 4207 contentReleaseNotesClass: 'release__notes', 4208 majorReleaseContainerClass: 'release--major', 4209 checksumButtonSelector: '.release__copy-checksum' 4210 } 4211}; 4212FN.releaseNotes = { 4213 init: function init() { 4214 if ($('#page--release-notes').length != 1) return; 4215 FN.releaseNotes.autoplayVideos(); 4216 FN.releaseNotes.autoloadAnimatedGifs(); 4217 FN.releaseNotes.initNavi(); 4218 FN.releaseNotes.initChecksumButtons(); 4219 }, 4220 autoplayVideos: function autoplayVideos() { 4221 FN.videoAutoplayer.init(); 4222 }, 4223 autoloadAnimatedGifs: function autoloadAnimatedGifs() { 4224 FN.releaseNotes.loadAnimatedGifs(); // scroll event handler 4225 4226 $(window).on('scroll', FN.releaseNotes.loadAnimatedGifs); 4227 }, 4228 loadAnimatedGifs: function loadAnimatedGifs() { 4229 $('img[data-preview="true"]').each(function (i, item) { 4230 // is the image currently visible on the page / viewport? 4231 var visibleInViewport = FN.helpers.isVisibleInViewport({ 4232 element: item, 4233 minimumVisiblePercent: 40 4234 }); // is the image currently visible by CSS properties (note that we have to check the _parent_ element) 4235 4236 var visibleInCSS = $(item).parent().css(['opacity', 'visibility', 'display']); 4237 4238 if (visibleInViewport && visibleInCSS.opacity > 0 && visibleInCSS.visibility != 'hidden' && visibleInCSS.display != 'none') { 4239 var newSource = $(item).data('src'); 4240 $(item).attr('src', newSource); 4241 } 4242 }); 4243 }, 4244 initNavi: function initNavi() { 4245 $(FN.releaseNotesConfig.selectors.naviLinksSelector).on('click', function (event) { 4246 var parentContainer = $(this).closest('.' + FN.releaseNotesConfig.selectors.majorReleaseContainerClass); 4247 4248 if ($(this).hasClass(FN.releaseNotesConfig.selectors.naviLinkHighlightClass)) { 4249 // show highlights 4250 $('.' + FN.releaseNotesConfig.selectors.contentReleaseNotesClass).hide(); 4251 $('.' + FN.releaseNotesConfig.selectors.contentHighlightsClass).show(); // Set active state for "highlights" links 4252 4253 $(FN.releaseNotesConfig.selectors.naviLinksSelector).removeClass(FN.releaseNotesConfig.selectors.naviLinkActiveClass); 4254 $('.' + FN.releaseNotesConfig.selectors.naviLinkHighlightClass).addClass(FN.releaseNotesConfig.selectors.naviLinkActiveClass); 4255 } else { 4256 // show release notes 4257 $('.' + FN.releaseNotesConfig.selectors.contentHighlightsClass).hide(); 4258 $('.' + FN.releaseNotesConfig.selectors.contentReleaseNotesClass).show(); // Set active state for "release notes" links 4259 4260 $(FN.releaseNotesConfig.selectors.naviLinksSelector).removeClass(FN.releaseNotesConfig.selectors.naviLinkActiveClass); 4261 $('.' + FN.releaseNotesConfig.selectors.naviLinkReleaseNotesClass).addClass(FN.releaseNotesConfig.selectors.naviLinkActiveClass); 4262 } 4263 }); 4264 }, 4265 initChecksumButtons: function initChecksumButtons() { 4266 $(FN.releaseNotesConfig.selectors.checksumButtonSelector).on('click', function (event) { 4267 event.preventDefault(); 4268 var checksum = $(event.target).data('checksum'); // To copy the checksum to the clipboard, we create a temporary, 4269 // non-visible input element with the checksum as its value, add it to the 4270 // DOM, select its contents, and execute the "copy" command. The temporary 4271 // element is then removed from the DOM. 4272 4273 var tmpEl = $('<input>'); 4274 tmpEl.css({ 4275 position: 'absolute', 4276 left: '-1000%', 4277 opacity: '0' 4278 }); 4279 tmpEl.val(checksum); 4280 $('body').append(tmpEl); 4281 tmpEl[0].select(); 4282 document.execCommand("copy"); 4283 tmpEl.remove(); 4284 $(event.target).text($(event.target).text().replace('Copy', 'Copied')); 4285 }); 4286 } 4287}; 4288$(document).ready(function () { 4289 FN.releaseNotes.init(); 4290}); 4291if (typeof FN == 'undefined') FN = {}; 4292FN.educationOffer = { 4293 config: { 4294 selectors: { 4295 'educationPageId': 'page--education', 4296 'studentsPageId': 'page--students', 4297 'formId': 'education-offer-form', 4298 'emailFieldSelector': 'input[name="email"]', 4299 'requestNoticeSelector': '#education-offer-form__request-notice', 4300 'formRowClass': 'contact-form__row', 4301 'honeypotSelector': '#field-hp_consent' 4302 }, 4303 messages: { 4304 'progress': 'One moment, please: your request is in progress...', 4305 'invalidEmail': 'Please provide a valid email address!', 4306 'generalError': 'The request failed: please try again later or contact [email protected]!', 4307 'requiredFieldMissing': 'Please fill and check all required fields.', 4308 'alreadyVerified': 'It seems that this email address has already been approved for this offer.', 4309 'checkInbox': 'Thank you! We\'ll be in touch via email shortly.', 4310 'honeypotFilled': 'Please don\'t enter anything into the honeypot field.' 4311 }, 4312 constants: { 4313 'enableSubmitButton': 1001, 4314 'disableSubmitButton': 1002 4315 }, 4316 formTypes: { 4317 'student': { 4318 'showFields': ['email'], 4319 'requiredFields': ['email'] 4320 }, 4321 'teacher': { 4322 'showFields': ['email', 'info[user_name]', 'info[org_name]', 'info[org_type]', 'info[user_role]', 'info[license_usage]', 'info[notes]', 'legal', 'confirm_education'], 4323 'requiredFields': ['email', 'info[user_name]', 'info[org_name]', 'info[org_type]', 'info[user_role]', 'info[license_usage]', 'legal', 'confirm_education'] 4324 }, 4325 'school': { 4326 'showFields': ['email', 'info[user_name]', 'info[org_name]', 'info[org_type]', 'info[user_role]', 'info[license_users]', 'info[license_seats]', 'info[license_usage]', 'info[notes]', 'legal', 'confirm_education'], 4327 'requiredFields': ['email', 'info[user_name]', 'info[org_name]', 'info[org_type]', 'info[user_role]', 'info[license_users]', 'info[license_seats]', 'info[license_usage]', 'legal', 'confirm_education'] 4328 } 4329 } 4330 },
4331 init: function init() { 4332 if ($('#' + FN.educationOffer.config.selectors.educationPageId).length !== 1 && $('#' + FN.educationOffer.config.selectors.studentsPageId).length !== 1) return; 4333 $('#' + FN.educationOffer.config.selectors.formId + ' #form-type select').change(function () { 4334 FN.educationOffer.showFormType(); 4335 }); 4336 $('#' + FN.educationOffer.config.selectors.formId).on('submit', FN.educationOffer.sendEducationOfferRequest); 4337 }, 4338 sendEducationOfferRequest: function sendEducationOfferRequest(event) { 4339 event.preventDefault(); 4340 4341 var isFormValid = FN.educationOffer.__checkFormValidity(); 4342 4343 if (!isFormValid) return false; 4344 var formElement = $('#' + FN.educationOffer.config.selectors.formId); // notification that request is in progress... 4345 4346 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.progress, false); 4347 4348 FN.educationOffer.__toggleSubmitButton(FN.educationOffer.config.constants.disableSubmitButton); // ajax request 4349 4350 4351 $.ajax({ 4352 url: FN.config.urls.educationApi, 4353 method: 'POST', 4354 headers: { 4355 "Accept": "application/json", 4356 "Content-Type": "application/json" 4357 }, 4358 data: $(formElement).serialize(), 4359 dataType: 'json', 4360 success: function success(response) { 4361 if (response['status'] == 400) { 4362 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.invalidEmail, 'error'); 4363 4364 FN.educationOffer.__toggleSubmitButton(FN.educationOffer.config.constants.enableSubmitButton); 4365 } else if (response['status'] > 400) { 4366 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.generalError, 'error'); 4367 4368 FN.educationOffer.__toggleSubmitButton(FN.educationOffer.config.constants.enbleSubmitButton); 4369 } else if ('data' in response && 'status' in response.data && response.data.status === "verified") { 4370 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.alreadyVerified, 'success'); 4371 4372 FN.educationOffer.__toggleSubmitButton(FN.educationOffer.config.constants.disableSubmitButton); 4373 } else { 4374 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.checkInbox, 'success'); 4375 4376 FN.educationOffer.__toggleSubmitButton(FN.educationOffer.config.constants.disableSubmitButton); 4377 } // log event in Google Analytics 4378 4379 4380 var offerType = FN.educationOffer.__getFormTypeId() === 'student' ? 'student' : 'education'; 4381 FN.analytics.sendEventToGA({ 4382 'eventCategory': 'sales', 4383 'eventAction': offerType + '-offer-signup', 4384 'eventLabelSuffix': '' 4385 }); 4386 }, 4387 fail: function fail(data) { 4388 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.generalError, 'error'); 4389 4390 FN.educationOffer.__toggleSubmitButton(FN.educationOffer.config.constants.enableSubmitButton); 4391 } 4392 }); 4393 }, 4394 showFormType: function showFormType() { 4395 var formTypeId = FN.educationOffer.__getFormTypeId(); 4396 4397 var formConfig = FN.educationOffer.__getFormTypeConfig(); 4398 4399 if (!formConfig) return; // show fields 4400 4401 $('.' + FN.educationOffer.config.selectors.formRowClass).hide(); 4402 $('#form-type, #form-submit').show(); 4403 4404 for (var f = 0; f < formConfig.showFields.length; f++) { 4405 var fieldNameSelector = '#' + FN.educationOffer.config.selectors.formId + ' [name="' + formConfig.showFields[f] + '"]'; 4406 $(fieldNameSelector).closest('.' + FN.educationOffer.config.selectors.formRowClass).show(); 4407 } // prepare form fields 4408 4409 4410 if (formTypeId === 'teacher') { 4411 $('input[name="info[license_seats]"]').val("1"); 4412 } 4413 }, 4414 __getFormTypeId: function __getFormTypeId() { 4415 if ($('#' + FN.educationOffer.config.selectors.studentsPageId).length === 1) { 4416 var formTypeId = 'student'; 4417 } else { 4418 var formTypeId = $('#' + FN.educationOffer.config.selectors.formId + ' #form-type select').val(); 4419 } 4420 4421 return formTypeId; 4422 }, 4423 __getFormTypeConfig: function __getFormTypeConfig() { 4424 var formTypeId = FN.educationOffer.__getFormTypeId(); 4425 4426 var formTypeConfig = formTypeId in FN.educationOffer.config.formTypes ? FN.educationOffer.config.formTypes[formTypeId] : false;
4427 return formTypeConfig; 4428 }, 4429 __checkFormValidity: function __checkFormValidity() { 4430 if (FN.educationOffer.__honeypotFilled()) { 4431 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.honeypotFilled, 'error'); 4432 4433 return false; 4434 } // every visible field is 'required' 4435 4436 4437 var formTypeConfig = FN.educationOffer.__getFormTypeConfig(); 4438 4439 if (!formTypeConfig) return false; 4440 4441 for (var i = 0; i < formTypeConfig.requiredFields.length; i++) { 4442 var selector = '#' + FN.educationOffer.config.selectors.formId + " [name='" + formTypeConfig.requiredFields[i] + "']"; 4443 var field = $(selector); 4444 4445 if ($(field).is('input[type="text"]') || $(field).is('textarea')) { 4446 //----- text input // textarea 4447 var fieldVal = $(field).val(); 4448 4449 if (fieldVal === "") { 4450 // a required field is empty! 4451 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.requiredFieldMissing, 'error'); 4452 4453 return false; 4454 } 4455 4456 if ($(field).is('input[name="email"]')) { 4457 var emailFilter = /^.+@.+\..{2,10}$/; // check email address 4458 4459 if (!fieldVal || !emailFilter.test(fieldVal)) { 4460 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.invalidEmailMessage, 'error'); 4461 4462 return false; 4463 } 4464 } 4465 } else if ($(field).is('input[type="checkbox"]')) { 4466 //----- checkboxes 4467 var checked = $(field).prop('checked'); 4468 4469 if (!checked) { 4470 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.requiredFieldMissing, 'error'); 4471 4472 return false; 4473 } 4474 } else if ($(field).is('select')) { 4475 //----- select fields 4476 var fieldVal = $(field).val(); 4477 4478 if (!fieldVal) { 4479 // a required field is empty! 4480 FN.educationOffer.__showStatus(FN.educationOffer.config.messages.requiredFieldMissing, 'error'); 4481 4482 return false; 4483 } 4484 } 4485 } 4486 4487 return true; 4488 }, 4489 __honeypotFilled: function __honeypotFilled() { 4490 $field = $(FN.educationOffer.config.selectors.honeypotSelector); 4491 val = $field.val(); 4492 return val !== ""; 4493 }, 4494 __showStatus: function __showStatus(statusMessage, statusType) { 4495 var requestNoticeContainer = $(FN.educationOffer.config.selectors.requestNoticeSelector); 4496 $(requestNoticeContainer).html(statusMessage); 4497 $(requestNoticeContainer).removeClass('success error'); 4498 if (statusType) $(requestNoticeContainer).addClass(statusType); 4499 }, 4500 __toggleSubmitButton: function __toggleSubmitButton(onOrOff) { 4501 var disabled = onOrOff === FN.educationOffer.config.constants.disableSubmitButton ? true : false; 4502 $('#' + FN.educationOffer.config.selectors.formId + ' input[type="submit"]').prop('disabled', disabled); 4503 } 4504}; 4505$(window).on('load', function () { 4506 FN.educationOffer.init(); 4507}); 4508if (typeof FN == 'undefined') { 4509 FN = {}; 4510} 4511 4512FN.jobApplication = function () { 4513 var fields = ['name', 'email', 'position', 'remote', 'location', 'language', 'portfolio', 'person', 'projects', 'like-hate', 'why-tower', 'good-team', '10-years', 'anything-else']; 4514 var labels = { 4515 'name': 'Your name', 4516 'email': 'Your email', 4517 'position': 'Which position are you interested in?', 4518 'remote': 'Have you worked remotely before?', 4519 'location': 'Where are you located?', 4520 'language': 'Which language(s) do you speak?', 4521 'portfolio': 'Portfolio URL(s)', 4522 'person': 'Tell us a bit about yourself. Who are you? What are you passionate about?', 4523 'projects': 'Tell us about some of your recent projects.', 4524 'like-hate': "What do you like about your job? What don't you like?", 4525 'why-tower': 'Why are you interested in working at Tower?', 4526 'good-team': 'What characteristics must a "good" team have, in your opinion? What\'s important for you when working with other people?', 4527 '10-years': 'Can you imagine working at Tower for the next 10 years? What do you need in order to stay happy?', 4528 'anything-else': 'Anything else? Any questions for us?' 4529 }; 4530 var messages = {
4531 'success': 'Your application has been sent. Thank you for applying!', 4532 'error': 'Your application could not be sent. Please try again or contact us directly at [email protected].', 4533 'honeypotError': 'Please don\'t enter anything into the honeypot field.' 4534 }; 4535 var selectors = { 4536 form: '#job-application-form', 4537 button: 'input[type="submit"]', 4538 result: '.request-notice', 4539 honeypot: '#field-hp_consent' 4540 }; 4541 var elements = { 4542 form: null, 4543 button: null, 4544 result: null 4545 }; 4546 var apiURL = FN.config.urls.apiBase + FN.config.urls.apiJobApplication; 4547 4548 function init() { 4549 elements.form = document.querySelector(selectors.form); 4550 4551 if (elements.form) { 4552 elements.button = elements.form.querySelector(selectors.button); 4553 elements.result = elements.form.querySelector(selectors.result); 4554 elements.form.addEventListener('submit', submitJobApplication); 4555 } 4556 } 4557 4558 function getInternalMessage() { 4559 var data = {}; 4560 var message = ''; 4561 var key; 4562 fields.forEach(function (key) { 4563 data[key] = elements.form.elements[key].value.trim(); 4564 }); 4565 4566 for (key in data) { 4567 message += '#### ' + labels[key] + '\n\n' + data[key] + '\n\n'; 4568 } 4569 4570 return message.trim(); 4571 } 4572 4573 function getApplicantMessage() { 4574 var message = "Thank you so much for your interest in joining our team!\n\n" + "We are currently collecting all incoming applications before we approach a handful of candidates for an in-depth interview.\n\n" + "Here's a copy of what you've sent us:\n\n" + "================\n\n" + getInternalMessage(); 4575 return message.trim(); 4576 } 4577 4578 function displaySuccess() { 4579 elements.result.textContent = messages.success; 4580 elements.result.classList.remove('error'); 4581 elements.result.classList.add('success'); 4582 } 4583 4584 function displayError() { 4585 elements.result.textContent = messages.error; 4586 elements.result.classList.remove('success'); 4587 elements.result.classList.add('error'); 4588 } 4589 4590 function displayHoneypotError() { 4591 elements.result.textContent = messages.honeypotError; 4592 elements.result.classList.remove('success'); 4593 elements.result.classList.add('error'); 4594 } 4595 4596 function onRequestLoad() { 4597 elements.button.disabled = false; 4598 4599 if (this.status < 400) { 4600 displaySuccess(); // Clear form 4601 4602 fields.forEach(function (key) { 4603 console.log("clearing field", key); 4604 console.log(elements.form.elements[key]); 4605 elements.form.elements[key].value = ""; 4606 console.log(elements.form.elements[key].value); 4607 }); 4608 } else { 4609 // "Errors" which still result in a status code (like 404s) end up here 4610 displayError(); 4611 } 4612 } 4613 4614 function onRequestError() { 4615 elements.button.disabled = false; 4616 displayError(); 4617 } 4618 4619 function honeypotFilled() { 4620 $field = $(selectors.honeypot); 4621 val = $field.val(); 4622 return val !== ""; 4623 } 4624 4625 function submitJobApplication(event) { 4626 var data; 4627 var request; 4628 event.preventDefault(); 4629 elements.button.disabled = true; 4630 4631 if (honeypotFilled()) { 4632 elements.button.disabled = false; 4633 displayHoneypotError(); 4634 return; 4635 } // By this point, the input types and their "required" attributes have done 4636 // any necessary front-end validation. 4637 4638 4639 data = { 4640 applicantName: elements.form.elements['name'].value, 4641 applicantEmail: elements.form.elements['email'].value, 4642 messageInternal: getInternalMessage(), 4643 messageApplicant: getApplicantMessage() 4644 }; 4645 request = new XMLHttpRequest(); 4646 request.addEventListener('load', onRequestLoad); 4647 request.addEventListener('error', onRequestError); 4648 request.open('POST', apiURL); 4649 request.setRequestHeader('Content-Type', 'application/json'); 4650 request.send(JSON.stringify(data)); 4651 } 4652 4653 return { 4654 init: init 4655 }; 4656}(); 4657 4658document.addEventListener('DOMContentLoaded', function () { 4659 FN.jobApplication.init(); 4660}); 4661$(document).ready(function () { 4662 $('.job-opening__teaser a, .job-opening h3').on('click', function (event) { 4663 event.preventDefault(); 4664 var parentContainer = $(this).closest('.job-opening'); 4665 $(this).hide(); 4666 $(parentContainer).find('.job-opening__details').show(); 4667 }); 4668}); 4669//# sourceMappingURL=default.min.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.