PageSourceSearch

https://www.git-tower.com/assets/js/default.min.1790093363.js

js git-tower.com collected 2026-09-24 06:23:21 UTC 164,581 bytes, 4,669 lines download raw bytes

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, '&amp;').replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
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.