/* Minification failed. Returning unminified contents.
(7843,22-23): run-time error JS1014: Invalid character: `
(7843,25-26): run-time error JS1003: Expected ':': {
(7843,29-30): run-time error JS1003: Expected ':': }
(7843,31-32): run-time error JS1014: Invalid character: `
(7965,31-32): run-time error JS1014: Invalid character: `
(7967,1-2): run-time error JS1195: Expected expression: .
(7967,1-2): run-time error JS1195: Expected expression: .
(7970,9-10): run-time error JS1004: Expected ';': :
(7977,28-29): run-time error JS1197: Too many errors. The file might not be a JavaScript file: ,
 */
/*!
* Modernizr v2.0.6
* http://www.modernizr.com
*
* Copyright (c) 2009-2011 Faruk Ates, Paul Irish, Alex Sexton
* Dual-licensed under the BSD or MIT licenses: www.modernizr.com/license/
*/

/*
* Modernizr tests which native CSS3 and HTML5 features are available in
* the current UA and makes the results available to you in two ways:
* as properties on a global Modernizr object, and as classes on the
* <html> element. This information allows you to progressively enhance
* your pages with a granular level of control over the experience.
*
* Modernizr has an optional (not included) conditional resource loader
* called Modernizr.load(), based on Yepnope.js (yepnopejs.com).
* To get a build that includes Modernizr.load(), as well as choosing
* which tests to include, go to www.modernizr.com/download/
*
* Authors        Faruk Ates, Paul Irish, Alex Sexton, 
* Contributors   Ryan Seddon, Ben Alman
*/

window.Modernizr = (function (window, document, undefined) {

  var version = '2.0.6',

    Modernizr = {},

  // option for enabling the HTML classes to be added
    enableClasses = true,

    docElement = document.documentElement,
    docHead = document.head || document.getElementsByTagName('head')[0],

  /**
  * Create our "modernizr" element that we do most feature tests on.
  */
    mod = 'modernizr',
    modElem = document.createElement(mod),
    mStyle = modElem.style,

  /**
  * Create the input element for various Web Forms feature tests.
  */
    inputElem = document.createElement('input'),

    smile = ':)',

    toString = Object.prototype.toString,

  // List of property values to set for css tests. See ticket #21
    prefixes = ' -webkit- -moz- -o- -ms- -khtml- '.split(' '),

  // Following spec is to expose vendor-specific style properties as:
  //   elem.style.WebkitBorderRadius
  // and the following would be incorrect:
  //   elem.style.webkitBorderRadius

  // Webkit ghosts their properties in lowercase but Opera & Moz do not.
  // Microsoft foregoes prefixes entirely <= IE8, but appears to
  //   use a lowercase `ms` instead of the correct `Ms` in IE9

  // More here: http://github.com/Modernizr/Modernizr/issues/issue/21
    domPrefixes = 'Webkit Moz O ms Khtml'.split(' '),

    ns = { 'svg': 'http://www.w3.org/2000/svg' },

    tests = {},
    inputs = {},
    attrs = {},

    classes = [],

    featureName, // used in testing loop


  // Inject element with style element and some CSS rules
    injectElementWithStyles = function (rule, callback, nodes, testnames) {

      var style, ret, node,
          div = document.createElement('div');

      if (parseInt(nodes, 10)) {
        // In order not to give false positives we create a node for each test
        // This also allows the method to scale for unspecified uses
        while (nodes--) {
          node = document.createElement('div');
          node.id = testnames ? testnames[nodes] : mod + (nodes + 1);
          div.appendChild(node);
        }
      }

      // <style> elements in IE6-9 are considered 'NoScope' elements and therefore will be removed
      // when injected with innerHTML. To get around this you need to prepend the 'NoScope' element
      // with a 'scoped' element, in our case the soft-hyphen entity as it won't mess with our measurements.
      // http://msdn.microsoft.com/en-us/library/ms533897%28VS.85%29.aspx
      style = ['&shy;', '<style>', rule, '</style>'].join('');
      div.id = mod;
      div.innerHTML += style;
      docElement.appendChild(div);

      ret = callback(div, rule);
      div.parentNode.removeChild(div);

      return !!ret;

    },


  // adapted from matchMedia polyfill
  // by Scott Jehl and Paul Irish
  // gist.github.com/786768
    testMediaQuery = function (mq) {

      if (window.matchMedia) {
        return matchMedia(mq).matches;
      }

      var bool;

      injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function (node) {
        bool = (window.getComputedStyle ?
                  getComputedStyle(node, null) :
                  node.currentStyle)['position'] == 'absolute';
      });

      return bool;

    },


  /**
  * isEventSupported determines if a given element supports the given event
  * function from http://yura.thinkweb2.com/isEventSupported/
  */
    isEventSupported = (function () {

      var TAGNAMES = {
        'select': 'input', 'change': 'input',
        'submit': 'form', 'reset': 'form',
        'error': 'img', 'load': 'img', 'abort': 'img'
      };

      function isEventSupported(eventName, element) {

        element = element || document.createElement(TAGNAMES[eventName] || 'div');
        eventName = 'on' + eventName;

        // When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those
        var isSupported = eventName in element;

        if (!isSupported) {
          // If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element
          if (!element.setAttribute) {
            element = document.createElement('div');
          }
          if (element.setAttribute && element.removeAttribute) {
            element.setAttribute(eventName, '');
            isSupported = is(element[eventName], 'function');

            // If property was created, "remove it" (by setting value to `undefined`)
            if (!is(element[eventName], undefined)) {
              element[eventName] = undefined;
            }
            element.removeAttribute(eventName);
          }
        }

        element = null;
        return isSupported;
      }
      return isEventSupported;
    })();

  // hasOwnProperty shim by kangax needed for Safari 2.0 support
  var _hasOwnProperty = ({}).hasOwnProperty, hasOwnProperty;
  if (!is(_hasOwnProperty, undefined) && !is(_hasOwnProperty.call, undefined)) {
    hasOwnProperty = function (object, property) {
      return _hasOwnProperty.call(object, property);
    };
  }
  else {
    hasOwnProperty = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */
      return ((property in object) && is(object.constructor.prototype[property], undefined));
    };
  }

  /**
  * setCss applies given styles to the Modernizr DOM node.
  */
  function setCss(str) {
    mStyle.cssText = str;
  }

  /**
  * setCssAll extrapolates all vendor-specific css strings.
  */
  function setCssAll(str1, str2) {
    return setCss(prefixes.join(str1 + ';') + (str2 || ''));
  }

  /**
  * is returns a boolean for if typeof obj is exactly type.
  */
  function is(obj, type) {
    return typeof obj === type;
  }

  /**
  * contains returns a boolean for if substr is found within str.
  */
  function contains(str, substr) {
    return !! ~('' + str).indexOf(substr);
  }

  /**
  * testProps is a generic CSS / DOM property test; if a browser supports
  *   a certain property, it won't return undefined for it.
  *   A supported CSS property returns empty string when its not yet set.
  */
  function testProps(props, prefixed) {
    for (var i in props) {
      if (mStyle[props[i]] !== undefined) {
        return prefixed == 'pfx' ? props[i] : true;
      }
    }
    return false;
  }

  /**
  * testPropsAll tests a list of DOM properties we want to check against.
  *   We specify literally ALL possible (known and/or likely) properties on
  *   the element including the non-vendor prefixed one, for forward-
  *   compatibility.
  */
  function testPropsAll(prop, prefixed) {

    var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1),
            props = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' ');

    return testProps(props, prefixed);
  }

  /**
  * testBundle tests a list of CSS features that require element and style injection.
  *   By bundling them together we can reduce the need to touch the DOM multiple times.
  */
  /*>>testBundle*/
  var testBundle = (function (styles, tests) {
    var style = styles.join(''),
            len = tests.length;

    injectElementWithStyles(style, function (node, rule) {
      var style = document.styleSheets[document.styleSheets.length - 1],
      // IE8 will bork if you create a custom build that excludes both fontface and generatedcontent tests.
      // So we check for cssRules and that there is a rule available
      // More here: https://github.com/Modernizr/Modernizr/issues/288 & https://github.com/Modernizr/Modernizr/issues/293
                cssText = style.cssRules && style.cssRules[0] ? style.cssRules[0].cssText : style.cssText || "",
                children = node.childNodes, hash = {};

      while (len--) {
        hash[children[len].id] = children[len];
      }

      /*>>touch*/Modernizr['touch'] = ('ontouchstart' in window) || hash['touch'].offsetTop === 9; /*>>touch*/
      /*>>csstransforms3d*/Modernizr['csstransforms3d'] = hash['csstransforms3d'].offsetLeft === 9;          /*>>csstransforms3d*/
      /*>>generatedcontent*/Modernizr['generatedcontent'] = hash['generatedcontent'].offsetHeight >= 1;       /*>>generatedcontent*/
      /*>>fontface*/Modernizr['fontface'] = /src/i.test(cssText) &&
                                                                  cssText.indexOf(rule.split(' ')[0]) === 0;        /*>>fontface*/
    }, len, tests);

  })([
  // Pass in styles to be injected into document
  /*>>fontface*/'@font-face {font-family:"font";src:url("https://")}'         /*>>fontface*/

  /*>>touch*/, ['@media (', prefixes.join('touch-enabled),('), mod, ')',
                                '{#touch{top:9px;position:absolute}}'].join('')           /*>>touch*/

  /*>>csstransforms3d*/, ['@media (', prefixes.join('transform-3d),('), mod, ')',
                                '{#csstransforms3d{left:9px;position:absolute}}'].join('')/*>>csstransforms3d*/

  /*>>generatedcontent*/, ['#generatedcontent:after{content:"', smile, '";visibility:hidden}'].join('')  /*>>generatedcontent*/
    ],
      [
  /*>>fontface*/'fontface'          /*>>fontface*/
  /*>>touch*/, 'touch'            /*>>touch*/
  /*>>csstransforms3d*/, 'csstransforms3d'  /*>>csstransforms3d*/
  /*>>generatedcontent*/, 'generatedcontent' /*>>generatedcontent*/

    ]); /*>>testBundle*/


  /**
  * Tests
  * -----
  */

  tests['flexbox'] = function () {
    /**
    * setPrefixedValueCSS sets the property of a specified element
    * adding vendor prefixes to the VALUE of the property.
    * @param {Element} element
    * @param {string} property The property name. This will not be prefixed.
    * @param {string} value The value of the property. This WILL be prefixed.
    * @param {string=} extra Additional CSS to append unmodified to the end of
    * the CSS string.
    */
    function setPrefixedValueCSS(element, property, value, extra) {
      property += ':';
      element.style.cssText = (property + prefixes.join(value + ';' + property)).slice(0, -property.length) + (extra || '');
    }

    /**
    * setPrefixedPropertyCSS sets the property of a specified element
    * adding vendor prefixes to the NAME of the property.
    * @param {Element} element
    * @param {string} property The property name. This WILL be prefixed.
    * @param {string} value The value of the property. This will not be prefixed.
    * @param {string=} extra Additional CSS to append unmodified to the end of
    * the CSS string.
    */
    function setPrefixedPropertyCSS(element, property, value, extra) {
      element.style.cssText = prefixes.join(property + ':' + value + ';') + (extra || '');
    }

    var c = document.createElement('div'),
            elem = document.createElement('div');

    setPrefixedValueCSS(c, 'display', 'box', 'width:42px;padding:0;');
    setPrefixedPropertyCSS(elem, 'box-flex', '1', 'width:10px;');

    c.appendChild(elem);
    docElement.appendChild(c);

    var ret = elem.offsetWidth === 42;

    c.removeChild(elem);
    docElement.removeChild(c);

    return ret;
  };

  // On the S60 and BB Storm, getContext exists, but always returns undefined
  // http://github.com/Modernizr/Modernizr/issues/issue/97/

  tests['canvas'] = function () {
    var elem = document.createElement('canvas');
    return !!(elem.getContext && elem.getContext('2d'));
  };

  tests['canvastext'] = function () {
    return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function'));
  };

  // This WebGL test may false positive. 
  // But really it's quite impossible to know whether webgl will succeed until after you create the context. 
  // You might have hardware that can support a 100x100 webgl canvas, but will not support a 1000x1000 webgl 
  // canvas. So this feature inference is weak, but intentionally so.

  // It is known to false positive in FF4 with certain hardware and the iPad 2.

  tests['webgl'] = function () {
    return !!window.WebGLRenderingContext;
  };

  /*
  * The Modernizr.touch test only indicates if the browser supports
  *    touch events, which does not necessarily reflect a touchscreen
  *    device, as evidenced by tablets running Windows 7 or, alas,
  *    the Palm Pre / WebOS (touch) phones.
  *
  * Additionally, Chrome (desktop) used to lie about its support on this,
  *    but that has since been rectified: http://crbug.com/36415
  *
  * We also test for Firefox 4 Multitouch Support.
  *
  * For more info, see: http://modernizr.github.com/Modernizr/touch.html
  */

  tests['touch'] = function () {
    return Modernizr['touch'];
  };

  /**
  * geolocation tests for the new Geolocation API specification.
  *   This test is a standards compliant-only test; for more complete
  *   testing, including a Google Gears fallback, please see:
  *   http://code.google.com/p/geo-location-javascript/
  * or view a fallback solution using google's geo API:
  *   http://gist.github.com/366184
  */
  tests['geolocation'] = function () {
    return !!navigator.geolocation;
  };

  // Per 1.6:
  // This used to be Modernizr.crosswindowmessaging but the longer
  // name has been deprecated in favor of a shorter and property-matching one.
  // The old API is still available in 1.6, but as of 2.0 will throw a warning,
  // and in the first release thereafter disappear entirely.
  tests['postmessage'] = function () {
    return !!window.postMessage;
  };

  // Web SQL database detection is tricky:

  // In chrome incognito mode, openDatabase is truthy, but using it will
  //   throw an exception: http://crbug.com/42380
  // We can create a dummy database, but there is no way to delete it afterwards.

  // Meanwhile, Safari users can get prompted on any database creation.
  //   If they do, any page with Modernizr will give them a prompt:
  //   http://github.com/Modernizr/Modernizr/issues/closed#issue/113

  // We have chosen to allow the Chrome incognito false positive, so that Modernizr
  //   doesn't litter the web with these test databases. As a developer, you'll have
  //   to account for this gotcha yourself.
  tests['websqldatabase'] = function () {
    var result = !!window.openDatabase;
    /*  if (result){
    try {
    result = !!openDatabase( mod + "testdb", "1.0", mod + "testdb", 2e4);
    } catch(e) {
    }
    }  */
    return result;
  };

  // Vendors had inconsistent prefixing with the experimental Indexed DB:
  // - Webkit's implementation is accessible through webkitIndexedDB
  // - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB
  // For speed, we don't test the legacy (and beta-only) indexedDB
  tests['indexedDB'] = function () {
    for (var i = -1, len = domPrefixes.length; ++i < len; ) {
      if (window[domPrefixes[i].toLowerCase() + 'IndexedDB']) {
        return true;
      }
    }
    return !!window.indexedDB;
  };

  // documentMode logic from YUI to filter out IE8 Compat Mode
  //   which false positives.
  tests['hashchange'] = function () {
    return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7);
  };

  // Per 1.6:
  // This used to be Modernizr.historymanagement but the longer
  // name has been deprecated in favor of a shorter and property-matching one.
  // The old API is still available in 1.6, but as of 2.0 will throw a warning,
  // and in the first release thereafter disappear entirely.
  tests['history'] = function () {
    return !!(window.history && history.pushState);
  };

  tests['draganddrop'] = function () {
    return isEventSupported('dragstart') && isEventSupported('drop');
  };

  // Mozilla is targeting to land MozWebSocket for FF6
  // bugzil.la/659324
  tests['websockets'] = function () {
    for (var i = -1, len = domPrefixes.length; ++i < len; ) {
      if (window[domPrefixes[i] + 'WebSocket']) {
        return true;
      }
    }
    return 'WebSocket' in window;
  };


  // http://css-tricks.com/rgba-browser-support/
  tests['rgba'] = function () {
    // Set an rgba() color and check the returned value

    setCss('background-color:rgba(150,255,150,.5)');

    return contains(mStyle.backgroundColor, 'rgba');
  };

  tests['hsla'] = function () {
    // Same as rgba(), in fact, browsers re-map hsla() to rgba() internally,
    //   except IE9 who retains it as hsla

    setCss('background-color:hsla(120,40%,100%,.5)');

    return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla');
  };

  tests['multiplebgs'] = function () {
    // Setting multiple images AND a color on the background shorthand property
    //  and then querying the style.background property value for the number of
    //  occurrences of "url(" is a reliable method for detecting ACTUAL support for this!

    setCss('background:url(https://),url(https://),red url(https://)');

    // If the UA supports multiple backgrounds, there should be three occurrences
    //   of the string "url(" in the return value for elemStyle.background

    return /(url\s*\(.*?){3}/.test(mStyle.background);
  };


  // In testing support for a given CSS property, it's legit to test:
  //    `elem.style[styleName] !== undefined`
  // If the property is supported it will return an empty string,
  // if unsupported it will return undefined.

  // We'll take advantage of this quick test and skip setting a style
  // on our modernizr element, but instead just testing undefined vs
  // empty string.


  tests['backgroundsize'] = function () {
    return testPropsAll('backgroundSize');
  };

  tests['borderimage'] = function () {
    return testPropsAll('borderImage');
  };


  // Super comprehensive table about all the unique implementations of
  // border-radius: http://muddledramblings.com/table-of-css3-border-radius-compliance

  tests['borderradius'] = function () {
    return testPropsAll('borderRadius');
  };

  // WebOS unfortunately false positives on this test.
  tests['boxshadow'] = function () {
    return testPropsAll('boxShadow');
  };

  // FF3.0 will false positive on this test
  tests['textshadow'] = function () {
    return document.createElement('div').style.textShadow === '';
  };


  tests['opacity'] = function () {
    // Browsers that actually have CSS Opacity implemented have done so
    //  according to spec, which means their return values are within the
    //  range of [0.0,1.0] - including the leading zero.

    setCssAll('opacity:.55');

    // The non-literal . in this regex is intentional:
    //   German Chrome returns this value as 0,55
    // https://github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632
    return /^0.55$/.test(mStyle.opacity);
  };


  tests['cssanimations'] = function () {
    return testPropsAll('animationName');
  };


  tests['csscolumns'] = function () {
    return testPropsAll('columnCount');
  };


  tests['cssgradients'] = function () {
    /**
    * For CSS Gradients syntax, please see:
    * http://webkit.org/blog/175/introducing-css-gradients/
    * https://developer.mozilla.org/en/CSS/-moz-linear-gradient
    * https://developer.mozilla.org/en/CSS/-moz-radial-gradient
    * http://dev.w3.org/csswg/css3-images/#gradients-
    */

    var str1 = 'background-image:',
            str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));',
            str3 = 'linear-gradient(left top,#9f9, white);';

    setCss(
            (str1 + prefixes.join(str2 + str1) + prefixes.join(str3 + str1)).slice(0, -str1.length)
        );

    return contains(mStyle.backgroundImage, 'gradient');
  };


  tests['cssreflections'] = function () {
    return testPropsAll('boxReflect');
  };


  tests['csstransforms'] = function () {
    return !!testProps(['transformProperty', 'WebkitTransform', 'MozTransform', 'OTransform', 'msTransform']);
  };


  tests['csstransforms3d'] = function () {

    var ret = !!testProps(['perspectiveProperty', 'WebkitPerspective', 'MozPerspective', 'OPerspective', 'msPerspective']);

    // Webkit’s 3D transforms are passed off to the browser's own graphics renderer.
    //   It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in
    //   some conditions. As a result, Webkit typically recognizes the syntax but
    //   will sometimes throw a false positive, thus we must do a more thorough check:
    if (ret && 'webkitPerspective' in docElement.style) {

      // Webkit allows this media query to succeed only if the feature is enabled.
      // `@media (transform-3d),(-o-transform-3d),(-moz-transform-3d),(-ms-transform-3d),(-webkit-transform-3d),(modernizr){ ... }`
      ret = Modernizr['csstransforms3d'];
    }
    return ret;
  };


  tests['csstransitions'] = function () {
    return testPropsAll('transitionProperty');
  };


  /*>>fontface*/
  // @font-face detection routine by Diego Perini
  // http://javascript.nwbox.com/CSSSupport/
  tests['fontface'] = function () {
    return Modernizr['fontface'];
  };
  /*>>fontface*/

  // CSS generated content detection
  tests['generatedcontent'] = function () {
    return Modernizr['generatedcontent'];
  };



  // These tests evaluate support of the video/audio elements, as well as
  // testing what types of content they support.
  //
  // We're using the Boolean constructor here, so that we can extend the value
  // e.g.  Modernizr.video     // true
  //       Modernizr.video.ogg // 'probably'
  //
  // Codec values from : http://github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845
  //                     thx to NielsLeenheer and zcorpan

  // Note: in FF 3.5.1 and 3.5.0, "no" was a return value instead of empty string.
  //   Modernizr does not normalize for that.

  tests['video'] = function () {
    var elem = document.createElement('video'),
            bool = false;

    // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224
    try {
      if (bool = !!elem.canPlayType) {
        bool = new Boolean(bool);
        bool.ogg = elem.canPlayType('video/ogg; codecs="theora"');

        // Workaround required for IE9, which doesn't report video support without audio codec specified.
        //   bug 599718 @ msft connect
        var h264 = 'video/mp4; codecs="avc1.42E01E';
        bool.h264 = elem.canPlayType(h264 + '"') || elem.canPlayType(h264 + ', mp4a.40.2"');

        bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"');
      }

    } catch (e) { }

    return bool;
  };

  tests['audio'] = function () {
    var elem = document.createElement('audio'),
            bool = false;

    try {
      if (bool = !!elem.canPlayType) {
        bool = new Boolean(bool);
        bool.ogg = elem.canPlayType('audio/ogg; codecs="vorbis"');
        bool.mp3 = elem.canPlayType('audio/mpeg;');

        // Mimetypes accepted:
        //   https://developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements
        //   http://bit.ly/iphoneoscodecs
        bool.wav = elem.canPlayType('audio/wav; codecs="1"');
        bool.m4a = elem.canPlayType('audio/x-m4a;') || elem.canPlayType('audio/aac;');
      }
    } catch (e) { }

    return bool;
  };


  // Firefox has made these tests rather unfun.

  // In FF4, if disabled, window.localStorage should === null.

  // Normally, we could not test that directly and need to do a
  //   `('localStorage' in window) && ` test first because otherwise Firefox will
  //   throw http://bugzil.la/365772 if cookies are disabled

  // However, in Firefox 4 betas, if dom.storage.enabled == false, just mentioning
  //   the property will throw an exception. http://bugzil.la/599479
  // This looks to be fixed for FF4 Final.

  // Because we are forced to try/catch this, we'll go aggressive.

  // FWIW: IE8 Compat mode supports these features completely:
  //   http://www.quirksmode.org/dom/html5.html
  // But IE8 doesn't support either with local files

  tests['localstorage'] = function () {
    try {
      return !!localStorage.getItem;
    } catch (e) {
      return false;
    }
  };

  tests['sessionstorage'] = function () {
    try {
      return !!sessionStorage.getItem;
    } catch (e) {
      return false;
    }
  };


  tests['webworkers'] = function () {
    return !!window.Worker;
  };


  tests['applicationcache'] = function () {
    return !!window.applicationCache;
  };


  // Thanks to Erik Dahlstrom
  tests['svg'] = function () {
    return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect;
  };

  // specifically for SVG inline in HTML, not within XHTML
  // test page: paulirish.com/demo/inline-svg
  tests['inlinesvg'] = function () {
    var div = document.createElement('div');
    div.innerHTML = '<svg/>';
    return (div.firstChild && div.firstChild.namespaceURI) == ns.svg;
  };

  // Thanks to F1lt3r and lucideer, ticket #35
  tests['smil'] = function () {
    return !!document.createElementNS && /SVG/.test(toString.call(document.createElementNS(ns.svg, 'animate')));
  };

  tests['svgclippaths'] = function () {
    // Possibly returns a false positive in Safari 3.2?
    return !!document.createElementNS && /SVG/.test(toString.call(document.createElementNS(ns.svg, 'clipPath')));
  };

  // input features and input types go directly onto the ret object, bypassing the tests loop.
  // Hold this guy to execute in a moment.
  function webforms() {
    // Run through HTML5's new input attributes to see if the UA understands any.
    // We're using f which is the <input> element created early on
    // Mike Taylr has created a comprehensive resource for testing these attributes
    //   when applied to all input types:
    //   http://miketaylr.com/code/input-type-attr.html
    // spec: http://www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary

    // Only input placeholder is tested while textarea's placeholder is not. 
    // Currently Safari 4 and Opera 11 have support only for the input placeholder
    // Both tests are available in feature-detects/forms-placeholder.js
    Modernizr['input'] = (function (props) {
      for (var i = 0, len = props.length; i < len; i++) {
        attrs[props[i]] = !!(props[i] in inputElem);
      }
      return attrs;
    })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' '));

    // Run through HTML5's new input types to see if the UA understands any.
    //   This is put behind the tests runloop because it doesn't return a
    //   true/false like all the other tests; instead, it returns an object
    //   containing each input type with its corresponding true/false value

    // Big thanks to @miketaylr for the html5 forms expertise. http://miketaylr.com/
    Modernizr['inputtypes'] = (function (props) {

      for (var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++) {

        inputElem.setAttribute('type', inputElemType = props[i]);
        bool = inputElem.type !== 'text';

        // We first check to see if the type we give it sticks..
        // If the type does, we feed it a textual value, which shouldn't be valid.
        // If the value doesn't stick, we know there's input sanitization which infers a custom UI
        if (bool) {

          inputElem.value = smile;
          inputElem.style.cssText = 'position:absolute;visibility:hidden;';

          if (/^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined) {

            docElement.appendChild(inputElem);
            defaultView = document.defaultView;

            // Safari 2-4 allows the smiley as a value, despite making a slider
            bool = defaultView.getComputedStyle &&
                              defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' &&
            // Mobile android web browser has false positive, so must
            // check the height to see if the widget is actually there.
                              (inputElem.offsetHeight !== 0);

            docElement.removeChild(inputElem);

          } else if (/^(search|tel)$/.test(inputElemType)) {
            // Spec doesnt define any special parsing or detectable UI
            //   behaviors so we pass these through as true

            // Interestingly, opera fails the earlier test, so it doesn't
            //  even make it here.

          } else if (/^(url|email)$/.test(inputElemType)) {
            // Real url and email support comes with prebaked validation.
            bool = inputElem.checkValidity && inputElem.checkValidity() === false;

          } else if (/^color$/.test(inputElemType)) {
            // chuck into DOM and force reflow for Opera bug in 11.00
            // github.com/Modernizr/Modernizr/issues#issue/159
            docElement.appendChild(inputElem);
            docElement.offsetWidth;
            bool = inputElem.value != smile;
            docElement.removeChild(inputElem);

          } else {
            // If the upgraded input compontent rejects the :) text, we got a winner
            bool = inputElem.value != smile;
          }
        }

        inputs[props[i]] = !!bool;
      }
      return inputs;
    })('search tel url email datetime date month week time datetime-local number range color'.split(' '));
  }


  // End of test definitions
  // -----------------------



  // Run through all tests and detect their support in the current UA.
  // todo: hypothetically we could be doing an array of tests and use a basic loop here.
  for (var feature in tests) {
    if (hasOwnProperty(tests, feature)) {
      // run the test, throw the return value into the Modernizr,
      //   then based on that boolean, define an appropriate className
      //   and push it into an array of classes we'll join later.
      featureName = feature.toLowerCase();
      Modernizr[featureName] = tests[feature]();

      classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);
    }
  }

  // input tests need to run.
  Modernizr.input || webforms();


  /**
  * addTest allows the user to define their own feature tests
  * the result will be added onto the Modernizr object,
  * as well as an appropriate className set on the html element
  *
  * @param feature - String naming the feature
  * @param test - Function returning true if feature is supported, false if not
  */
  Modernizr.addTest = function (feature, test) {
    if (typeof feature == "object") {
      for (var key in feature) {
        if (hasOwnProperty(feature, key)) {
          Modernizr.addTest(key, feature[key]);
        }
      }
    } else {

      feature = feature.toLowerCase();

      if (Modernizr[feature] !== undefined) {
        // we're going to quit if you're trying to overwrite an existing test
        // if we were to allow it, we'd do this:
        //   var re = new RegExp("\\b(no-)?" + feature + "\\b");  
        //   docElement.className = docElement.className.replace( re, '' );
        // but, no rly, stuff 'em.
        return;
      }

      test = typeof test == "boolean" ? test : !!test();

      docElement.className += ' ' + (test ? '' : 'no-') + feature;
      Modernizr[feature] = test;

    }

    return Modernizr; // allow chaining.
  };


  // Reset modElem.cssText to nothing to reduce memory footprint.
  setCss('');
  modElem = inputElem = null;

  //>>BEGIN IEPP
  // Enable HTML 5 elements for styling (and printing) in IE.
  if (window.attachEvent && (function () {
    var elem = document.createElement('div');
    elem.innerHTML = '<elem></elem>';
    return elem.childNodes.length !== 1;
  })()) {

    // iepp v2 by @jon_neal & afarkas : github.com/aFarkas/iepp/
    (function (win, doc) {
      win.iepp = win.iepp || {};
      var iepp = win.iepp,
            elems = iepp.html5elements || 'abbr|article|aside|audio|canvas|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video',
            elemsArr = elems.split('|'),
            elemsArrLen = elemsArr.length,
            elemRegExp = new RegExp('(^|\\s)(' + elems + ')', 'gi'),
            tagRegExp = new RegExp('<(\/*)(' + elems + ')', 'gi'),
            filterReg = /^\s*[\{\}]\s*$/,
            ruleRegExp = new RegExp('(^|[^\\n]*?\\s)(' + elems + ')([^\\n]*)({[\\n\\w\\W]*?})', 'gi'),
            docFrag = doc.createDocumentFragment(),
            html = doc.documentElement,
            head = html.firstChild,
            bodyElem = doc.createElement('body'),
            styleElem = doc.createElement('style'),
            printMedias = /print|all/,
            body;
      function shim(doc) {
        var a = -1;
        while (++a < elemsArrLen)
        // Use createElement so IE allows HTML5-named elements in a document
          doc.createElement(elemsArr[a]);
      }

      iepp.getCSS = function (styleSheetList, mediaType) {
        if (styleSheetList + '' === undefined) { return ''; }
        var a = -1,
              len = styleSheetList.length,
              styleSheet,
              cssTextArr = [];
        while (++a < len) {
          styleSheet = styleSheetList[a];
          //currently no test for disabled/alternate stylesheets
          if (styleSheet.disabled) { continue; }
          mediaType = styleSheet.media || mediaType;
          // Get css from all non-screen stylesheets and their imports
          if (printMedias.test(mediaType)) cssTextArr.push(iepp.getCSS(styleSheet.imports, mediaType), styleSheet.cssText);
          //reset mediaType to all with every new *not imported* stylesheet
          mediaType = 'all';
        }
        return cssTextArr.join('');
      };

      iepp.parseCSS = function (cssText) {
        var cssTextArr = [],
              rule;
        while ((rule = ruleRegExp.exec(cssText)) != null) {
          // Replace all html5 element references with iepp substitute classnames
          cssTextArr.push(((filterReg.exec(rule[1]) ? '\n' : rule[1]) + rule[2] + rule[3]).replace(elemRegExp, '$1.iepp_$2') + rule[4]);
        }
        return cssTextArr.join('\n');
      };

      iepp.writeHTML = function () {
        var a = -1;
        body = body || doc.body;
        while (++a < elemsArrLen) {
          var nodeList = doc.getElementsByTagName(elemsArr[a]),
                nodeListLen = nodeList.length,
                b = -1;
          while (++b < nodeListLen)
            if (nodeList[b].className.indexOf('iepp_') < 0)
            // Append iepp substitute classnames to all html5 elements
              nodeList[b].className += ' iepp_' + elemsArr[a];
        }
        docFrag.appendChild(body);
        html.appendChild(bodyElem);
        // Write iepp substitute print-safe document
        bodyElem.className = body.className;
        bodyElem.id = body.id;
        // Replace HTML5 elements with <font> which is print-safe and shouldn't conflict since it isn't part of html5
        bodyElem.innerHTML = body.innerHTML.replace(tagRegExp, '<$1font');
      };


      iepp._beforePrint = function () {
        // Write iepp custom print CSS
        styleElem.styleSheet.cssText = iepp.parseCSS(iepp.getCSS(doc.styleSheets, 'all'));
        iepp.writeHTML();
      };

      iepp.restoreHTML = function () {
        // Undo everything done in onbeforeprint
        bodyElem.innerHTML = '';
        html.removeChild(bodyElem);
        html.appendChild(body);
      };

      iepp._afterPrint = function () {
        // Undo everything done in onbeforeprint
        iepp.restoreHTML();
        styleElem.styleSheet.cssText = '';
      };



      // Shim the document and iepp fragment
      shim(doc);
      shim(docFrag);

      //
      if (iepp.disablePP) { return; }

      // Add iepp custom print style element
      head.insertBefore(styleElem, head.firstChild);
      styleElem.media = 'print';
      styleElem.className = 'iepp-printshim';
      win.attachEvent(
            'onbeforeprint',
            iepp._beforePrint
          );
      win.attachEvent(
            'onafterprint',
            iepp._afterPrint
          );
    })(window, document);
  }
  //>>END IEPP

  // Assign private properties to the return object with prefix
  Modernizr._version = version;

  // expose these for the plugin API. Look in the source for how to join() them against your input
  Modernizr._prefixes = prefixes;
  Modernizr._domPrefixes = domPrefixes;

  // Modernizr.mq tests a given media query, live against the current state of the window
  // A few important notes:
  //   * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false
  //   * A max-width or orientation query will be evaluated against the current state, which may change later.
  //   * You must specify values. Eg. If you are testing support for the min-width media query use: 
  //       Modernizr.mq('(min-width:0)')
  // usage:
  // Modernizr.mq('only screen and (max-width:768)')
  Modernizr.mq = testMediaQuery;

  // Modernizr.hasEvent() detects support for a given event, with an optional element to test on
  // Modernizr.hasEvent('gesturestart', elem)
  Modernizr.hasEvent = isEventSupported;

  // Modernizr.testProp() investigates whether a given style property is recognized
  // Note that the property names must be provided in the camelCase variant.
  // Modernizr.testProp('pointerEvents')
  Modernizr.testProp = function (prop) {
    return testProps([prop]);
  };

  // Modernizr.testAllProps() investigates whether a given style property,
  //   or any of its vendor-prefixed variants, is recognized
  // Note that the property names must be provided in the camelCase variant.
  // Modernizr.testAllProps('boxSizing')    
  Modernizr.testAllProps = testPropsAll;



  // Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards
  // Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... })
  Modernizr.testStyles = injectElementWithStyles;


  // Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input
  // Modernizr.prefixed('boxSizing') // 'MozBoxSizing'

  // Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style.
  // Return values will also be the camelCase variant, if you need to translate that to hypenated style use:
  //
  //     str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-');

  // If you're trying to ascertain which transition end event to bind to, you might do something like...
  // 
  //     var transEndEventNames = {
  //       'WebkitTransition' : 'webkitTransitionEnd',
  //       'MozTransition'    : 'transitionend',
  //       'OTransition'      : 'oTransitionEnd',
  //       'msTransition'     : 'msTransitionEnd', // maybe?
  //       'transition'       : 'transitionEnd'
  //     },
  //     transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];

  Modernizr.prefixed = function (prop) {
    return testPropsAll(prop, 'pfx');
  };



  // Remove "no-js" class from <html> element, if it exists:
  docElement.className = docElement.className.replace(/\bno-js\b/, '')

  // Add the new classes to the <html> element.
                            + (enableClasses ? ' js ' + classes.join(' ') : '');

  return Modernizr;

})(this, this.document);;
/*globals */

// application root namespace

var wng = (function() {
  var 
    version = "1.0.0";

    return {
      version: version
    };
}());
;
/*globals window:false, document:false, FB:false, wng:true */

// Options available: 
//  {
//   'url': null,
//   'preliminaryUrl': null,
//   'brand': null,
//   'enable_cross_domain_tracking': null,
//   'quote': {
//     'countries': null,
//     'price': null, // Full dollar value as int
//     'plan': null,
//     'campaign_code': null,
//     'campaign_code_discount': null, 
//     'duration': null,
//     'ages': null
//    },
//    'policy': {
//      'policyNumber': null
//    },
//  }

var _gaq = _gaq || [];
var dataLayer = dataLayer || [];

var wng = (function ($, undefined) {
  var opt, transactionID, groupedDuration, groupedAges, singleOrFamily, orderedCountries, limitedCountries, allSubdomains;

  var trackPageOnNextLoadCookieName = 'trackPageOnNextLoad';
  var trackEventOnNextLoadCookiePrefix = 'trackEventOnNextLoad';
  var trackEventOnNextLoadCounter = 1;
  var googleAnalyticsScriptLoaded = false;

  $.googleAnalytics = function (googleAnalyticsTrackingCode, googleTagManagerCode, options) {
    opt = options || {};
    $.googleAnalytics.options = opt;

    allSubdomains = document.location.hostname.replace(/^www|service/, '');

    // Set the tracking code
    _gaq.push(['_setAccount', googleAnalyticsTrackingCode]);

    _gaq.push(function () {
      googleAnalyticsScriptLoaded = true;
    });

    // Set the domain to .travelinsurancedirect.suffix
    _gaq.push(['_setDomainName', allSubdomains]);

    // Enable cross domain tracking
    if (opt.enable_cross_domain_tracking) {
      _gaq.push(['_setAllowLinker', true]);
    }

    // Set custom variable for brand
    if (opt.brand) {
      _gaq.push(['_setCustomVar', 5, 'Brand', opt.brand, 2]);
    }

    // Set custom variables for quote
    if (opt.quote) {
      // Plan and countries
      if (opt.quote.countries) {
        orderedCountries = utils.orderCountries(opt.quote.countries);
        limitedCountries = utils.limitCountries(orderedCountries);
        var plan = opt.quote.plan || 'No Plan';
        // Custom variable requires limited countries as custom variables have a max length: Documented 64 bites for Key and Value, but up to 128 seems fine
        _gaq.push(['_setCustomVar', 1, plan, limitedCountries, 2]);
      }

      // Duration
      if (opt.quote.duration) {
        groupedDuration = utils.groupDuration(opt.quote.duration);
        // Key is 
        _gaq.push(['_setCustomVar', 2, groupedDuration, String(opt.quote.duration), 2]);
      }

      // Ages 
      if (opt.quote.ages) {
        groupedAges = utils.groupAges(opt.quote.ages);

        // Technically they are already sorted as utils.groupAges performs a sort on the ages object, but if that goes these still should be sorted, and if that method is used stand alone it should sort as well.
        opt.quote.ages.sort(function (a, b) {
          return a - b;
        });

        singleOrFamily = (opt.quote.ages.length === 1) ? 'Single' : 'Family';
        _gaq.push(['_setCustomVar', 3, groupedAges, opt.quote.ages.join(', '), 2]);
      }

      // Campaign code
      if (opt.quote.campaign_code && opt.quote.campaign_code_discount) {
        _gaq.push(['_setCustomVar', 4, opt.quote.campaign_code_discount, opt.quote.campaign_code, 2]);
      } else {
        _gaq.push(['_setCustomVar', 4, 'No Discount', 'None', 2]);
      }
    }

    // Up the sitespeed sampling rate from 1% to 100% - note that the current maximum google gives 
    // you is 10%.
    _gaq.push(['_setSiteSpeedSampleRate', 10]);

    // Track any urls passed in from the previous page
    var urlFromLastPage = utils.getCookie(trackPageOnNextLoadCookieName);

    if (urlFromLastPage) {
      _gaq.push(['_trackPageview', urlFromLastPage]);
      utils.deleteCookie(trackPageOnNextLoadCookieName);
    }

    // Track the preliminaryUrl if it exists
    if (opt.preliminaryUrl) {
      _gaq.push(['_trackPageview', opt.preliminaryUrl]);
    }

    // Track the initial page load
    if (opt.url) {
      _gaq.push(['_trackPageview', opt.url]);
    } else {
      _gaq.push(['_trackPageview']);
    }

    // Load GA javascript asynchronously
    var ga = document.createElement('script');
    ga.type = 'text/javascript';
    ga.async = true;
    ga.src = ('https:' == document.location.protocol ? 'https://' : 'http://') + 'stats.g.doubleclick.net/dc.js';
    var s = document.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(ga, s);

    // Track any events passed in from the previous page
    var eventsFromLastPage = utils.findCookies(new RegExp("^" + trackEventOnNextLoadCookiePrefix));

    for (var key in eventsFromLastPage) {
      var decodedEvent = utils.decodeEvent(eventsFromLastPage[key]);
      wng.googleAnalytics.trackEvent.apply(this, decodedEvent);
      utils.deleteCookie(key);
    }

    // Set the fbAsyncInit value to be a callback that tracks FB social actions
    window.fbAsyncInit = function () {
      wng.googleAnalytics.listenForFacebookSocialEvents();
    };

    // Set up for Google Tag Manager
    dataLayer.push({ 'Custom Tracking URL': opt.url });

    if (opt.preliminaryUrl) {
      dataLayer.push({ 'Preliminary URL': opt.preliminaryUrl });
    }

    if (opt.quote) {
      // Send quote destinations to Google Tag Manager
      if (opt.quote.countries) {
        dataLayer.push({ 'Destination Countries': utils.limitCountries(utils.orderCountries(opt.quote.countries)) });
      }

      // Send price to Google Tag Manager
      if (opt.quote.price) {
        dataLayer.push({ 'Quote Price': Number(opt.quote.price) });
      }
    }

    if (opt.policy) {
        if (opt.policy.policyNumber) {
            dataLayer.push({ 'Policy Number': opt.policy.policyNumber });
        }
    }

    if (opt.quote && opt.policy) {
        if (opt.quote.price && opt.policy.policyNumber) {
            dataLayer.push({
                'ecommerce': {
                    'purchase': {
                        'id': opt.policy.policyNumber,
                        'revenue': opt.quote.price,
                        'coupon': opt.quote.campaign_code
                    }
                }
            });
        }
    }

    // Load Google Tag Manager
    (function (w, d, s, l, i) {
      w[l] = w[l] || [];
      w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
      var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true;
      j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl;
      f.parentNode.insertBefore(j, f);
    })(window, document, 'script', 'dataLayer', googleTagManagerCode);
  };

  // Track a virtual page, this is in addition to the inital page tracking
  $.googleAnalytics.trackVirtualPage = function (url) {
    _gaq.push(['_trackPageview', url]);
  };

  // Track a virtual page, but not until just before the next page is tracked.
  // Useful for when the page will unload before a request can be sent to Google Analytics.
  // Note that there can only be one page tracked this way.
  $.googleAnalytics.trackVirtualPageOnNextLoad = function (url) {
    utils.setCookie(trackPageOnNextLoadCookieName, url);
  };

  // Track an event
  // Note: the value option must be an integer
  $.googleAnalytics.trackEvent = function (category, action, eventOptions) {
    // Options available:
    // {
    //   'label': null,
    //   'value': null
    // }

    if (eventOptions == undefined) {
      eventOptions = {};
    }

    _gaq.push(['_trackEvent', category, action, eventOptions['label'], eventOptions['value']]);
  };


  // Track an event, but not until just before the next page is tracked.
  // Useful for when the page will unload before a request can be sent to Google Analytics.
  $.googleAnalytics.trackEventOnNextLoad = function (category, action, eventOptions) {
    var encodedEvent = utils.encodeEvent(category, action, eventOptions);
    var cookieName = trackEventOnNextLoadCookiePrefix + trackEventOnNextLoadCounter;
    utils.setCookie(cookieName, encodedEvent);
    trackEventOnNextLoadCounter++;
  };

  // Create a transaction. Note order ID should be unique and allow the looking up of the order in the DB. Total should just be an int.
  $.googleAnalytics.createTransaction = function (orderID, total) {
    transactionID = orderID;
    _gaq.push(['_addTrans',
      String(transactionID),    // order ID - required
      '',                       // affiliation or store name
      String(total),            // total - required
      '0',                      // tax
      '0',                      // shipping
      '',                       // city
      '',                       // state or province
      ''                        // country
    ]);
  };

  // Add policy details to a transaction, this method is required. singleOrFamily should be 'Single' or 'Family', both days
  // and price should be an int 
  $.googleAnalytics.addPolicyDetails = function (price) {
    var variation = singleOrFamily + ' ' + groupedDuration;

    _gaq.push(['_addItem',
      String(transactionID),  // order ID - required
      variation,              // SKU/code - required
      'Policy',               // product name - not required by spec but appears to be required anyway
      orderedCountries,       // category or variation
      String(price),          // unit price - required
      '1'                     // quantity - required
    ]);
  };

  // Add policy excess reduction, both newPolicyExcess and price should be an int
  $.googleAnalytics.addPolicyExcessReduction = function (newPolicyExcess, price) {
    _gaq.push(['_addItem',
      String(transactionID),                      // order ID - required
      '$' + newPolicyExcess + ' policy excess',   // SKU/code - required
      'Policy excess reduction',                  // product name - not required by spec but appears to be required anyway
      orderedCountries,                           // category or variation
      String(price),                              // unit price - required
      '1'                                         // quantity - required
    ]);
  };

  // Add car rental excess cover increase, both newExcessCover and price should be an int  
  $.googleAnalytics.addCarRentalExcessIncrease = function (newExcessCover, price) {
    _gaq.push(['_addItem',
      String(transactionID),                  // order ID - required
      '$' + newExcessCover + ' car excess',   // SKU/code - required
      'Car excess cover increased',           // product name - not required by spec but appears to be required anyway
      orderedCountries,                       // category or variation
      String(price),                          // unit price - required
      '1'                                     // quantity - required
    ]);
  };

  // Add aditional item, both value and price should be an int, description is a string of what the user entered as 'item name'
  $.googleAnalytics.addAdditionalItem = function (value, description, price) {
    _gaq.push(['_addItem',
      String(transactionID),            // order ID - required
      '$' + value + ' ' + description,  // SKU/code - required
      'Additional item',                // product name - not required by spec but appears to be required anyway
      orderedCountries,                 // category or variation
      String(price),                    // unit price - required
      '1'                               // quantity - required
    ]);
  };

  // Add Footprints, price should be an int, projectName is a string
  $.googleAnalytics.addFootprints = function (projectName, price) {
    _gaq.push(['_addItem',
      String(transactionID),            // order ID - required
      projectName,                      // SKU/code - required
      'Footprints donation',            // product name - not required by spec but appears to be required anyway
      orderedCountries,                 // category or variation
      String(price),                    // unit price - required
      '1'                               // quantity - required
    ]);
  };

  // Add Pre-Ex, price should be an int
  $.googleAnalytics.addPreEx = function (price) {
    _gaq.push(['_addItem',
      String(transactionID),            // order ID - required
      'Medial premium',                 // SKU/code - required
      'Pre-Ex cover',                   // product name - not required by spec but appears to be required anyway
      orderedCountries,                 // category or variation
      String(price),                    // unit price - required
      '1'                               // quantity - required
    ]);
  };

  // Add Business benefits cover, price should be an int
  $.googleAnalytics.addBusinessBenefitCover = function (price) {
    _gaq.push(['_addItem',
      String(transactionID),            // order ID - required
      'Business premium',                 // SKU/code - required
      'Business Benefit cover',                   // product name - not required by spec but appears to be required anyway
      orderedCountries,                 // category or variation
      String(price),                    // unit price - required
      '1'                               // quantity - required
    ]);
  };

  // Add Snow sports cover, price should be an int
  $.googleAnalytics.addSnowSportsCover = function (price) {
    _gaq.push(['_addItem',
      String(transactionID),            // order ID - required
      'Snow sports premium',                 // SKU/code - required
      'Snow sports cover',                   // product name - not required by spec but appears to be required anyway
      orderedCountries,                 // category or variation
      String(price),                    // unit price - required
      '1'                               // quantity - required
    ]);
  };

  // This has to be called after the transaction has been built to submit it.
  $.googleAnalytics.submitTransaction = function () {
    _gaq.push(['_trackTrans']);
  };

  // This adds listeners to track Facebook likes, unlikes, and shares. This has to be called after the FB object has been created.
  $.googleAnalytics.listenForFacebookSocialEvents = function () {
    try {
      if (FB && FB.Event && FB.Event.subscribe) {
        FB.Event.subscribe('edge.create', function (opt_target) {
          _gaq.push(['_trackSocial', 'Facebook', 'Like', opt_target]);
        });

        FB.Event.subscribe('edge.remove', function (opt_target) {
          _gaq.push(['_trackSocial', 'Facebook', 'Unlike', opt_target]);
        });

        FB.Event.subscribe('message.send', function (opt_target) {
          _gaq.push(['_trackSocial', 'Facebook', 'Send', opt_target]);
        });
      }
    } catch (e) { }
  };

  // Load a script tag when window load fires
  $.googleAnalytics.loadScriptOnLoad = function (url) {
    utils.fireOnWindowLoad(function () {
      utils.loadScript(url);
    });
  };

  // TODO: Might be better to use _getLinkerUrl for both of the following! This would better line up with how
  // universal analytics behaves

  // Call this whenever an internal, cross domain link is clicked. Let the link continue as normal
  $.googleAnalytics.adjustLinkForCrossDomainTracking = function (link) {
    if (googleAnalyticsScriptLoaded) {
      _gaq.push(['_link', url]);
    }
  }

  // Call this whenever an internal, cross domain form is submitted. Prevent default on the form 
  // itself depending on the value returned
  // Note: This code is not unit tested due to it's effect on page state
  $.googleAnalytics.crossDomainPostWithSuccess = function (formDomElement) {
    if (googleAnalyticsScriptLoaded) {
      _gaq.push(['_linkByPost', formDomElement]);

      return true;
    } else {
      return false;
    }
  }

  var utils = $.googleAnalytics.utils = {
    // Convert an int value for days into a standardised string format
    groupDuration: function (days) {
      var approximateMonth = 365 / 12;

      if (days < 28) {
        return days + (days == 1 ? ' day' : ' days');
      } else if (days < Math.round(3 * approximateMonth)) {
        return Math.floor(days / 7) + ' weeks';
      } else {
        return Math.floor((days + 1) / approximateMonth) + ' months'; // +1 on days to fudge the numbers slightly, so 91 days = 3 months (instead of 91.25+), and 12 months is guaranteed for 365 / 12 * 12 in case of a rounding error
      }
    },

    // Convert days into weeks
    weekDuration: function (days) {
      var weeks = Math.floor(days / 7);

      return weeks + (weeks == 1 ? ' week' : ' weeks');
    },

    // Group an array of ages into meaningful strings
    groupAges: function (ages) {
      var groupedAges = [];

      ages.sort(function (a, b) {
        return a - b;
      });

      for (var i = 0; i < ages.length; i++) {
        var groupedAge;
        var age = ages[i];

        if (age < 18) {
          groupedAge = 'Under 18';
        } else if (age < 25) {
          groupedAge = '18 - 24';
        } else if (age < 35) {
          groupedAge = '25 - 34';
        } else if (age < 45) {
          groupedAge = '35 - 44';
        } else if (age >= 45) {
          groupedAge = '45+';
        }

        groupedAges.push(groupedAge);
      }

      return groupedAges.join(', ');
    },

    // Order the country list alphabetically
    orderCountries: function (countryList) {
      var countries = countryList.split('|');

      countries.sort();

      return countries.join('|');
    },

    // Limit countries to 100 characters max, truncating before the | - this is important for custom variables as they have a max length (speced 64 bytes, 128 seems okay)
    limitCountries: function (countryList) {
      var match = countryList.match(/^(.{0,100})(?:\|.*)?$/);

      return match[1];
    },

    // Set an individual cookie
    setCookie: function (name, value, options) {
      options = options || {};

      var cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);

      if (options.expires) {
        var expires;

        if (typeof options.expires == 'number') {
          // A number of days was passed in, convert it into a date from now
          expires = new Date();
          expires.setDate(expires.getDate() + options.expires);
        } else {
          // A date was passed in, just use the date
          expires = options.expires;
        }

        cookie += '; expires=' + expires.toUTCString();
      }

      // Even though cookies normally default to current path, it is more useful to default to root for analytics
      cookie += '; path=' + (options.path || '/');

      // Even though cookies normally default to current domain, it is more useful to default to parent domain for analytics. 
      // Note that this means we lose the ability to set a domain without a . prefix, as the only way to do that is to not add a 'domain=' to the cookie.
      cookie += '; domain=' + (options.domain || allSubdomains);

      if (options.secure) {
        cookie += '; secure';
      }

      document.cookie = cookie;

      return cookie;
    },

    // Get an individual cookie by name.
    // Keep in mind, it's possible to have multiple cookies with the same name, eg: one for .demo.com, another for subdomain.demo.com, and a third for .demo.com/cheese.
    // There is no way to differentiate between these, and a random one will be returned. In other words, don't reuse names between domain / path levels.
    getCookie: function (name) {
      var cookies = utils.getCookies();

      for (var cookieName in cookies) {
        if (cookieName == name) {
          return cookies[cookieName];
        }
      }

      return null;
    },

    // Finds cookies by name based on a passed in regex. Returns an filtered object of cookies
    findCookies: function (pattern) {
      var allCookies = utils.getCookies();
      var matchingCookies = {};

      for (var cookieName in allCookies) {
        if (cookieName.match(pattern)) {
          matchingCookies[cookieName] = allCookies[cookieName];
        }
      }

      return matchingCookies;
    },

    // Returns an object of all cookies decoded
    getCookies: function () {
      var rawCookies = document.cookie.split('; ');
      var cookies = {};

      for (var i = 0; i < rawCookies.length; i++) {
        var rawCookie = rawCookies[i].split('=');
        cookies[decodeURIComponent(rawCookie[0])] = decodeURIComponent(rawCookie[1] || ''); // IE saves empty cookie strings as just the cookie name, sans =, so cookie[1] might be null
      }

      return cookies;
    },

    // Remove a cookie, this is done by setting a cookie with a date of yesterday.
    // Keep in mind that if you specify path or domain when you create the cookie, you have to also specify them when you destroy it.
    deleteCookie: function (name, options) {
      options = options || {};
      options.expires = -1;

      utils.setCookie(name, '', options);
    },

    // Accepts an event, and encodes as a string - makes use of encodeURIComponent to make sure things can be split safely later
    encodeEvent: function (category, action, eventOptions) {
      eventOptions = eventOptions || {};

      var categoryString = 'category:' + encodeURIComponent(category);
      var actionString = 'action:' + encodeURIComponent(action);
      var labelString = 'label:' + encodeURIComponent(eventOptions['label'] || '');
      var valueString = 'value:' + (eventOptions['value'] || '');

      return [categoryString, actionString, labelString, valueString].join(';');
    },

    // Accepts an encoded event, and returns an array of arguments
    decodeEvent: function (encodedEvent) {
      var match = encodedEvent.match(/^category:(.+);action:(.+);label:(.+)?;value:(.+)?$/);
      var options = {};

      if (match[3]) {
        options['label'] = decodeURIComponent(match[3]);
      }

      if (match[4]) {
        options['value'] = Number(match[4]);
      }

      return [decodeURIComponent(match[1]), decodeURIComponent(match[2]), options];
    },

    // Attaches passed in callbacks to the browsers window load event
    fireOnWindowLoad: function (toFire) {
      if (window.addEventListener) {
        // One for normal browsers
        window.addEventListener('load', toFire, false);
      } else if (window.attachEvent) {
        // One for IE 8 and below
        window.attachEvent('onload', toFire);
      }
    },

    // Load a script tag
    loadScript: function (url) {
      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.async = true;
      script.src = url;
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(script, firstScriptTag);
    }
  };

  return $;

})(wng || {});

// Namespaced under wng, but also assigned to window.googleAnalytics for backwards compatibility across brands
window.googleAnalytics = wng.googleAnalytics;
;
  /**
  * Spoofs placeholders in browsers that don't support them (eg Firefox 3)
  * 
  * Copyright 2011 Dan Bentley
  * Licensed under the Apache License 2.0
  *
  * Author: Dan Bentley [github.com/danbentley]
  * Refactored by: Jayphen [github.com/jayphen]
  */

(function($){
  $.fn.extend({
    'placeholderShiv' : function () {
      return this.each(function(){

        if ("placeholder" in document.createElement("input")) return; 

        var self = $(this),
          holder = self.attr('placeholder');
          self.bind('init.placeholder', function () {
            self.val() === '' ? self.val(holder).addClass('placeholder') : self.data('changed', true).removeClass('placeholder');
          })

          .bind('submitCheck.placeholder', function () {
            if (self.data('changed')) return;
            if (self.val() === holder) self.val('');
          })

          .bind('focus.placeholder click.placeholder', function () {
            self.removeClass('placeholder').trigger('submitCheck.placeholder');
          })

          .bind('blur.placeholder', function () {
            self.trigger('init.placeholder');
          })

          .bind('change.placeholder', function () {
            self.data('changed', self.val() !== '');
          });
      }).trigger('init.placeholder');
    }
  });
})(jQuery);


$(document).ready(function() {
  $('input[placeholder]').placeholderShiv();

  $('form').has("input[placeholder]").bind('submit', function (e) {
    $(this).find("input[placeholder]").trigger('submitCheck.placeholder');
  });
});
;
/*!
 * jQuery Cookie Plugin v1.3.1
 * https://github.com/carhartl/jquery-cookie
 *
 * Copyright 2013 Klaus Hartl
 * Released under the MIT license
 */
(function (factory) {
	if (typeof define === 'function' && define.amd) {
		// AMD. Register as anonymous module.
		define(['jquery'], factory);
	} else {
		// Browser globals.
		factory(jQuery);
	}
}(function ($) {

	var pluses = /\+/g;

	function decode(s) {
		if (config.raw) {
			return s;
		}
		return decodeURIComponent(s.replace(pluses, ' '));
	}

	function decodeAndParse(s) {
		if (s.indexOf('"') === 0) {
			// This is a quoted cookie as according to RFC2068, unescape...
			s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
		}

		s = decode(s);

		try {
			return config.json ? JSON.parse(s) : s;
		} catch(e) {}
	}

	var config = $.cookie = function (key, value, options) {

		// Write
		if (value !== undefined) {
			options = $.extend({}, config.defaults, options);

			if (typeof options.expires === 'number') {
				var days = options.expires, t = options.expires = new Date();
				t.setDate(t.getDate() + days);
			}

			value = config.json ? JSON.stringify(value) : String(value);

			return (document.cookie = [
				config.raw ? key : encodeURIComponent(key),
				'=',
				config.raw ? value : encodeURIComponent(value),
				options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
				options.path    ? '; path=' + options.path : '',
				options.domain  ? '; domain=' + options.domain : '',
				options.secure  ? '; secure' : ''
			].join(''));
		}

		// Read
		var cookies = document.cookie.split('; ');
		var result = key ? undefined : {};
		for (var i = 0, l = cookies.length; i < l; i++) {
			var parts = cookies[i].split('=');
			var name = decode(parts.shift());
			var cookie = parts.join('=');

			if (key && key === name) {
				result = decodeAndParse(cookie);
				break;
			}

			if (!key) {
				result[name] = decodeAndParse(cookie);
			}
		}

		return result;
	};

	config.defaults = {};

	$.removeCookie = function (key, options) {
		if ($.cookie(key) !== undefined) {
			// Must not alter options, thus extending a fresh object...
			$.cookie(key, '', $.extend({}, options, { expires: -1 }));
			return true;
		}
		return false;
	};

}));
;
/*

jquery.autocomplete.js by Jaidev Soin

Version 2

Usage:
$(input).autocomplete(url || array of data, options);
  
Ajax mode:
  
If the first parameter is a URL (string), the autocompleter will work using aysnc JSON requests to that url. It will pass a cleansed version of what
the user entered as the 'text' parameter. The results returned should be of the form:
  
{
matches: [
match1,
match2
]
}
  
If exact matching is occuring, an additional url paramter of 'exact=true' will be passed, matching should then only occur on exact terms, with results
returned in the form:
  
{
matches: [
match1,
match2
],
failed: {
termThatFailedToMatch1,
termThatFailedToMatch2
}
}
    
The server is responsible for splitting multiple terms in the users text on symbols such as ',' and 'and', have a look within for how we do it locally if you want an example
    
If requesting remote matches, it is advised to set the typingTimeOut option to something like 500 milliseconds
  
  
Local matching:
    
If the first paramater is an array, matching will occur using the opt.matchFromLocal method. This is responsible for both exact and partial matching.
  
  
Data passed to autocompleter:
    
By default, the autocompleter expects either an array of objects of the form { 'id', 'name', 'alias'(optional), 'sort'(optional) } to search through to find matches. If you want to use any other data format, you need to override opt.matchFromLocal and opt.matchTemplate. Finally, keep in mind that the order of data passed to the autocompleter is significant, affecting both the order of results as well as their grouping.

Triggers you might want to listen for:

itemChosen(data, textUserEntered, (optional)selectedListItem)
errorFeedback.autocomplete(errorType, errorDetails)
instructionsShown(instructionsElement)
autocompleterShown(autocompleterElement)
showInstructions((optional)noFadeIn)
showMatches(matches, textUserEntered, (optional)noFadeIn)
nextSelected(selectedElement)
previousSelected(selectedElement)
removeAutocompleter((optional)noFadeOut)
removeInstructions((optional)noFadeOut)
findingExactMatchesFor(filteredTextInInput, triggeringAction)
  
  
Triggers you might want to use yourself:

itemChosen(data, textuserEntered)
showMatches(matches, textUserEntered, (optional)noFadeIn)
triggerUseSelectedOrFindMatch
useSelectedItem((optional)triggeringAction)
findExactMatches((optional)triggeringAction)
showInstructions((optional)noFadeIn)
showInstructionsOrAllIfRequired
selectNext
selectPrevious
ensureSelectedVisible
removeAutocompleter((optional)noFadeOut)
removeInstructions((optional)noFadeOut)

Data you might find useful:

Each li in the autocompleter has .data('dataObject), containing the data object that was matched against.

*/

(function ($) {
  var KEY = {
    ESC: 27,
    RETURN: 13,
    TAB: 9,
    BS: 8,
    DEL: 46,
    UP: 38,
    DOWN: 40,
    SHIFT: 16
  };

  $.fn.extend({
    autocomplete: function (matchSource, opt) {
      opt = $.extend({
        loadingClass: 'loading',              // Class applied to the input when an ajax request is in progress
        autocompleterOpenClass: 'autocompleter-open', // Class applied to the input when the autocompleter is visible 
        instructionsOpenClass: 'instructions-open', // Class applied to the input when the instructions are visible
        selectedClass: 'selected',            // Class applied to an item in the list when it is selected (return / click will use that item)
        selectableClass: 'selectable',        // Class applied to an item in the list if it can be selected (not a title)
        groupTitleClass: 'group-title',       // Class applied to the title of a group of items in the list
        autocompleterClass: 'autocomplete',   // Class applied to the ul that is the immediate parent of the list items
        maxLocalResults: 10,                  // Max number of results to show when searching against a local array, does not affect ajax results, ignored if no instructions
        maxHeightInItems: null,               // Number of items that should fit within the autocompleter without scrolling, if null defaults to max local results. (Ignores group titles)
        selectedID: null,                     // Currently selected item (matched by id) - Note, only really makes sense if current usage can only select one item at a time
        selectFirstItem: true,                // Auto select first item in the list when matches show, ignored if selected set
        enableExactMatching: true,            // Find exact matches to the users text when nothing is selected, but an action to enter data (such as return or tab) occurs
        typingTimeOut: 0,                     // Number of milliseconds between a keypress and matches showing / an ajax request being fired. Recommend upping for ajax (500),
        fadeInSpeed: 200,                     // Number of miliseconds fading in takes
        fadeOutSpeed: 200,                    // Number of miliseconds fading out takes
        alignment: 'left',                    // Autocompleter will be left aligned with the left side of the input. Alternative is 'right'
        getUrlParameters: {},                 // Any additional url paramters to send with the ajax requests
        topOffset: 0,                         // Number of pixels to tweak the intro / autocompleters top offset by
        leftOffset: 0,                        // Number of pixels to tweak the intro / autocompleters left offset by
        focusNextFieldOnItemSelect: false,    // Automatically go to the next field based on tabindex when an item is chosen 
        anchorTo: null,                       // Element to anchor the autocompleter / instructions to - if null defaults to input
        multiTermSeperatorRegex: /\s*(?:,|\s&\s|\sand\s)\s*/i,       // Regex for splitting user text into individual terms to be matched - set to null to disable splitting
        instructions: null,                   // jQuery element to display to the user on input focus
        ignoreClicksOn: null,                 // jQuery elements that when clicked on have no effect on the state of the autocompleter
        groupingTitle: function (match) { return null; },              // What title does this item come under (string, may contain html). Grouping is dependant on order of aray / ajax results
        inputFilter: function (text) { return text; },                 // Allows modification of text the user entered before it is used to match against array / sent with ajax request (return string)
        autocompleterTemplate: function (autocompleter) { return autocompleter; }, // Allows wrapping of autocompleter ul in other elements / addition of extra instructions

        // Create contents of li for a matching item. Can return string, dom element, or jQuery object.
        //    match: matching item from either ajax call or local array. At it's simplest a string but can be anything
        //    textUserEntered: what is in the input field, trimmed and with inputFilter applied
        //    highlightFunction: method to help highlight text, usage is (needle, haystack)
        matchTemplate: function (match, textUserEntered, highlightUtility) {
          if (match.matchedAlias) {
            return match.name + " (" + highlightUtility(textUserEntered, match.matchedAlias) + ")";
          } else {
            return highlightUtility(textUserEntered, match.name);
          }
        },

        // Used when searching through a passed in array - not used at all when in ajax mode. Returns an array of matches. Can also sort your results in here.
        // May optionally add a "matchedAlias" key / value
        //    textUserEntered: what is in the input field, trimmed and with inputFilter applied
        //    list: The array that was passed to the autocompleter at init.
        //    regexToMatchWordStartUtility: Function to match a given text against the start of a word, usage is (text)
        //    exact: Boolean of whether exact matching is required, an explanation of exact matching can be found in the intro of this plugin
        matchFromLocal: function (textUserEntered, list, regexToMatchWordStartUtility, sortBySortPropertyUtility, exact) {
          var matches = [];

          var matchRegex = regexToMatchWordStartUtility(textUserEntered);

          $.each(list, function (i, item) {
            var nameAndAliases = [item.name].concat(item.aliases || []);

            $.each(nameAndAliases, function (j, nameOrAlias) {
              if (exact ? (nameOrAlias.toLowerCase() == textUserEntered.toLowerCase()) : (nameOrAlias.match(matchRegex))) {
                matches.push($.extend(item, { 'matchedAlias': (j == 0) ? null : nameOrAlias }));
                return false;
              }
            });
          });

          matches.sort(function (a, b) {
            if (!!a.matchedAlias == !!b.matchedAlias) {
              return sortBySortPropertyUtility(a, b);
            }
            return a.matchedAlias ? 1 : -1;
          });

          return matches;
        }
      }, opt);

      var utils = {
        // Look for terms in a string of text and wrap them in <em>. Regex is there to escape any regex characters that might be in the terms
        highlight: function (needle, haystack) {
          var trimmed = $.trim(needle);

          // Can this be merged?
          if (needle.length == 0) {
            return haystack;
          } else {
            return haystack.replace(utils.regexToMatchWordStart(trimmed), function (match, prefix, term) {
              return prefix + "<em>" + term + "</em>";
            });
          }
        },

        // Creates a regex used to test if terms start with the text parameter
        regexToMatchWordStart: (function () {
          var replacements = [
            // ae -> ligature or umlaut
            { search: 'ae', replacement: '([a\xE0-\xE5][e\xE8-\xEB]|\xE6|\xE4)' },
            // ae ligature
            { search: '\xE6', replacement: '(ae|\xE6)' },
            // oe -> ligature or umlaut
            { search: 'oe', replacement: '([o\xF2-\xF6\xF8][e\xE8-\xEB]|\x153|\xF6)' },
            // oe ligature
            { search: '\x153', replacement: '(oe|\x153)' },
            // ue -> umlaut
            { search: 'ue', replacement: '([u\xF9-\xFC][e\xE8-\xEB]|\xFC)' },
            // ny -> enya
            { search: 'ny', replacement: '(\xF1|n[y\xFD\xFF])' },
            // a umlaut
            { search: '\xE4', replacement: '([a\xE0-\xE5]|ae)' },
            // a - all
            { search: '[a\xE0-\xE5]', replacement: '[a\xE0-\xE5]' },
            // c, cedila
            { search: '[c\xE7]', replacement: '[c\xE7]' },
            // e -all
            { search: '[e\xE8-\xEB]', replacement: '[e\xE8-\xEB]' },
            // i - all
            { search: '[i\xEC-\xEF]', replacement: '[i\xEC-\xEF]' },
            // n -> enya
            { search: 'n', replacement: '[n\xF1]' },
            // enya
            { search: '\xF1', replacement: '(n|\xF1|ny)' },
            // o umlaut
            { search: '\xF6', replacement: '([o\xF2-\xF6\xF8]|oe)' },
            // o - all
            { search: '[o\xF2-\xF5\xF8]', replacement: '[o\xF2-\xF6\xF8]' },
            // u umlaut
            { search: '\xFC', replacement: '([u\xF9-\xFC]|ue)' },
            // u - all
            { search: '[u\xF9-\xFB]', replacement: '[u\xF9-\xFC]' },
            // y - all
            { search: '[y\xFD\xFF]', replacement: '[y\xFD\xFF]' }
          ];

          var searchDiacriticsParts = [];
          for (var i = 0; i < replacements.length; i++) {
            searchDiacriticsParts.push('(' + replacements[i].search + ')');
          }

          var searchDiacritics = new RegExp('(?:' +
            searchDiacriticsParts.join('|') +
            ')', 'gi');

          function replaceDiacritics() {
            for (var index = 1; index < arguments.length; index++) {
              if (arguments[index]) {
                return replacements[index - 1].replacement;
              }
            }
          }

          return function (text) {
            // Regex is there to escape any regex characters that might be in the terms

            var textSafeForRegex = $.trim(text.toLowerCase())
              .replace(/([\\\^\$*+[\]?{}.=!:(|)])/g, "\\$1")
              .replace(searchDiacritics, replaceDiacritics);

            return new RegExp('(^|[\\s(\\-])(' + textSafeForRegex + ')', 'gi');
          }
        })(),

        // Function for sorting an array of objects by the .sort property
        sortBySortProperty: function (a, b) {
          if (a.sort < b.sort) {
            return -1;
          } else if (a.sort > b.sort) {
            return 1;
          } else {
            return 0;
          }
        },

        // Used to insert either the autocompleter, or it's instructions into the dom, positioning it based on the input.
        insertAbsoluteElement: function (input, element, constrainWidth) {
          element.css('position', 'absolute').appendTo(opt.container || 'body');
          utils.setAbsoluteElementsTopProperty(input, element, true);

          if (constrainWidth) {
            var borderAndPaddingWidth = element.outerWidth() - element.width();
            element.width(input.outerWidth() - borderAndPaddingWidth);
          }

          // This has to happen once the datepicker is loaded into the dom so we know how wide it is. The user won't see it move at all.
          utils.setAbsoluteElementsLeftProperty(input, element);
        },

        // Sets the CSS left property of the element based on the position of the input and opt.alignment
        setAbsoluteElementsLeftProperty: function (input, element) {
          var left = input.offset().left + opt.leftOffset;

          if (opt.alignment == 'right') {
            left = left + input.outerWidth() - element.outerWidth();
          }

          element.css('left', left + 'px');
        },

        // Sets the CSS top property of the element based on the position of the input
        setAbsoluteElementsTopProperty: function (input, element, scroll) {
          var top = input.offset().top + input.outerHeight(false) + opt.topOffset;
          if (opt.container) {
            top -= $(opt.container).offset().top - $(opt.container).scrollTop() + parseInt($(opt.container).css('border-top-width'), 10);
          }
          element.css('top', top + 'px');

          if (scroll) {
            setTimeout(function () {
              var $container = $(opt.container || 'html, body'),
                  alignTop = top - input.outerHeight(false),
                  alignBottom = top + element.outerHeight() + parseInt(element.css('margin-top')) - $(opt.container || 'body').innerHeight(),
                  current = Math.max($(opt.container || 'html').scrollTop(), $(opt.container || 'body').scrollTop());

              if (opt.scrollTo) {
                alignTop += $(opt.scrollTo).offset().top - input.offset().top;
              }

              if (current > alignTop) {
                $container.animate({ scrollTop: alignTop });
              } else if (current < alignTop && current < alignBottom) {
                $container.animate({ scrollTop: Math.min(alignTop, alignBottom) });
              }
            });
          }
        },

        // Retuns the the field in the same form that is plus or minus the passed in index
        getFieldByRelativeTabIndex: function (field, relativeIndex) {
          var fields = $(field.closest('form')
          .find('a[href], button, input, select, textarea')
          .filter(':visible').filter(':enabled')
          .toArray()
          .sort(function (a, b) {
            return ((a.tabIndex > 0) ? a.tabIndex : 1000) - ((b.tabIndex > 0) ? b.tabIndex : 1000);
          }));

          return fields.eq((fields.index(field) + relativeIndex) % fields.length);
        },

        // Convinience method to grab the next field
        nextField: function (field) {
          return utils.getFieldByRelativeTabIndex(field, 1);
        },

        // Convinience method to grab the previous field
        previousField: function (field) {
          return utils.getFieldByRelativeTabIndex(field, -1);
        }
      };

      return this.each(function () {
        var localMatchArray, mouseoverLockTimeout;
        var mouseoverLock = false;
        var selectedID = opt.selectedID;

        var self = $(this)

          .on('init.autocomplete', function () {
            self.attr('autocomplete', 'off');

            if ($.type(matchSource) == 'array') {
              self.triggerHandler('setLocalMatchArray', [matchSource]);
            }

            self.triggerHandler('addClickOutsideListener.autocomplete');
            self.triggerHandler('addWindowResizeListener.autocomplete');
          })

          .on('setLocalMatchArray', function (e, array) {
            localMatchArray = array
          })

          .on('keydown.autocomplete', function (e) {
            // The returns in this switch are used to explictly allow or block the keypress going through. Anything that isn't explicitly handled sets data('supressKey') to be handled on the keyup.
            // Keycode works in all browsers (thanks to jQuery)
            switch (e.keyCode) {
              case KEY.ESC:
                if (self.data('typingTimeOut')) {
                  clearInterval(self.data('typingTimeOut'));
                }
                self.blur();
                self.triggerHandler('removeAutocompleter');
                self.triggerHandler('removeInstructions');
                return true;
              case KEY.RETURN:
                self.triggerHandler('triggerUseSelectedOrFindMatch', ['return']);
                return false;
              case KEY.TAB:
                self.triggerHandler('triggerUseSelectedOrFindMatch', ['tab']);
                self.triggerHandler('removeInstructions');

                if (e.shiftKey) {
                  utils.previousField(self).focus();
                } else {
                  utils.nextField(self).focus();
                }

                return false;
              case KEY.DOWN:
                self.triggerHandler('selectNext');
                break;
              case KEY.UP:
                self.triggerHandler('selectPrevious');
                break;
              default:
                return true;
            }

            // Tracking to ignore the keyup event for any of the non default cases, as we don't want the autocompleter to find matches based on these keys.
            self.data('supressKey', true);
          })


          // Companion to the above keydown event listener - Decides if we want to update the results based on the keypress and also handles typing timeout
          .on('keyup.autocomplete', function (e) {
            if (self.data('supressKey')) {
              self.data("supressKey", false);
              return;
            }

            var key = e.keyCode;
            // >= 48 ignores things we aren't interested in such as control and page up, keyCode gives a unicode value, for more detail google or look at           
            // http://www.cambiaresearch.com/c4/702b8cd1-e5b0-42e6-83ac-25f0306e3e25/javascript-char-codes-key-codes.aspx
            if (key >= 48 || key == KEY.DEL || key == KEY.BS) {
              if (self.data('typingTimeOut')) {
                clearInterval(self.data('typingTimeOut'));
              }

              self.data("typingTimeOut", setTimeout(function () {
                self.triggerHandler('findMatches');
              }, opt.typingTimeOut));
            }
          })

          // If either return or tab are pressed, use whatever is selected. If nothing is selected, then find exact matches from the text in the input
          .on('triggerUseSelectedOrFindMatch', function (e, triggeringAction) {
            if (self.data('autocompleter') && self.data('autocompleter').find('.' + opt.selectedClass).length > 0) {
              self.triggerHandler('useSelectedItem', [triggeringAction]);
            } else if (opt.enableExactMatching) {
              self.triggerHandler('findExactMatches', [triggeringAction]);
            }
          })

          // When user focuses on input, display whatever is required
          .on('focus.autocomplete', function () {
            // This check is required as in certain situations, IE double focuses on elements
            if (!self.data('autocompleter')) {
              if (opt.inputFilter($.trim(self.val())) == '') {
                self.triggerHandler('showInstructionsOrAllIfRequired');
              } else {
                self.triggerHandler('findMatches');
              }
            }
          })

          .on('showInstructionsOrAllIfRequired', function (e, noFadeIn) {
            if (opt.instructions) {
              self.triggerHandler('showInstructions', noFadeIn);
            } else if (localMatchArray) {
              self.triggerHandler('showMatches', [localMatchArray, '', noFadeIn]);
            }
          })

          // Triggered when there is text in the input that we want to use to find autocompleter results. Will either search through passed in array 
          // using the opt.mathFromLocal method, or make a JSON request to the passed in URL. Fires off the showMatches event to display these matches.
          // When making an ajax request, the 'text' paramter contains a cleansed version of the entered text, and expects a hash, with a "matches" item 
          // containing an array of matches
          .on('findMatches', function () {
            var text = opt.inputFilter($.trim(self.val()));

            if (text == '') {
              self.triggerHandler('removeAutocompleter', true);
              self.triggerHandler('showInstructionsOrAllIfRequired', true);
            } else {
              self.triggerHandler('removeInstructions', true);

              if (localMatchArray) {
                // Using passed in array of matches
                var matches = opt.matchFromLocal(text, localMatchArray, utils.regexToMatchWordStart, utils.sortBySortProperty);

                if (opt.instructions) {
                  matches = matches.slice(0, opt.maxLocalResults);
                }

                if (matches.length > 0) {
                  self.triggerHandler('showMatches', [matches, text, true]);
                } else {
                  self.triggerHandler('removeAutocompleter');
                }
              } else {
                // Making JSON request for matches
                self.addClass(opt.loadingClass);
                $.getJSON(matchSource, $.extend(opt.getUrlParameters, { text: text }), function (data) {
                  self.removeClass(opt.loadingClass);

                  if (data.matches.length > 0) {
                    self.triggerHandler('showMatches', [data.matches, text, true]);
                  } else {
                    self.triggerHandler('removeAutocompleter');
                  }
                });
              }
            }
          })


          // Triggered when there is text in the input that we want to use to find exact matches for, and immediately fire itemChosen from. 
          // Will either search through passed in array using the opt.mathFromLocal method, or make a JSON request to the passed in URL.
          // If using the passed in url, will pass both a 'text' paramter containing a cleansed version of the entered text, as well as a 
          // boolean value of 'exact'. The JSON returned should be a hash, with a "matches" item containing an array of matches, as well as
          // a "failed"  item containing an array of the terms that could not be matched against.

          // Note: This skips displaying the autocompleter entirely.
          .on('findExactMatches', function (e, triggeringAction) {
            var text = opt.inputFilter($.trim(self.val()));

            self.triggerHandler('findingExactMatchesFor', [text, triggeringAction]);

            if (text != '') {
              self.triggerHandler('removeInstructions');

              if (localMatchArray) {
                // Using passed in array of matches
                var items = opt.multiTermSeperatorRegex ? text.split(opt.multiTermSeperatorRegex) : [text];
                var failed = [];

                $.each(items, function (i, item) {
                  if (item != '') {
                    var matchArray = opt.matchFromLocal(item, localMatchArray, utils.regexToMatchWordStart, utils.sortBySortProperty, true);
                    if (matchArray.length > 0) {
                      self.triggerHandler('itemChosen', [matchArray[0], text]);
                    } else {
                      failed.push(item);
                    }
                  }
                });

                if (failed.length > 0) {
                  self.triggerHandler('errorFeedback.autocomplete', ['failed on exact match', failed]);
                } else if (triggeringAction != 'tab' && opt.focusNextFieldOnItemSelect) {
                  utils.nextField(self).focus();
                }
              } else {
                // Making JSON request for matches
                self.addClass(opt.loadingClass);
                $.getJSON(matchSource, $.extend(opt.getUrlParameters, { text: text, exact: true }), function (data) {
                  self.removeClass(opt.loadingClass);

                  $.each(data.matches, function (i, match) {
                    self.triggerHandler('itemChosen', [match, text]);
                  });

                  if (data.failed.length > 0) {
                    self.triggerHandler('errorFeedback.autocomplete', ['failed on exact match', data.failed]);
                  } else if (triggeringAction != 'tab' && opt.focusNextFieldOnItemSelect) {
                    utils.nextField(self).focus();
                  }
                });
              }

            }

            // This is required for the situation where either selectFirstItem is not true and the user presses return without selecting anything
            self.triggerHandler('removeAutocompleter');
          })


          // Shows instructions to the user, positioning them using utils.insertAbsoluteElement
          .on('showInstructions', function (e, noFadeIn) {
            if (!self.data('instructions') && opt.instructions) {
              var instructions = opt.instructions.hide();
              utils.insertAbsoluteElement(opt.anchorTo || self, instructions);
              instructions.fadeIn(noFadeIn ? 0 : opt.fadeInSpeed);
              self.data('instructions', instructions);
              self.addClass(opt.instructionsOpenClass);

              self.triggerHandler('instructionsShown', [instructions]);
            }
          })


          // Displays the autocompleter to the user
          // Here title and match elements are created from the opt.groupingTitle and opt.matchTemplate, and appended to the autocompleter ul.
          // The ul is then wrapped using opt.autocompleterTemplate, and then inserted into the dom using utils.insertAbsoluteElement
          .on('showMatches', function (e, matches, textUserEntered, noFadeIn) {
            var currentGroupingTitle;

            var autocompleter = $("<ul/>")
              .addClass(opt.autocompleterClass)
              .addClass(opt.alignment)
              .data('textUserEntered', textUserEntered);

            $.each(matches, function (i, match) {
              var title = opt.groupingTitle(match);

              if (title && title != currentGroupingTitle) {
                currentGroupingTitle = title;

                $('<li/>')
                  .addClass(opt.groupTitleClass)
                  .html(title)
                  .appendTo(autocompleter);
              }

              $('<li/>')
                .html(opt.matchTemplate(match, textUserEntered, utils.highlight))
                .addClass(i % 2 ? 'even' : 'odd')
                .addClass((match.id == selectedID) ? opt.selectedClass : null)
                .addClass(opt.selectableClass)
                .data('dataObject', match)
                .appendTo(autocompleter);
            });

            autocompleter = opt.autocompleterTemplate(autocompleter);
            self.triggerHandler('removeAutocompleter', true);
            utils.insertAbsoluteElement(opt.anchorTo || self, autocompleter, true);
            autocompleter.hide().fadeIn(noFadeIn ? 0 : opt.fadeInSpeed);
            self.data('autocompleter', autocompleter);
            self.triggerHandler('addSelectionListeners.autocomplete');
            self.addClass(opt.autocompleterOpenClass);

            var maxItemsToShow = opt.maxHeightInItems || opt.maxLocalResults;
            if (matches.length > maxItemsToShow) {
              autocompleter
                .css('overflow', 'auto')
                .height(Math.ceil((maxItemsToShow - 0.5) * autocompleter.children('.' + opt.selectableClass + ':first').outerHeight(true)));
            }

            if ($.grep(matches, function (match) { return match.id == selectedID }).length != 1) {
              self.trigger('selectNext');
            }

            // Mousedown is prevented to stop the input from defocussing - this is useful is if the autocompleter has non clickable areas, or
            // if it has a scrollbar. Also helps with placeholderPlus with a default, so that the default isn't displayed to on mouse down.
            autocompleter.on('mousedown', function (e) {
              e.preventDefault();
            })

            self.triggerHandler('ensureSelectedVisible');

            self.triggerHandler('autocompleterShown', [autocompleter]);
          })

          // Update the value of selectedID - useful if no instructions set.
          .on('setSelectedID', function (e, newSelectedID) {
            selectedID = newSelectedID;
          })

          // Add page click listener to hide the autocompleter / instructions when they are showing and a click occurs outside them
          .on('addClickOutsideListener.autocomplete', function () {
            $(document).on('click.autocomplete', function (e) {
              if (self.data('autocompleter') && $(e.target).closest(self.add(self.data('autocompleter')).add(opt.ignoreClicksOn)).length == 0) {
                self.triggerHandler('removeAutocompleter');
              }

              if (self.data('instructions') && $(e.target).closest(self.add(self.data('instructions')).add(opt.ignoreClicksOn)).length == 0) {
                self.triggerHandler('removeInstructions');
              }
            });
          })

          // Add a window resize listener to reposition the autocompleter / instructions if the window is resized
          .on('addWindowResizeListener.autocomplete', function () {
            $(window).on('resize.autocomplete', function () {
              if (self.data('autocompleter')) {
                utils.setAbsoluteElementsLeftProperty(self, self.data('autocompleter'));
              }

              if (self.data('instructions')) {
                utils.setAbsoluteElementsLeftProperty(self, self.data('instructions'));
              }
            });
          })

          // Add listeners to the autocompleter ul. These are for the mouse selection / highlighting of items, and the use of a specific item when it is clicked on. 
          .on('addSelectionListeners.autocomplete', function () {
            self.data('autocompleter')
              .on('click.autocomplete', function (e) {
                if ($(e.target).closest('li.' + opt.selectableClass)[0]) {
                  self.data('supressKey', false);
                  self.triggerHandler('useSelectedItem', ['click']);
                  self.blur();
                  e.stopPropagation();
                }
              })
              .on('mouseover.autocomplete', function (e) {
                // mouseoverLock is required to resolve an issue where having the mouse cursor over the autocompleter while scrolling through options using the keyboard
                // caused the autocompleter to incorrectly select the element under the mouse cursor when ensureSelectedVisible was triggered
                if (!mouseoverLock) {
                  var li = $(e.target).closest('li.' + opt.selectableClass);

                  if (li[0] && !li.hasClass(opt.selectedClass)) {
                    li.addClass(opt.selectedClass).siblings().removeClass(opt.selectedClass);
                  }
                }
              });
          })


          // Select the next selectable item in the autocompleter. If nothing is selected, select the first selectable item
          .on('selectNext', function () {
            if (self.data('autocompleter')) {
              var ulSelector = 'ul.' + opt.autocompleterClass;
              var ul = self.data('autocompleter').is(ulSelector) ? self.data('autocompleter') : self.data('autocompleter').find(ulSelector);
              var next = ul.children('.' + opt.selectedClass).next('.' + opt.selectableClass);
              var toSelect = next.length == 1 ? next : ul.children('.' + opt.selectableClass + ':first');
              toSelect.addClass(opt.selectedClass).siblings().removeClass(opt.selectedClass);

              self.triggerHandler('ensureSelectedVisible');
              self.triggerHandler('nextSelected', [toSelect]);
            }
          })


          // Select the previous selectable item in the autocompleter. If nothing is selected, select the last selectable item
          .on('selectPrevious', function () {
            if (self.data('autocompleter')) {
              var ulSelector = 'ul.' + opt.autocompleterClass;
              var ul = self.data('autocompleter').is(ulSelector) ? self.data('autocompleter') : self.data('autocompleter').find(ulSelector);
              var prev = ul.children('.' + opt.selectedClass).prev('.' + opt.selectableClass);
              var toSelect = prev.length == 1 ? prev : ul.children('.' + opt.selectableClass + ':last');
              toSelect.addClass(opt.selectedClass).siblings().removeClass(opt.selectedClass);

              self.triggerHandler('ensureSelectedVisible');
              self.triggerHandler('previousSelected', [toSelect]);
            }
          })

          // Ensure the currently selected item is visible
          .on('ensureSelectedVisible', function () {
            if (self.data('autocompleter')) {
              var selected = self.data('autocompleter').find('.' + opt.selectedClass);

              if (selected[0]) {
                var ul = selected.parent();

                var topOfSelected = selected.position().top;
                var bottomOfSelected = topOfSelected + selected.outerHeight(false);

                mouseoverLock = true;

                if (bottomOfSelected > ul.height()) {
                  ul.scrollTop(ul.scrollTop() + bottomOfSelected - ul.height());
                } else if (topOfSelected < 0) {
                  ul.scrollTop(topOfSelected + ul.scrollTop());
                }

                clearTimeout(mouseoverLockTimeout);
                mouseoverLockTimeout = setTimeout(function () {
                  mouseoverLock = false;
                }, 100);
              }
            }
          })

          // Fire off itemChosen event for whichever item is currently selected.
          .on('useSelectedItem', function (e, triggeringAction) {
            var selected = self.data('autocompleter').find('.' + opt.selectedClass);
            self.triggerHandler('itemChosen', [selected.data('dataObject'), self.data('autocompleter').data('textUserEntered'), selected]);

            if (triggeringAction != 'tab' && opt.focusNextFieldOnItemSelect) {
              utils.nextField(self).focus();
            }

            self.triggerHandler('removeAutocompleter');
          })


          // Remove autocompleter from the dom and clear out our reference to it
          .on('removeAutocompleter', function (e, noFadeOut) {
            if (self.data('autocompleter')) {
              // Set the top property as the input may have moved due to the adding of an item to the page
              utils.setAbsoluteElementsTopProperty(self, self.data('autocompleter'));

              self.data('autocompleter').fadeOut((noFadeOut ? 0 : opt.fadeOutSpeed), function () {
                self.data('autocompleter').remove();
                self.data('autocompleter', null);
                self.removeClass(opt.autocompleterOpenClass);
              });
            }
          })


          // Remove instructions from the dom and clear out our reference to it
          .on('removeInstructions', function (e, noFadeOut) {
            if (self.data('instructions')) {
              // Set the top property as the input may have moved due to the adding of an item to the page
              utils.setAbsoluteElementsTopProperty(self, self.data('instructions'));

              self.data('instructions').fadeOut((noFadeOut ? 0 : opt.fadeOutSpeed), function () {
                self.data('instructions').remove();
                self.data('instructions', null);
                self.removeClass(opt.instructionsOpenClass);
              });
            }
          });


        // On plugin load, fire off init
        self.triggerHandler('init.autocomplete');
      });
    }
  });
})(jQuery);;
/*

jQuery.autocompleteWithPanel.js by Jaidev Soin

This plugin configures the autocomplete for use on TID, including support for aliases, and the country selection panel.
For usage, options, and triggers, refer to the parent plugin.

Options passed in to this plugin are passed to parent

*/


(function ($) {
  $.fn.extend({
    autocompleteWithPanel: function (countriesWithAliases, panelData, opt) {
      opt = $.extend({
        // Autocomplete with panel options
        countryPanelID: 'countryPanel', // ID of the country picker panel
        regionsClass: 'regions',        // Class applied to the div that wraps the regions
        regionClass: 'region',          // Class applied to the dl that wraps a region, in addition to the region name
        panelCloseText: 'Close x',      // Text for the link that closes the country picker panel.
        panelCloseClass: 'closePanel',  // Class applied to the close link at the top of the country picker panel
        panelHeader: '<strong>Type</strong> the country name', // Title displayed in the country picker panel
        panelFooter: "<strong>Can't find the country in this list?</strong> Type in the country name in the box above.",
        alignment: 'left',
        fadeInSpeed: 200                // Autocompleter options you might want to change
      }, opt);

      var utils = {
        regexToMatchWordStart: (function () {
          var replacements = [
            // ae -> ligature or umlaut
            { search: 'ae', replacement: '([a\xE0-\xE5][e\xE8-\xEB]|\xE6|\xE4)' },
            // ae ligature
            { search: '\xE6', replacement: '(ae|\xE6)' },
            // oe -> ligature or umlaut
            { search: 'oe', replacement: '([o\xF2-\xF6\xF8][e\xE8-\xEB]|\x153|\xF6)' },
            // oe ligature
            { search: '\x153', replacement: '(oe|\x153)' },
            // ue -> umlaut
            { search: 'ue', replacement: '([u\xF9-\xFC][e\xE8-\xEB]|\xFC)' },
            // ny -> enya
            { search: 'ny', replacement: '(\xF1|n[y\xFD\xFF])' },
            // a umlaut
            { search: '\xE4', replacement: '([a\xE0-\xE5]|ae)' },
            // a - all
            { search: '[a\xE0-\xE5]', replacement: '[a\xE0-\xE5]' },
            // c, cedila
            { search: '[c\xE7]', replacement: '[c\xE7]' },
            // e -all
            { search: '[e\xE8-\xEB]', replacement: '[e\xE8-\xEB]' },
            // i - all
            { search: '[i\xEC-\xEF]', replacement: '[i\xEC-\xEF]' },
            // n -> enya
            { search: 'n', replacement: '[n\xF1]' },
            // enya
            { search: '\xF1', replacement: '(n|\xF1|ny)' },
            // o umlaut
            { search: '\xF6', replacement: '([o\xF2-\xF6\xF8]|oe)' },
            // o - all
            { search: '[o\xF2-\xF5\xF8]', replacement: '[o\xF2-\xF6\xF8]' },
            // u umlaut
            { search: '\xFC', replacement: '([u\xF9-\xFC]|ue)' },
            // u - all
            { search: '[u\xF9-\xFB]', replacement: '[u\xF9-\xFC]' },
            // y - all
            { search: '[y\xFD\xFF]', replacement: '[y\xFD\xFF]' }
          ];

          var searchDiacriticsParts = [];
          for (var i = 0; i < replacements.length; i++) {
            searchDiacriticsParts.push('(' + replacements[i].search + ')');
          }

          var searchDiacritics = new RegExp('(?:' +
            searchDiacriticsParts.join('|') +
            ')', 'gi');

          function replaceDiacritics() {
            for (var index = 1; index < arguments.length; index++) {
              if (arguments[index]) {
                return replacements[index - 1].replacement;
              }
            }
          }

          return function (text) {
            // Regex is there to escape any regex characters that might be in the terms

            var textSafeForRegex = $.trim(text.toLowerCase())
              .replace(/([\\\^\$*+[\]?{}.=!:(|)])/g, "\\$1")
              .replace(searchDiacritics, replaceDiacritics);

            return new RegExp('(^|[\\s(\\-])(' + textSafeForRegex + ')', 'gi');
          }
        })()
      };

      return this.each(function () {

        var self = $(this)

          .bind('init.autocompleteWithPanel', function () {
            self.triggerHandler('createCountryPanel');
            self.triggerHandler('setupAutocompleter');
          })

          .bind('setupAutocompleter', function () {
            // The below options to the autocomplete plugin are overridable just like the ones at the top of this plugin.
            // They are however kept seperate, as a way of indicating that overriding them goes against the purpose of this
            // plugin - In other words, if you feel you need to overrite them, you probably should not be using this plugin
            // at all, and should be writing a seperate wrapper for the autocomplete plugin.

            self.autocomplete(countriesWithAliases, $.extend({
              // Pass in big panel of counties
              instructions: self.data('countryPanel'),

              // Custom matchTemplate method is required as the countries array isn't just country names, but is a combination of ids, names and alises.
              // This checks if the string the user searched for can be found in the country name. If it can't, it picks the shortest alias of the country that contains the string to display as well.
              matchTemplate: function (country, searchText, highlightMethod) {
                var displayText;
                var searchRegex = utils.regexToMatchWordStart(searchText);

                // First check if what the user entered matches the country
                if (!country['aliases'] || country['name'].match(searchRegex)) {
                  displayText = country['name'];
                } else {
                  // Otherwise, find which aliases are matched
                  var matchingAliases = [];

                  $.each(country['aliases'], function (i, alias) {
                    if (alias.match(searchRegex)) {
                      matchingAliases.push(alias);
                    }
                  });

                  // Use sort provided by Wng.Insurance - so Los Angeles (LA) can be before Louisiana (LA)

                  displayText = country['name'] + " (" + matchingAliases[0] + ")";
                }

                return highlightMethod(searchText, displayText);
              }
            }, opt))

            // Clear the autocompleter when a country is selected
            .bind('itemChosen', function (e, data, textUserEntered, selectedListItem) {
              self.val('');
            })

            // When the country panel is shown, add a listener to check for when the countries are clicked on.
            // This causes the country to be searched for using the autocompleter
            .bind('instructionsShown', function (e, instructions) {
              instructions.bind('click.autocompleteWithPanel', function (e) {
                var target = $(e.target);

                if (target.is('a')) {
                  if (!target.hasClass(opt.panelCloseClass)) {
                    self
                      .val(target.text())
                      .triggerHandler('findExactMatches', ['click_on_panel']);
                  }

                  self.triggerHandler('removeInstructions');

                  return false;
                }
              });

            });
          })

        // Build the country menu
          .bind('createCountryPanel', function () {
            var countryPanel = $("<div/>").attr('id', opt.countryPanelID).addClass(opt.alignment);

            $('<h3/>').html(opt.panelHeader).appendTo(countryPanel);

            $('<a/>').text(opt.panelCloseText).addClass(opt.panelCloseClass).appendTo(countryPanel);

            var regions = $('<div/>').addClass(opt.regionsClass).appendTo(countryPanel);

            $.each(panelData, function (i, region) {
              var dl = $("<dl/>").addClass(region.name.toLowerCase()).addClass(opt.regionClass);
              $("<dt/>").text(region.name).appendTo(dl);

              $.each(region.countries, function (i, countryName) {
                $("<dd><a>" + countryName + "</a></dd>").appendTo(dl);
              });

              regions.append(dl);
            });

            $('<div/>').html(opt.panelFooter).addClass('note').appendTo(countryPanel);

            self.data('countryPanel', countryPanel);
          });

        self.triggerHandler('init.autocompleteWithPanel');
      });
    }
  });
})(jQuery);;
/*

jquery.datePicker.js by Jaidev Soin

Version 2.1

Note: Currently highlighted dates in range doesn't work properly - not sure if fixing it is bloat as it really clutters the date pickers look

Triggers you might want to listen for:

datePickerDrawn(datePickerDiv)
writeOutDate(date)
datePickerRemoveStart(selectedDate, datePickerDiv) - selectedDate may be null
datePickerRemoveDone(selectedDate) - selectedDate may be null
  
Triggers you might want to use yourself:

writeOutDate(date)
click.datePicker
showNextMonth
showPastMonth
removeDatePicker(selectedDate) - selectedDate may be null
  
Date formatting:

d           Day of the month as digits; no leading zero for single-digit days.
dd          Day of the month as digits; leading zero for single-digit days.
ddd         Day of the month as ordinalized number (eg: 23rd)
m           Month as digits; no leading zero for single-digit months.
mm          Month as digits; leading zero for single-digit months.
mmm         Month as a three-letter abbreviation.
mmmm        Month as its full name.
yy          Year as last two digits; leading zero for years less than 10.
yyyy        Year represented by four digits.
*/

(function ($) {
  var KEY = {
    ESC: 27,
    RETURN: 13,
    TAB: 9,
    BS: 8
  };

  $.fn.extend({
    datePicker: function (opt) {
      opt = $.extend({
        fieldName: "date",
        monthNames: ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
        shortMonthNames: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"],
        shortDayNames: ["S", "M", "T", "W", "T", "F", "S"],
        startOfWeek: 1, // 0 = Sunday
        pastMonthSymbol: '&laquo;',
        nextMonthSymbol: '&raquo;',
        numberOfMonths: 1, // Number of months to show
        dateFormat: 'dd/mm/yy',
        fadeInSpeed: 200,
        fadeOutSpeed: 200,
        topOffset: 0,
        leftOffset: 0,
        delayHideOnSelectBy: 0,
        minDate: null, // Date object
        maxDate: null, // Date object
        earliest20thCenturyYear: 1940, // if only two year digits are pre-filled or typed in the input, this represents the earliest year that will be handled as a 20th century value (rather than 21st century) e.g. 1950.
        calculateMaxDateBasedOnCompanion: null, // Callback that returns a Date object. Expects the companion date (Date object) to be passed in. If this option is set maxDate will be ignored.
        companionPicker: null, // jQuery object
        useCompanionDateAsMin: false,
        startCalendarAtCompanionDate: false, // The month the calendar stats on is based on the value of the companion date
        header: null, // dom / jQuery element
        preventManualEntry: true, // Prevent manual date entry. 
        showOnFocus: true, // Show the datepicker when the field is tabbed to. Note: setting this to false without setting preventManualEntry to false will result in a weird interface
        alignment: 'left', // Datepicker will be left aligned with the left side of the input. Alternative is 'right',
        focusNextFieldOnDateSelect: true, // Automatically go to the next field based on tabindex when a date is chosen
        minDateErrorMessage: function (fieldName, minDateString) {
          return "Please enter a " + fieldName + " later than " + minDateString;
        },
        maxDateErrorMessage: function (fieldName, maxDateString) {
          return "Please enter a " + fieldName + " earlier than " + maxDateString;
        },
        lessThanCompanionDateErrorMessage: function (fieldName, companionDateString) {
          return "Please enter a " + fieldName + " later than " + companionDateString;
        },
        generalDateErrorMessage: function (fieldName, dateString) {
          return 'Please enter a valid ' + fieldName;
        },
        noDateEnteredErrorMessage: function (fieldName) {
          return "Please enter a " + fieldName;
        }
      }, opt);

      var utils = {
        calendarForMonth: function (month, year, selectedDate, selectedCompanionDate) {
          var tbody = $("<tbody></tbody>");

          // Generate day names
          var dayNames = $('<tr/>');

          for (var i = 0; i < 7; i++) {
            $('<th/>').html(opt.shortDayNames[(i + opt.startOfWeek) % 7]).appendTo(dayNames);
          }

          tbody.append(dayNames);

          // Generate the weeks
          var monthStartsOn = utils.monthStartsOn(month, year);
          var daysThisMonth = utils.daysInMonth(month, year);

          // Take into account startOfWeek, with the -7 % 7 to ensure the calendar starts at day -6 to 0
          var dayIndex = (monthStartsOn * -1 + opt.startOfWeek - 7) % 7;

          var today = utils.simplifyOrCorrectDate(new Date());

          var effectiveMaxDate = utils.getEffectiveMaxDate();

          while (dayIndex < daysThisMonth) {
            var week = $('<tr/>');

            for (var i = 0; i < 7; i++) {
              if (dayIndex < 0 || dayIndex >= daysThisMonth) {
                $('<td/>').appendTo(week);
              } else {
                var classes = [];

                // Valid day for this month
                var thisDay = utils.newDSTSafeDate(year, month, dayIndex + 1);

                // Direct comparrison fails for some reason.
                if (selectedDate - thisDay == 0) {
                  classes.push('selected');
                }

                if (today - thisDay == 0) {
                  classes.push('today');
                }

                if (selectedCompanionDate && selectedCompanionDate - thisDay == 0) {
                  classes.push('selectedCompanion');
                }

                if (selectedDate && selectedCompanionDate && ((selectedDate > thisDay && selectedCompanionDate < thisDay) || (selectedDate < thisDay && selectedCompanionDate > thisDay))) {
                  classes.push('inSelectionRange');
                }

                if ((opt.minDate && opt.minDate > thisDay) || (effectiveMaxDate && effectiveMaxDate < thisDay) || selectedCompanionDate && opt.useCompanionDateAsMin && selectedCompanionDate > thisDay) {
                  classes.push('disabled');
                }

                $(utils.sub("<td class='day'><a>#{day}</a></td>", {
                  day: thisDay.getDate()
                }))
                .addClass(classes.join(' '))
                .appendTo(week);
              }

              dayIndex++;
            };

            tbody.append(week);
          }

          var calendar = $(utils.sub("\
            <div class='datepicker-calendar'>\
              <div class='datepicker-calendar-title'>\
                <span class='datepicker-month'>#{month}</span> <span class='datepicker-year'>#{year}</span>\
              </div>\
              <table></table>\
            </div>\
          ", {
            month: opt.monthNames[month],
            year: year
          }))
          .data('month', month)
          .data('year', year);

          calendar.find('table').append(tbody);

          return calendar;
        },
        monthStartsOn: function (month, year) {
          return utils.newDSTSafeDate(year, month, 1).getDay();
        },
        daysInMonth: function (month, year) {
          // Look at how many days the month overflows by when you check what the 32nd day would be
          return 32 - utils.newDSTSafeDate(year, month, 32).getDate();
        },
        sub: function (html, values) {
          return html.replace(/#\{(\w*)\}/g, function (token, key) {
            return values[key] || token;
          });
        },
        dateFromInput: function (input) {
          if (input && input[0] && $.trim(input.val()).length > 0) {
            return utils.dateFromString(input.val());
          } else {
            return null;
          }
        },
        dateFromString: function (dateString) {
          // This utility strives for robustness. Whether whitespace is added / removed, numbers start with leading zeros or not, and whether years are 2 or 4 digits 
          // are all handled gracefully. Also allows use of numbers and strings for months to be interchangable. Eg: 03 will work if March is expected.
          var day, month, year, yearAsNumber, isTwoDigitYear, centuryForParsingTwoDigitYear;

          var dateFormat = opt.dateFormat;
          dateString = $.trim(dateString);

          // Treat the first non-word character as the seperator.
          var dateSeperators = dateString.replace(/[\w+\s+]/g, '');
          var dateSeperator = dateSeperators.length > 0 ? dateSeperators.substr(0, 1) : '';

          var formatSeperators = dateFormat.replace(/[dmy\s+]/g, '');
          var formatSeperator = formatSeperators.length > 0 ? formatSeperators.substr(0, 1) : '';

          // If date string has a different seperator to the date format, then for the purpose of parsing, change the seperator of the date format.
          if (dateSeperator != formatSeperator) {
            // Empty string means white space for either seperator. Gotta deal with that greedily.
            if (formatSeperator.length > 0) {
              dateFormat = dateFormat.replace(new RegExp(formatSeperator, 'g'),
                dateSeperator.length > 0 ? dateSeperator : ' ');
            } else {
              dateFormat = dateFormat.replace(/\s+/g, dateSeperator.length > 0 ? dateSeperator : ' ');
            }
          }

          // If the date string seperator is white space, be greedy.
          if (dateSeperator.length == 0) {
            dateSeperator = /\s+/;
          }

          var splitFormat = dateFormat.split(dateSeperator);
          var splitDate = dateString.split(dateSeperator);

          for (var i = 0; i < splitFormat.length; i++) {
            var formatSection = $.trim(splitFormat[i]).substr(0, 1);
            var dateSection = $.trim(splitDate[i]);

            switch (formatSection) {
              case 'd':
                day = Number(dateSection.replace(/\D/g, ''));
                break;

              case 'm':
                if (isNaN(dateSection)) {
                  var lowerCaseMonth = dateSection.toLowerCase();

                  for (var j = 0; j < opt.shortMonthNames.length; j++) {
                    if (opt.shortMonthNames[j].toLowerCase() == lowerCaseMonth) {
                      month = j;
                      break;
                    }
                  }

                  if (month == undefined) {
                    for (var j = 0; j < opt.shortMonthNames.length; j++) {
                      if (opt.monthNames[j].toLowerCase() == lowerCaseMonth) {
                        month = j;
                        break;
                      }
                    }
                  }
                } else {
                  month = Number(dateSection) - 1;
                }
                break;

              case 'y':
                yearAsNumber = Number(dateSection);
                isTwoDigitYear = (yearAsNumber < 100);
                if (!isTwoDigitYear) {
                  year = yearAsNumber;
                  break;  
                }
                // Handle a two digit year
                if (yearAsNumber >= (opt.earliest20thCenturyYear - 1900)) {
                  centuryForParsingTwoDigitYear = 1900;
                } else {
                  centuryForParsingTwoDigitYear = 2000;
                }
                year = Number(dateSection) + centuryForParsingTwoDigitYear;
                break;
            }
          }

          // If there is an invalid date in the input, nothing should be selected
          if (isNaN(year) || isNaN(month) || isNaN(day) || year < 0 || month < 0 || day < 1) {
            return null;
          } else {
            return utils.newDSTSafeDate(year, month, day);
          }
        },
        getDateError: function (date, checkAgainstCompanionDateIfMin) {

          if (checkAgainstCompanionDateIfMin && opt.useCompanionDateAsMin) {
            var companionDate = utils.dateFromInput(opt.companionPicker);
          }

          var effectiveMaxDate = utils.getEffectiveMaxDate();

          if ((opt.minDate && opt.minDate > date) || (companionDate && companionDate > date)) {
            // We want to return based on the larger of either min date or companion date
            if (companionDate && companionDate > opt.minDate) {
              return opt.lessThanCompanionDateErrorMessage(
                opt.fieldName, utils.getDateString(companionDate));
            } else {
              return opt.minDateErrorMessage(opt.fieldName, utils.getDateString(opt.minDate));
            }
          } else if (effectiveMaxDate && effectiveMaxDate < date) {
            return opt.maxDateErrorMessage(opt.fieldName, utils.getDateString(effectiveMaxDate));
          } else {
            return null;
          }
        },
        getDateString: function (date) {
          // Convert the passed in date format into something sub can use.
          var dateFormatForOutput = opt.dateFormat.replace(/(?:d+|m+|y+)/g, function (token) {
            return "#{" + token + "}";
          });

          var day = date.getDate();
          var month = date.getMonth() + 1;
          var year = date.getFullYear();

          // Sub will only sub in what tokens exist in the dateFormatForOutput string
          return utils.sub(dateFormatForOutput, {
            d: day,
            dd: utils.addLeadingZero(day),
            ddd: utils.ordinalize(day),
            m: month,
            mm: utils.addLeadingZero(month),
            mmm: opt.shortMonthNames[month - 1],
            mmmm: opt.monthNames[month - 1],
            yy: String(year).substr(2, 2),
            yyyy: year
          });
        },
        simplifyOrCorrectDate: function (date) {
          // This method does two things:
          // 1) Specific hours / minutes / seconds in the min and max dates mess with comparrisons
          // 2) There exists a bug with certain browsers (e.g. Safari) where creating a date on the first day of daylight savings 
          //    instead creates one for 11pm the night before. This method tests for those broken dates and fixes them.
          //
          // Note: Things can still go funny once a year around the hour of the DST change over. I don't think there is much
          // we can do about this, as the Javascript Date object just is fundamentally broken. Also, the following code could shift 
          // date by sheer luck if the time is legitimately 11pm and 0 miliseconds on DST eve, but the probability of this doing
          // harm is very very low.

          var dayAfter = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
          var twoDaysAfter = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 2);

          var exactly11pm = date.getHours() == 23 && date.getMinutes() == 0 && date.getSeconds() == 0 && date.getMilliseconds() == 0;
          var dayAfterSameTimezone = date.getTimezoneOffset() == dayAfter.getTimezoneOffset();
          var offsetHoursDifference = (date.getTimezoneOffset() - twoDaysAfter.getTimezoneOffset())/60;
          var twoDaysAfterLaterTimezone = offsetHoursDifference > 0;

          if (exactly11pm && dayAfterSameTimezone && twoDaysAfterLaterTimezone) {
            return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, offsetHoursDifference);
          } else {
            return new Date(date.getFullYear(), date.getMonth(), date.getDate());
          }
        },
        ordinalize: function (number) {
          if (11 <= number % 100 && number % 100 <= 13) {
            return number + "th";
          } else {
            var extenions = { 1: 'st', 2: 'nd', 3: 'rd' };
            return number + (extenions[number % 10] || 'th');
          }
        },
        addLeadingZero: function (number) {
          return (number >= 10) ? number : '0' + String(number);
        },
        getFieldByRelativeTabIndex: function (field, relativeIndex) {
          var fields = $(field.closest('form')
          .find('a[href], button, input, select, textarea')
          .filter(':visible').filter(':enabled')
          .toArray()
          .sort(function (a, b) {
            return ((a.tabIndex > 0) ? a.tabIndex : 1000) - ((b.tabIndex > 0) ? b.tabIndex : 1000);
          }));

          return fields.eq((fields.index(field) + relativeIndex) % fields.length);
        },
        nextField: function (field) {
          return utils.getFieldByRelativeTabIndex(field, 1);
        },
        previousField: function (field) {
          return utils.getFieldByRelativeTabIndex(field, -1);
        },
        getEffectiveMaxDate: function () {
          if (typeof opt.calculateMaxDateBasedOnCompanion === 'function') {
            return utils.getMaxDateBasedOnCompanion();
          } else {
            return opt.maxDate;
          }
        },
        getMaxDateBasedOnCompanion: function() {
          var companionDate = utils.dateFromInput(opt.companionPicker);
          return utils.simplifyOrCorrectDate(opt.calculateMaxDateBasedOnCompanion(companionDate));
        },
        // This is to help fix a Safari 7 / iOS 7 bug referred to in utils.simplifyOrCorrectDate()
        newDSTSafeDate: function(year, month, day) {
          var date = new Date(year, month, day);

          if (date.getDate() != day) {
            return utils.simplifyOrCorrectDate(date);
          } else {
            return date;
          }
        }
      };

      if (opt.minDate) {
        opt.minDate = utils.simplifyOrCorrectDate(opt.minDate);
      }

      if (opt.maxDate) {
        opt.maxDate = utils.simplifyOrCorrectDate(opt.maxDate);
      }

      return this.each(function () {
        var self = $(this)

          .bind('focus.datepicker', function () {
            if (opt.showOnFocus) {
              self.triggerHandler('setup.datepicker');
            }
          })

          .bind('blur.datepicker', function () {
            if (!opt.preventManualEntry) {
              if ($.trim(self.val()).length > 0) {
                var selectedDate = utils.dateFromInput(self);

                if (selectedDate) {
                  var dateError = utils.getDateError(selectedDate, true);

                  if (dateError) {
                    self.triggerHandler('displayDateError', dateError, [selectedDate]);
                  } else {
                    self.triggerHandler('writeOutDate', [selectedDate]);
                    self.triggerHandler('removeDateError');
                  }
                } else {
                  self.triggerHandler('displayDateError', 
                    opt.generalDateErrorMessage(opt.fieldName, self.val()), [self.val()]);
                }
              } else {
                self.triggerHandler('displayDateError', 
                  opt.noDateEnteredErrorMessage(opt.fieldName));
              }
            }
          })

          .bind('click.datepicker', function () {
            self.triggerHandler('setup.datepicker');
          })

          .bind('setup.datepicker', function () {
            if (!self.data('datepicker')) {
              if (opt.useCompanionDateAsMin) {
                var companionDate = utils.dateFromInput(opt.companionPicker);
                var dateError = utils.getDateError(companionDate);

                if (!dateError && companionDate > utils.dateFromInput(self)) {
                  self.triggerHandler('writeOutDate', [companionDate]);
                }
              }

              self.triggerHandler('drawDatePicker');
            }

            if (opt.preventManualEntry) {
              self.val(self.val()); // IE fix - while the input is blurred, the text remains selected which is confusing to the user. This deselects the text. 
              // Slight hack required to blur the datepicker when focus is manually triggered in the JS - Previously it was performing both a blur and a return false, but
              // I don't believe a return false does anything in this case (just returns false to the triggerHandler call). Note preventing default on the blur does not 
              // work as it doesn't cause the previously selected element to blur
              setTimeout(function() {
                self.trigger('blur.datepicker');
              }, 1);
            } else {
              self.select();
            }
          })

          .bind('drawDatePicker', function () {
            var top = self.offset().top + self.outerHeight() + opt.topOffset;
            if (opt.container) {
                top -= $(opt.container).offset().top - $(opt.container).scrollTop() + parseInt($(opt.container).css('border-top-width'), 10);
            }

            var datePickerDiv = $(utils.sub("\
              <div class='datepicker #{alignment}'>\
                <div class='datepicker-calendars'>\
                  <a class='datepicker-show-past-month'>#{pastMonth}</a>\
                  <a class='datepicker-show-next-month'>#{nextMonth}</a>\
                </div>\
              </div>\
            ", {
              pastMonth: opt.pastMonthSymbol,
              nextMonth: opt.nextMonthSymbol,
              alignment: opt.alignment
            }))
            .css({
              'position': 'absolute',
              'display': 'none',
              'top': top + 'px',
              'z-index': '99999'
            });

            if (opt.headerText || opt.panelCloseText) {
              var header = $("<h3>" + opt.headerText + "</h3>" +
                "<a class='" + opt.panelCloseClass + "'>" + opt.panelCloseText + "</a>")
              datePickerDiv.prepend(header);
            }

            var selectedDate = utils.dateFromInput(self);
            var companionDate = utils.dateFromInput(opt.companionPicker);
            var firstCalendarMonth;

            if (companionDate && opt.startCalendarAtCompanionDate) {
              firstCalendarMonth = companionDate;
            } else if (selectedDate) {
              firstCalendarMonth = selectedDate;
            } else {
              firstCalendarMonth = utils.simplifyOrCorrectDate(new Date());
            }

            for (var i = 0; i < opt.numberOfMonths; i++) {
              var month = utils.newDSTSafeDate(firstCalendarMonth.getFullYear(), firstCalendarMonth.getMonth() + i, 1);
              datePickerDiv.find('.datepicker-calendars').append(utils.calendarForMonth(month.getMonth(), month.getFullYear(), selectedDate, companionDate));
            };

            self.data('datepicker', datePickerDiv);

            $(opt.container || 'body').append(datePickerDiv);

            setTimeout(function () {
              var $container = $(opt.container || 'html, body'),
                  alignTop = top - self.outerHeight(false),
                  alignBottom = top + datePickerDiv.outerHeight() + parseInt(datePickerDiv.css('margin-top')) - $(opt.container || 'body').innerHeight(),
                  current = Math.max($(opt.container || 'html').scrollTop(), $(opt.container || 'body').scrollTop());

              if (current > alignTop) {
                $container.animate({ scrollTop: alignTop });
              } else if (current < alignTop && current < alignBottom) {
                $container.animate({ scrollTop: Math.min(alignTop, alignBottom) });
              }
            });

            self.addClass('datepicker-open');
            self.triggerHandler('addListeners');

            datePickerDiv.fadeIn(opt.fadeInSpeed, function () {
              self.triggerHandler('datePickerDrawn', [datePickerDiv]);
            });

            // This has to happen once the datepicker is loaded into the dom so we know how wide it is. The user won't see it move at all.
            self.triggerHandler('setDatePickersLeftProperty');
          })

          .bind('setDatePickersLeftProperty', function () {
            var left = self.offset().left + opt.leftOffset;

            if (opt.alignment == 'right') {
              left = left + self.outerWidth() - self.data('datepicker').outerWidth();
            }

            self.data('datepicker').css('left', left + 'px');
          })

          .bind('addListeners', function () {
            self.data('datepicker').bind('click.datepicker', function (e) {
              var a = $(e.target).closest('a');

              if (a.hasClass('datepicker-show-next-month')) {
                self.triggerHandler('showNextMonth');
              } else if (a.hasClass('datepicker-show-past-month')) {
                self.triggerHandler('showPastMonth');
              } else if (!a.closest('td').hasClass('disabled') && a.closest('td.day')[0] && a[0]) {
                self.triggerHandler('selectDay', [a]);
              }

              return false;
            });

            // We need to keep proper track of these methods so we can unbind them without unbinding the events set by any other datePicker instance.
            self.data('checkForClickOutside', function (e) {
              if (e.target != self[0]) {
                self.triggerHandler('removeDatePicker');
              }
            });

            $(document).bind('click.datepicker', self.data('checkForClickOutside'));

            self.data('checkForActionKeydowns', function (e) {
              if (e.keyCode == KEY.ESC || e.keyCode == KEY.TAB) {
                if (e.keyCode == KEY.TAB) {
                  if (e.shiftKey) {
                    utils.previousField(self).focus();
                  } else {
                    utils.nextField(self).focus();
                  }
                }

                self.triggerHandler('removeDatePicker');

                return false;
              }
              else if (e.keyCode == KEY.BS && opt.preventManualEntry) {
                // This handles the case where the user has clicked the input, and presses backspace in order to clear it, but intead the browser navigates back
                return false;
              }
            });

            $(document).bind('keydown.datepicker', self.data('checkForActionKeydowns'));

            self.data('checkForWindowResize', function () {
              self.triggerHandler('setDatePickersLeftProperty');
            });

            $(window).bind('resize.datepicker', self.data('checkForWindowResize'));
          })

          .bind('showNextMonth', function () {
            var lastVisibleMonth = self.data('datepicker').find('.datepicker-calendar:last');
            var nextMonth = utils.newDSTSafeDate(lastVisibleMonth.data('year'), lastVisibleMonth.data('month') + 1, 1);
            lastVisibleMonth.after(utils.calendarForMonth(nextMonth.getMonth(), nextMonth.getFullYear(), utils.dateFromInput(self), utils.dateFromInput(opt.companionPicker)));
            self.data('datepicker').find('.datepicker-calendar:first').remove();
          })

          .bind('showPastMonth', function () {
            var firstVisibleMonth = self.data('datepicker').find('.datepicker-calendar:first');
            var lastMonth = utils.newDSTSafeDate(firstVisibleMonth.data('year'), firstVisibleMonth.data('month') - 1, 1);
            firstVisibleMonth.before(utils.calendarForMonth(lastMonth.getMonth(), lastMonth.getFullYear(), utils.dateFromInput(self), utils.dateFromInput(opt.companionPicker)));
            self.data('datepicker').find('.datepicker-calendar:last').remove();
          })

          .bind('selectDay', function (e, a) {
            var calendar = a.closest('.datepicker-calendar');
            var selectedDate = utils.newDSTSafeDate(calendar.data('year'), calendar.data('month'), a.text());

            self.data('datepicker').find('.selected').removeClass('selected');
            a.closest('td').addClass('selected');

            self.triggerHandler('writeOutDate', [selectedDate]);
            self.triggerHandler('removeDatePicker', [selectedDate]);
            self.triggerHandler('removeDateError');
          })

          .bind('writeOutDate', function (e, date) {
            if (date) {
              self.val(utils.getDateString(date));
            }
          })

          .bind('setCalculateMaxDateBasedOnCompanion', function(e, newValue) {
            opt.calculateMaxDateBasedOnCompanion = newValue;
            if (typeof opt.calculateMaxDateBasedOnCompanion === 'function') {
              var maxDate = utils.getMaxDateBasedOnCompanion();
              var currentDate = utils.dateFromInput(self);
              if (currentDate > maxDate) {
                self.triggerHandler('writeOutDate', maxDate);
              }
            }
          })

          .bind('removeDatePicker', function (e, selectedDate) {
            if (self.data('datepicker')) {
              $(document).unbind('click.datepicker', self.data('checkForClickOutside'));
              $(document).unbind('keydown.datepicker', self.data('checkForActionKeydowns'));
              $(window).unbind('resize.datepicker', self.data('checkForWindowResize'));
              self.data('datepicker').unbind('click.datepicker');
              self.removeClass('datepicker-open');

              self.triggerHandler('datePickerRemoveStart', [selectedDate, self.data('datepicker')]);

              setTimeout(function () {
                self.data('datepicker').fadeOut(opt.fadeOutSpeed, function () {
                  self.data('datepicker').remove();
                  self.data('datepicker', null);
                  self.data('checkForClickOutside', null);

                  if (selectedDate && opt.focusNextFieldOnDateSelect) {
                    utils.nextField(self).focus();
                  }

                  self.triggerHandler('datePickerRemoveDone', [selectedDate]);
                });
              }, selectedDate ? opt.delayHideOnSelectBy : 0);
            }
          });
      });
    }
  });
})(jQuery);;
/*

jquery.datePickerForSelects.js by Jaidev Soin

This plugin configures a date picker with support for reading and writing to selects for non JS support.
For usage, options, and triggers, refer to the parent plugin.

Options passed in to this plugin are passed to parent

*/

(function ($) {

    $.fn.extend({
        datePickerForSelects: function (daySelect, monthSelect, yearSelect, opt) {
            var utils = {
                dateFromSelects: function () {
                    var day = daySelect.val();
                    var month = monthSelect.val() - 1;
                    var year = yearSelect.val();
                    return this.newDSTSafeDate(year, month, day);
                },
                maxDateFromSelects: function () {
                    var day = daySelect.find('option:last').val();
                    var month = monthSelect.find('option:last').val() - 1;
                    var year = yearSelect.find('option:last').val();
                    return this.newDSTSafeDate(year, month, day);
                },
                simplifyOrCorrectDate: function (date) {
                    // This method does two things:
                    // 1) Specific hours / minutes / seconds in the min and max dates mess with comparrisons
                    // 2) There exists a bug with certain browsers (e.g. Safari) where creating a date on the first day of daylight savings 
                    //    instead creates one for 11pm the night before. This method tests for those broken dates and fixes them.
                    //
                    // Note: Things can still go funny once a year around the hour of the DST change over. I don't think there is much
                    // we can do about this, as the Javascript Date object just is fundamentally broken. Also, the following code could shift 
                    // date by sheer luck if the time is legitimately 11pm and 0 miliseconds on DST eve, but the probability of this doing
                    // harm is very very low.

                    var dayAfter = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1);
                    var twoDaysAfter = new Date(date.getFullYear(), date.getMonth(), date.getDate() + 2);

                    var exactly11pm = date.getHours() == 23 && date.getMinutes() == 0 && date.getSeconds() == 0 && date.getMilliseconds() == 0;
                    var dayAfterSameTimezone = date.getTimezoneOffset() == dayAfter.getTimezoneOffset();
                    var offsetHoursDifference = (date.getTimezoneOffset() - twoDaysAfter.getTimezoneOffset()) / 60;
                    var twoDaysAfterLaterTimezone = offsetHoursDifference > 0;

                    if (exactly11pm && dayAfterSameTimezone && twoDaysAfterLaterTimezone) {
                        return new Date(date.getFullYear(), date.getMonth(), date.getDate() + 1, offsetHoursDifference);
                    } else {
                        return new Date(date.getFullYear(), date.getMonth(), date.getDate());
                    }
                },
                // This is to help fix a Safari 7 / iOS 7 bug referred to in utils.simplifyOrCorrectDate()
                newDSTSafeDate: function (year, month, day) {
                    var date = new Date(year, month, day);

                    if (date.getDate() != day) {
                        return utils.simplifyOrCorrectDate(date);
                    } else {
                        return date;
                    }
                }
            };

            opt = $.extend({
                headerText: "",                 // Title on the date picker panel
                panelCloseText: "Close x",      // Text displayed on close link in the date picker panel.
                panelCloseClass: "closePanel",  // Class applied to the close link in the date picker panel
                numberOfMonths: 2,              // Number of months to display in the date picker panel
                dateFormat: 'ddd mmmm yyyy',    // Date format of 1st Jan 2012
                minDate: new Date(),            // Minimum date allowed in the date picker
                maxDate: utils.maxDateFromSelects()  // Max date for the date picker, it is based on the max date the selects allow
            }, opt);

            return this.each(function () {
                var self = $(this)

                  .on('init.quotePanelDatePicker', function () {
                      // Hide selects
                      daySelect.add(monthSelect).add(yearSelect).hide();

                      // Init date from selects
                      self
                        .datePicker(opt)
                      // Add a listener to the "Close" link at the top of the datepicker
                        .on('datePickerDrawn', function (e, datePicker) {
                            datePicker.find('.' + opt.panelCloseClass).on('click', function () {
                                self.triggerHandler('removeDatePicker');
                            });
                        })

                        .on('writeOutDate', function (e, date) {
                            self.data('date', date);

                            daySelect.val(date.getDate());
                            monthSelect.val(date.getMonth() + 1);
                            yearSelect.val(date.getFullYear());
                        })

                        .trigger('writeOutDate', [utils.dateFromSelects()]);
                  });

                self.triggerHandler('init.quotePanelDatePicker');
            });
        }
    });
})(jQuery);;
(function ($) {
    $.fn.extend({
        dropdownPicker: function ($pickerAddButton, $hiddenSelectedItemsInput, $selectedItemsElement, opt) {
            opt = $.extend({
                cantFindItemErrorMessage: "Sorry, we don't recognise this: (#{item}). Please try again.",
                pickerInputClass: "pickerInput",
                destinationInputClass: "",
                hiddenInputItemDelimiter: "|",
                aliasDelimiter: "|",
                hiddenInputAliasDelimiter: ":",
                addedItemListItemTemplate: function (item) {
                    return utils.sub("#{name}#{aliasOrBlank}<a>&times;</a>", {
                        name: item['name'],
                        aliasOrBlank: item['matchedAlias'] ? utils.sub(' (#{alias}) ', { alias: item['matchedAlias'] }) : ' '
                    });
                },
                insertError: function (errorMessage, input, fieldIdentifier) {
                    var error = $(utils.sub("<ul class='input-validation-errors'><li><span>#{message}</span></li></ul>", {
                        message: errorMessage
                    }));
                    error.insertAfter(input).hide().fadeIn();
                    return error;
                }
            }, opt);

            var utils = {
                itemsFromOptions: function (options) {
                    return $.map(options, function (option) {
                        return {
                            'id': option.value,
                            'name': option.text,
                            'sort': Number(option.getAttribute('data-sort')),
                            'aliases': (option.getAttribute('data-aliases')
                                ? option.getAttribute('data-aliases').split(opt.aliasDelimiter)
                                : null)
                        };
                    });
                },
                getItemByID: function (id, items) {
                    for (var i = 0; i < items.length; i++) {
                        if (items[i]['id'] == id) {
                            return items[i];
                        }
                    }
                    return null;
                },
                isItemInHiddenInput: function (itemToFind) {
                    return ($.grep(utils.readFromHiddenInput(), function (item) {
                        return item['id'] == itemToFind['id'];
                    }).length > 0);
                },
                addItemToHiddenInput: function (itemToAdd) {
                    var hiddenItemsData = utils.readFromHiddenInput();

                    hiddenItemsData.push({ 'id': itemToAdd['id'], 'alias': itemToAdd['matchedAlias'] });
                    utils.writeOutToHiddenInput(hiddenItemsData);
                },
                readFromHiddenInput: function () {
                    var items = [];
                    var hiddenItemsData = $hiddenSelectedItemsInput.val();

                    if (hiddenItemsData.length > 0) {
                        $.each(hiddenItemsData.split(opt.hiddenInputItemDelimiter), function (i, hiddenItemsDataItem) {
                            var split = hiddenItemsDataItem.split(opt.hiddenInputAliasDelimiter);
                            items.push({ 'id': Number(split[0]), 'alias': split[1] || null });
                        });
                    }

                    return items;
                },
                writeOutToHiddenInput: function (items) {
                    $hiddenSelectedItemsInput.val($.map(items, function (item) {
                        if (item['alias']) {
                            return item['id'] + opt.hiddenInputAliasDelimiter + item['alias'];
                        } else {
                            return item['id'];
                        }
                    }).join(opt.hiddenInputItemDelimiter));
                },
                removeItemFromHiddenInput: function (itemToRemove) {
                    utils.writeOutToHiddenInput($.grep(utils.readFromHiddenInput(), function (item) {
                        return item['id'] != itemToRemove['id'];
                    }));
                },
                sub: function (html, values) {
                    return html.replace(/#\{(\w*)\}/g, function (token, key) {
                        return values[key] || token;
                    });
                }
            };

            return this.each(function () {

                var $this = $(this);
                var destinationsPlaceholderText;

                $this
                    .on("init.picker", function () {
                        $hiddenSelectedItemsInput.removeAttr('disabled');
                        $this.attr('disabled', 'disabled');

                        var $options = $this.children().toArray();
                        destinationsPlaceholderText = $($options.shift()).text();
                        var itemsWithAliases = utils.itemsFromOptions($options);

                        var $pickerInput = $("<input/>", {
                            'type': 'text',
                            'class': opt.pickerInputClass
                        });
                        $this.hide().after($pickerInput);
                        $pickerAddButton.remove();

                        $selectedItemsElement.empty();

                        $.each(utils.readFromHiddenInput(), function (i, itemFromHiddenInput) {
                            var item = $.extend(utils.getItemByID(itemFromHiddenInput['id'], itemsWithAliases), { 'matchedAlias': itemFromHiddenInput['alias'] });

                            if (item) {
                                $this.trigger('displayItem.where', [item, true]);
                            }
                        });

                        if (Modernizr && Modernizr.input.placeholder) {
                          $pickerInput.attr('placeholder', destinationsPlaceholderText);
                        } else {
                          $pickerInput.placeholderPlus(destinationsPlaceholderText);
                        }
                        $pickerInput
                            .autocomplete(itemsWithAliases)
                            .on('itemChosen', function (e, item, textUserEntered, selectedListItem) {
                                if (selectedListItem) {
                                    $this.trigger('logToAnalytics', ["Item - Selected an autocomplete item", 'I - ' + selectedListItem.text() + ' - ' + textUserEntered]);
                                }

                                $this.trigger('addItem.where', [item]);
                                $pickerInput.val('');
                            })
                            .on('findingExactMatchesFor', function (e, text, triggeringAction) {
                                $this.trigger('logToAnalytics', ["Item - Return or tab on input no autocomplete", 'I - ' + text]);
                            })
                            .on('errorFeedback.autocomplete', function (e, type, details) {
                                $this.trigger('logToAnalytics', ['Item - Failed on exact match', 'I - ' + details.join(', ').toLowerCase()]);
                                $this.trigger('couldNotFindItemsError', [details]);
                            })
                            .on('focus', function () {
                                $this.trigger('removeError', [$pickerInput]);
                            });

                        $this.data('pickerInput', $pickerInput);
                        $this.trigger('addListeners.where');
                    })
                    .on('addItem.where', function (e, item) {
                        if (utils.isItemInHiddenInput(item)) {
                            $this.trigger('logToAnalytics', ["Item - Tried to re-add item", 'I - ' + item['name']]);
                        } else {
                            utils.addItemToHiddenInput(item);
                            $this.trigger('displayItem.where', [item]);
                            $this.trigger('logToAnalytics', ["Item - Added item", 'I - ' + item['name']]);
                        }
                    })
                    .on('displayItem.where', function (e, item, noAnimation) {
                        $('<li/>')
                        .html(opt.addedItemListItemTemplate(item))
                        .data('itemData', item)
                        .appendTo($selectedItemsElement)
                        .hide()
                        .fadeIn(noAnimation ? 0 : 500);
                    })
                    .on('addListeners.where', function () {
                        // Listen for clicks on the remove link in the destination list, and remove countries as required
                        $selectedItemsElement
                            .on('click', 'a', function () {
                                var item = $(this).closest('li');
                                utils.removeItemFromHiddenInput(item.data('itemData'));
                                $this.trigger('logToAnalytics', ["Item - Removed item", 'I - ' + item.data('itemData')['name']]);
                                item.remove();
                            });
                    })

                    .on('logToAnalytics', function (e, action, label, value) {
                        if (typeof googleAnalytics !== 'undefined') {
                            googleAnalytics.trackEvent('Item Picker', action, { 'label': label, 'value': value });
                        }
                    })
                    .on('couldNotFindItemsError', function (e, items) {
                        var itemsForError = items.join(', ');

                        var errorMessage = utils.sub(opt.cantFindItemErrorMessage, {
                            item: itemsForError
                        });

                        $this.trigger('displayError', [$this.data('pickerInput'), errorMessage, 'item']);
                        $this.data('pickerInput').val('').blur();
                    })
                    .on('displayError', function (e, input, message, fieldIdentifier) {
                        $this.trigger('removeError', [input]);
                        var error = opt.insertError(message, input, fieldIdentifier);
                        input.data('error', error);
                    })
                    .on('removeError', function (e, input) {
                        if (input.data('error')) {
                            input.data('error').remove();
                            input.removeData('error');
                        }
                    });

                $this.trigger("init.picker");
            });
        }
    });
})(jQuery);;
/*

jQuery.placeholderPlus.js by Jaidev Soin

Version 1

*/

// For future dev, maybe support:
// - Registering a form to listen to a submit method from
// - Remove placeholder if required call that has to be explicitly called instead

(function ($) {
  $.fn.extend({
    placeholderPlus: function (placeholderText, opt) {
      opt = $.extend({
        placeholderClass: 'placeholder'
      }, opt);

      return this.each(function () {
        var mouseIsDown;

        var self = $(this)

          .bind('init.placeholderPlus', function () {
            self.triggerHandler('blur.placeholderPlus');
            self.triggerHandler('addBodyListener');
          })

          .bind('focus.placeholderPlus', function(e) {
            if (self.data('defaultValue')) {
              self.select()

              if (mouseIsDown) {
                // This allows the select to persist even after a click event, but ensures the field can then be manually deselected using the mouse
                self.one('mouseup.placeholderPlus', function (e) {
                  e.preventDefault();
                });
              }
            } else if (self.val() == placeholderText) {
              self.removeClass(opt.placeholderClass).val('');
            }
          })

          .bind('mousedown.placeholderPlus', function() {
            mouseIsDown = true;
          })

          .bind('addBodyListener', function() {
            $('body').bind('mouseup.placeholderPlus', function() {
              mouseIsDown = false;
            });
          })

          .bind('blur.placeholderPlus', function() {
            if (self.data('defaultValue')) {
              self.val(self.data('defaultValue'));
            } else if (self.val() == '') {
              self.addClass(opt.placeholderClass).val(placeholderText);
            }
          })

          .bind('setDefaultValue', function(e, defaultValue) {
            self.val(defaultValue).removeClass(opt.placeholderClass);
            self.data('defaultValue', defaultValue);
          });

        self.triggerHandler('init.placeholderPlus');
      });
    }
  });
})(jQuery);
;
/*globals window:false, jQuery:false */

(function ($) {

  $.fn.extend({
    contentAjax: function (options) {
      var
        opt = $.extend({
          bindSubmits: true,                                                    // Whether to hook into submits matching the selector and POST using AJAX
          bindLinks: true,                                                      // Whether to hook into anchors matching the selector and GET using AJAX
          bindManualUpdates: true,                                              // Whether to hook up other types of elements to auto-POST using AJAX upon change
          initialAjaxGetUrl: null,                                              // The URL for an AJAX GET to be done upon init. If null, no intial AJAX GET happens.
          submitSelector: 'input[type=submit], button[type!=button]',           // The selector for submits that need to POST using AJAX
          linkSelector: 'a.load-by-ajax',                                       // The selector for links that need to GET using AJAX
          manualUpdateSelector: '.manual-update-trigger',                       // The selector for other types of elements that need to auto-POST using AJAX
          indicateAjaxInProgressFn: wng.animations.showWhitewashLoadingIndicator, // Function for indicating that AJAX operation is starting, default adds whitewash and spinner.
          indicateAjaxCompleteFn: wng.animations.removeWhitewashLoadingIndicator, // Function for indicating that AJAX operation has completed, default hides spinner.
          modifyUrl: function (url) { return url; },                             // Function to modify (e.g. add params) to the AJAX GET/POST URL.
          showAjaxError: function (errorMessageHtml) { // Function to show failure error message. Default appends to global messages.
            var $messagesPanel = $('#global-messages');

            if ($messagesPanel.length > 0) {
              $messagesPanel.replaceWith(errorMessageHtml);
            } else {
              $('.global-messages-container').html(errorMessageHtml);
            }
          }
        }, options),

        indicateAjaxInProgress = function ($caller, contentData) {
          opt.indicateAjaxInProgressFn($caller);
          return $caller;
        },

        indicateAjaxComplete = function (contentData) {
          if (contentData.redirectUrl === undefined) {
            // We only want to indicate completion of AJAX if not redirecting anywhere.
            // Otherwise, the indicator disappears while the redirectUrl loads. Looks odd.
            opt.indicateAjaxCompleteFn();
          }
        };

      // Utility functions

      var utils = {
        replaceContent: function (contentPart) {
          var $matched;
          if (contentPart.hideId !== undefined) {
            $matched = $('#' + contentPart.hideId);
            if ($matched.length > 0) {
              $matched.css("display", "none");
              return;
            }
          }

          if (contentPart.hideClass !== undefined) {
            $matched = $('.' + contentPart.hideClass);
            if ($matched.length > 0) {
              $matched.css("display", "none");
              return;
            }
          }

          if (contentPart.showId !== undefined) {
            $matched = $('#' + contentPart.showId);
            if ($matched.length > 0) {
              $matched.css("display", "block");
              return;
            }
          }

          if (contentPart.showClass !== undefined) {
            $matched = $('.' + contentPart.showClass);
            if ($matched.length > 0) {
              $matched.css("display", "block");
              return;
            }
          }

          var newContent = $.trim(contentPart.content.html);

          if (contentPart.withinId !== undefined) {
            $matched = $('#' + contentPart.withinId);
            if ($matched.length > 0) {
              $matched.html(newContent);
              return;
            }
          }

          if (contentPart.replaceId !== undefined) {
            $matched = $('#' + contentPart.replaceId);
            if ($matched.length > 0) {
              $matched.replaceWith(newContent);
              return;
            }
          }

          if (contentPart.afterId !== undefined) {
            $matched = $('#' + contentPart.afterId);
            if ($matched.length > 0) {
              $matched.after(newContent);
              return;
            }
          }

          if (contentPart.beforeId !== undefined) {
            $matched = $('#' + contentPart.beforeId);
            if ($matched.length > 0) {
              $matched.before(newContent);
              return;
            }
          }

          if (contentPart.withinClass !== undefined) {
            $matched = $('.' + contentPart.withinClass);
            if ($matched.length > 0) {
              $matched.html(newContent);
              return;
            }
          }

          if (contentPart.replaceClass !== undefined) {
            $matched = $('.' + contentPart.replaceClass);
            if ($matched.length > 0) {
              $matched.replaceWith(newContent);
              return;
            }
          }

          if (contentPart.afterClass !== undefined) {
            $matched = $('.' + contentPart.afterClass);
            if ($matched.length > 0) {
              $matched.after(newContent);
              return;
            }
          }

          if (contentPart.beforeClass !== undefined) {
            $matched = $('.' + contentPart.beforeClass);
            if ($matched.length > 0) {
              $matched.before(newContent);
              return;
            }
          }
        }
      };

      // Set up handlers for each item JQuery object in the set.

      return this.each(function () {

        var $self = $(this)

          // Event Handlers

          .on('init.contentAjax', function () {
            if (opt.initialAjaxGetUrl) {
              $self.trigger('doAjaxGet.contentAjax', [null, opt.initialAjaxGetUrl]);
            } else {
              if (opt.bindSubmits) {
                $self.trigger('bindSubmits.contentAjax');
              }
              if (opt.bindLinks) {
                $self.trigger('bindLinks.contentAjax');
              }
              if (opt.bindManualUpdates) {
                $self.trigger('bindManualUpdates.contentAjax');
              }
            }
          })

          .on('bindSubmits.contentAjax', function () {
            var $clickableElements = $(opt.submitSelector, $self);
            $clickableElements.off('click.contentAjax');
            $clickableElements.on('click.contentAjax', function (ev) {
              ev.preventDefault();
              $self.trigger('doAjaxPost.contentAjax', [$(this)]);
              return false;
            });
          })

          .on('bindLinks.contentAjax', function () {
            var $clickableElements = $(opt.linkSelector, $self);
            $clickableElements.off('click.contentAjax');
            $clickableElements.on('click.contentAjax', function (ev) {
              ev.preventDefault();
              $self.trigger('doAjaxGet.contentAjax', [$(this)]);
              return false;
            });
          })

          .on('bindManualUpdates.contentAjax', function () {
            var $manualUpdateTriggers = $(opt.manualUpdateSelector, $self);
            $manualUpdateTriggers.each(function () {
              var $manualUpdateTrigger, $manualUpdateContainer;
              $manualUpdateTrigger = $(this);
              $manualUpdateContainer =
                $manualUpdateTrigger.closest('.manual-update-container');

              $manualUpdateContainer.find('.manual-update').hide();

              if ($manualUpdateTrigger.attr("type") == 'text') {
                $manualUpdateTrigger.off('blur.contentAjax');
                $manualUpdateTrigger.on('blur.contentAjax', function () {
                  $manualUpdateContainer.find('input[type="submit"]').eq(0).click();
                });
              } else {
                $manualUpdateTrigger.off('change.contentAjax');
                $manualUpdateTrigger.on('change.contentAjax', function () {
                  $manualUpdateContainer.find('input[type="submit"]').eq(0).click();
                });
              }
            });
          })

          .on('doAjaxPost.contentAjax', function (e, $caller) {
            var $form, url, formData;
            $self.trigger('beforeGetFormData.contentAjax', [$caller]);
            $form = $caller.parents('form');
            $form.data("isValid", true);
            $form.trigger("validateQuotePanel");
            if ($form.data("isValid") === true) {
              url = opt.modifyUrl($form.attr('action'));
              $form.find("input[placeholder]").trigger('submitCheck.placeholder');
              formData = $form.serialize();
              // JQuery does not include the submit button in the serialized form, but we need it.
              formData += '&' + $caller.attr('name') + '=' + $caller.attr('value');
              $self.trigger('afterGetFormData.contentAjax', [$caller]);
              indicateAjaxInProgress($caller);
              $.ajax({
                type: 'POST',
                url: url,
                cache: false,
                data: formData,
                dataType: 'json',
                success: function (contentData) {
                  $self.trigger('ajaxSuccessful.contentAjax', [contentData]);
                },
                error: function (contentData) {
                  $self.trigger('ajaxFailed.contentAjax', [contentData]);
                }
              });
            }
          })

          .on('doAjaxGet.contentAjax', function (e, $caller, url) {
            if (url === undefined || url === null) {
              url = $caller.attr('href');
            }
            url = opt.modifyUrl(url);
            if ($caller !== undefined && $caller !== null) {
              indicateAjaxInProgress($caller);
            }
            $.ajax({
              type: 'GET',
              url: url,
              cache: false,
              data: null,
              dataType: 'json',
              success: function (contentData) {
                $self.trigger('ajaxSuccessful.contentAjax', [contentData]);
              },
              error: function (contentData) {
                $self.trigger('ajaxFailed.contentAjax', [contentData]);
              }
            });
          })

          .on('ajaxSuccessful.contentAjax', function (e, contentData) {
            $self.trigger('beforeReplaceContent.contentAjax', [contentData]);

            // Redirect URL trumps everything, so check that first.
            if (contentData.redirectUrl !== undefined) {
              window.location.href = contentData.redirectUrl;
              return;
            }

            // Not navigating. Page content will now be modified. 

            // Start by removing spinners and any ajax operation indicators
            indicateAjaxComplete(contentData);

            // Iterate over each content block and perform replacement.
            try {
              $.each(contentData.parts, function (i, contentPart) {
                utils.replaceContent(contentPart);
              });
              if (opt.bindSubmits) {
                $self.trigger('bindSubmits.contentAjax');
              }
              if (opt.bindLinks) {
                $self.trigger('bindLinks.contentAjax');
              }
              if (opt.bindManualUpdates) {
                $self.trigger('bindManualUpdates.contentAjax');
              }
            }
            catch (err) {
              $self.trigger('ajaxFailed.contentAjax', [contentData]);
            }

            $self.trigger('afterReplaceContent.contentAjax', [contentData]);
          })

          .on('ajaxFailed.contentAjax', function (e, contentData) {
            var errorMessageHtml =
              '<div id="global-messages">' +
              '<div class="messages error">' +
              '<h3>Oops!</h3>' +
              '<ul>' +
              '<li>' +
              'Something went wrong. Please try again.' +
              '</li>' +
              '</ul>' +
              '</div>' +
              '</div>';

            // Remove spinners and any ajax operation indicators
            indicateAjaxComplete(contentData);

            opt.showAjaxError(errorMessageHtml);
          });


        // On plugin load, fire off init

        $self.trigger('init.contentAjax');
      });
    }
  });
})(jQuery);
;
(function ($) {
  $.fn.extend({
    popup: function(options) {
      var

        opt = $.extend({
          url: null,
          popupDivId: 'popup',
          showOverlay: true,
          respondToEsc: true,
          popupLinkSelector: null,
          ajaxPostSelector: "input[type=submit]",
          ajaxGetSelector: "a.load-by-ajax",
          closeButtonSelector: ".close"
        }, options),

        popupContentDivId = 'popup-content',

        appendToElement = $('body'),

        $popupDiv = $("<div></div>")
          .attr('class', 'javascript-popup loading')
          .attr('id', opt.popupDivId),

        $popupContentDiv = $("<div></div>")
          .attr('class', 'javascript-popup-content')
          .attr('id', popupContentDivId),

        $overlayDiv = $("<div></div>")
          .attr('class', 'javascript-popup-overlay'),

        $closeButton = $("<a href='#'>&times; Close</a>")
          .attr('class', 'close');

      // Set up handlers for each item JQuery object in the set.

      return this.each(function() {

        var $self = $(this)

          // Event handlers
          .on('init.popup', function () {
            // Set up click event.
            $self.on('click.popup', opt.popupLinkSelector, function (e) {
              // Popup function is actually attached to the container of the link that needs 
              // to open the popup, and we use event delegation to pick up whether it was bubbled 
              // up by the link. This is done rather than hooking into the link directly because 
              // the link itself might be replaced by AJAX operations. This also assumes use of
              // withinId as opposed to replaceId.
              var $caller = $(e.currentTarget);
              e.preventDefault();
              $self.trigger('setupClose.popup');
              $self.trigger('display.popup');
              $self.trigger('setupAjax.popup', [$caller]);
            });
          })

          .on('display.popup', function () {
            // Render the overlay
            if (opt.showOverlay == true) {
              $overlayDiv.appendTo(appendToElement);
            }

            $popupContentDiv.empty();
            $popupContentDiv.appendTo($popupDiv);
            $popupDiv.appendTo(appendToElement);
          })

          .on('setupAjax.popup', function (e, $caller) {
            var url = opt.url;
            if (url === undefined || url === null) {
              if ($caller.is('a')) {
                url = $caller.attr('href');
              }
            }

            $popupContentDiv.contentAjax({
              initialAjaxGetUrl: url,
              indicateAjaxInProgressFn: wng.animations.showInlineLoadingIndicator,
              indicateAjaxCompleteFn: wng.animations.removeInlineLoadingIndicator
            })
              .on('beforeReplaceContent.contentAjax', function(e, contentData) {
                // Close this popup if so flagged in the data.
                if (contentData.redirectUrl !== undefined || contentData.closePopup === true) {
                  $self.trigger('close.popup');
                }
                $popupDiv.removeClass('loading');
              })
              .on('afterReplaceContent.contentAjax', function() {
                // This popup may now be closed or its contents might have been replaced.
                // So reselect the popupContentDiv from the DOM.
                $popupContentDiv = $('#' + popupContentDivId);
              });
          })

          .on('setupClose.popup', function () {
            // Set up close button functionality
            $closeButton.appendTo($popupDiv);
            $closeButton.on('click', function (e) {
              e.preventDefault();
              $self.trigger('close.popup');
              return false;
            });
            if (opt.respondToEsc) {
              appendToElement.on('keyup', function (keyup) {
                if (keyup.which == 27) { // Escape key
                  $self.trigger('close.popup');
                }
              });
            }
          })

          .on('close.popup', function () {
            appendToElement.off('keyup');
            $popupDiv.remove();
            $overlayDiv.remove();
          });


        // On plugin load, fire off init

        $self.trigger('init.popup');
      });
    }
  });
})(jQuery);
;
/*

jquery.countryPicker.js by Jaidev Soin

Version 2

What plugin does: Provides interactions between the page and the autocompleter plugins, and provides analytics logging.

Dependencies: jquery.autocomplete.js, google_analytics.js.

Triggers you might want to listen for:

displayCountry.where(country)
displayError(input, message, fieldIdentifier)
  
Triggers you might want to use yourself:
 
addCountry.where(country, alias)
removeError(input)
 
Validations performed by this plugin:
Displays an error if the user enters a country it does not recognise
Displays an error if the user tried to add a world region
Displays an error if the user submits the form with no countries added (after checking for text in the country input and trying to add it as a country if present)
Will not allow submission of the form if any errors that it inserted are present
Note: Country errors are cleared when the user focuses on the country input

Naming conventions for the various country data:
Any country data relating to the hidden country input is labeled as such, eg: hiddenCountriesData, or countryFromHiddenInput. This is of the form { id: id, alias: (optional)'alias' }
Anything labeled as "country" is of the form { id: id, name: 'name', aliases: [aliases] }

Destination data to be passed in:
- Destinations supplied in a select with option format: <option val='ID' data-sort='(optional)SORT_INDEX' data-aliases='(optional)ALIASES_DELIMITED_BY_|'>COUNTRY_NAME</option>

*/

(function ($) {

  // Note: these custom errors will only trigger if there is an error in the first place.
  // This means that if the quote panel has a country called "asia" there will be no error.
  var customErrorLocations = {
    "europe": "Europe",
    "pacific": "the Pacific",
    "south pacific": "South Pacific",
    "south america": "South America",
    "north america": "North America",
    "asia": "Asia"
  };

  $.fn.extend({
    countryPicker: function (destinationSelect, destinationAdd, hiddenSelectedCountriesInput, selectedCountriesElement, opt) {
      opt = $.extend({
        initialiseFromPreviousQuoteCookie: true,     // Whether to use a previous quote cookie (if it exists) to set the initial field values.
        previousQuoteCookieName: "PreviousQuoteDetails",  // The cookie that contains the previous quote data. If null does not look at cookies.
        noDestinationsAddedErrorMessage: "Please enter in a country you are travelling to.", // Error displayed if the user submits the quote without adding a country
        cantFindCountryErrorMessage: "Sorry, we don't recognise this country (#{country}). Please try again.",  // Error displayed when the country selector can't find a country user entered
        cantFindRegionErrorMessage: "Please enter which countries in #{region} you are travelling to",          // Error displayed when the country selector knows the user tried to add a region
        panelAlignment: 'left',                      // On which side of the inputs should the autocompleter and date picker align themselves to?
        hiddenInputCountryDelimiter: '|',            // Seperates countries within the hidden input
        hiddenInputAliasDelimiter: ':',              // Seperates country ids from aliases within the hidden country input
        destinationInputId: 'destination-autocomplete', // Id of the country selector input field
        destinationInputClass: 'destination-autocomplete',// Class to apply to the country input field
        destinationInputTabIndex: 1,                 // Tab index of the country selector input
        showPanelClose: true,                        // Whether to add a close button to the autocomplete list shown (needed for mobile)
        panelCloseText: 'Close x',                   // The text to display on the close button on both the autocompleter and date picker
        panelCloseClass: 'closePanel',               // Class to apply to 'close' link on both the autocompleter and date picker
        anchorAutocompleterTo: null,                 // Element to anchor the autocompleter / country picker to - if null defaults to input
        onSubmitCallback: null,                      // Callback to be fired on submit after all validation hav been passed. Return value affects submit action.
        aliasDelimiter: '|',                         // Delimiter used to split the aliases found in the data attribute on destination 

        // Insert a country error message into the page. By default, this packages the error message into a ul and inserts it after the input
        //    errorMessage: a country error message to display.
        //    destinationInput: the input that autocompleteWithPanel will be applied to
        //    fieldIdentifier: string representing the field the error is being displayed for. Can be destination, departure-date, return-date
        insertError: function (errorMessage, input, fieldIdentifier) {
          var error = $(utils.sub("<ul class='input-validation-errors'><li><span>#{message}</span></li></ul>", {
            message: errorMessage
          }));
          error.insertAfter(input).hide().fadeIn();
          return error;
        },

        // Translate from the passed in countries array (structured for efficiency) to something the autocompleter can understand (structured for readbility)
        //    rawCountries: an array of raw countries
        countriesFromRaw: function (rawCountries) {
          return $.map(rawCountries, function (raw) {
            return { 'id': raw[0], 'name': raw[1], 'aliases': raw[2] || null };
          });
        },

        // Template for displaying a country that the user has selected, this forms the content of an li which is appended to the selectedCountriesElement
        //    country: a country hash, the same as what was created by countriesFromRaw
        //    alias: (optional) the alias that matches what the user was searching for
        addedCountryListItemTemplate: function (country) {
          return utils.sub("#{name}#{aliasOrBlank}<a>&times;</a>", {
            name: country['name'],
            aliasOrBlank: country['matchedAlias'] ? utils.sub(' (#{alias}) ', { alias: country['matchedAlias'] }) : ' '
          });
        }
      }, opt);

      var utils = {
        // Simple token substitution method for building strings. Usage is of the format: sub("Substitution is #{adjective}", { 'adjective': 'awesome'})
        sub: function (html, values) {
          return html.replace(/#\{(\w*)\}/g, function (token, key) {
            return values[key] || token;
          });
        },

        countriesFromOptions: function (options) {
          return $.map(options, function (option) {
            return {
              'id': option.value,
              'name': option.text,
              'sort': Number(option.getAttribute('data-sort')),
              'aliases': (option.getAttribute('data-aliases') ? option.getAttribute('data-aliases').split(opt.aliasDelimiter) : null)
            };
          });
        },

        // Looks up a country by ID from a supplied country list
        getCountryByID: function (id, countries) {
          for (var i = 0; i < countries.length; i++) {
            if (countries[i]['id'] == id) {
              return countries[i];
            }
          }

          return null;
        },

        /*
        The following utilities are all for modifying the data stored in the hidden country input. This input is
        the record of what countries (and their aliases) the user has nominated they are going to.
        */

        // Adds a country ID and alias (if required) to the hidden input containing what contries the user has selected
        addCountryToHiddenInput: function (countryToAdd) {
          var hiddenCountriesData = utils.readFromHiddenInput();
          hiddenCountriesData.push({ 'id': countryToAdd['id'], 'alias': countryToAdd['matchedAlias'] });

          utils.writeOutToHiddenInput(hiddenCountriesData);
        },

        // Removed a country from the hidden input containing what countries the user has selected
        removeCountryFromHiddenInput: function (countryToRemove) {
          utils.writeOutToHiddenInput($.grep(utils.readFromHiddenInput(), function (country) {
            return country['id'] != countryToRemove['id'];
          }));
        },

        // Returns a count of countries in the hidden input, thus the nunber of countries the user has selected
        numberOfCountriesInHiddenInput: function () {
          return utils.readFromHiddenInput().length;
        },

        // Tests if a specific country has been added to the hidden input already. Lookup is based on ID, alias is ignored
        isCountryInHiddenInput: function (countryToFind) {
          return ($.grep(utils.readFromHiddenInput(), function(country) {
            return country['id'] == countryToFind['id'];
          }).length > 0);
        },

        // Read countries from the hidden input. Data is in the form of { id: x, alias: y }
        readFromHiddenInput: function () {
          var countries = [];
          var hiddenCountriesData = hiddenSelectedCountriesInput.val();

          if (hiddenCountriesData.length > 0) {
            $.each(hiddenCountriesData.split(opt.hiddenInputCountryDelimiter), function (i, hiddenCountriesDataItem) {
              var split = hiddenCountriesDataItem.split(opt.hiddenInputAliasDelimiter);
              countries.push({ 'id': Number(split[0]), 'alias': split[1] || null });
            });
          }

          return countries;
        },

        // Helper utility utilised by the other hidden input modifying utilities to write out to the hidden input.
        writeOutToHiddenInput: function (countries) {
          hiddenSelectedCountriesInput.val($.map(countries, function (country) {
            if (country['alias']) {
              return country['id'] + opt.hiddenInputAliasDelimiter + country['alias'];
            } else {
              return country['id'];
            }
          }).join(opt.hiddenInputCountryDelimiter));
        }
      };

      return this.each(function () {
        var destinationsPlaceholderText;

        var self = $(this)

          .on('init.countryPicker', function () {
            if (opt.initialiseFromPreviousQuoteCookie) {
              self.triggerHandler('setUpInitialData');
            }
            self.triggerHandler('setUpWhereTo');
          })

        /*
        Initial values section
        */
          // Setting initial values from the previous quote cookie into the where and when fields.
          .on('setUpInitialData', function () {
            var cookie, j;
            var today, cookieDepartureDate, cookieCountryIds;

            if (opt.previousQuoteCookieName) {
              cookie = $.cookie(opt.previousQuoteCookieName);
              if (cookie) {
                //Format is: SelectedCountries={0},DepartureDate={1},ReturnDate={2},TravellerAge1={3},TravellerAge2={4},);
                var cookieParts = cookie.split(',');
                $.each(cookieParts, function(i, cookiePart) {
                  var pair = cookiePart.split('=');
                  var value = pair[1];
                  if (i == 0) {
                    cookieCountryIds = value;
                  } else if (i == 1) {
                    cookieDepartureDate = new Date(value);
                  }
                });

                today = new Date();
                // Remove time component from today.
                today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
                if (cookieDepartureDate >= today) {
                  hiddenSelectedCountriesInput.val(cookieCountryIds);
                }
              }
            }
         })

        /*
        Where to section
        */

          // Initial setup of the destination country selector. This creates the required input and inserts it into the DOM, hides the non JS stuff, 
          // loads any countries found in the hidden country input, and applies any required listeners.
          .on('setUpWhereTo', function () {
            // Enable JS controls and disable non-JS controls.
            hiddenSelectedCountriesInput.removeAttr('disabled');
            destinationSelect.attr('disabled', 'disabled');

            // Get the list of countries from the select
            var options = destinationSelect.children().toArray();
            destinationsPlaceholderText = $(options.shift()).text();
            var countriesWithAliases = utils.countriesFromOptions(options);

            // Create an input to apply autocompleteWithPanel to
            var destinationInput = $('<input/>', {
              'type': 'text',
              'class': opt.destinationInputClass,
              'id': opt.destinationInputId,
              'tabIndex': opt.destinationInputTabIndex
            });
            
            var destinationInputIcon = $('<span/>', {
              'class': 'tid-icon icon-tid-menu'
            });

            // Remove the country fields and replace with the input
            destinationSelect.hide().after(destinationInputIcon).after(destinationInput);
            destinationAdd.remove();

            // Clear country list so we can refill it
            selectedCountriesElement.empty();

            // Add any countries that were found in the hidden input
            $.each(utils.readFromHiddenInput(), function (i, countryFromHiddenInput) {
              var country = $.extend(utils.getCountryByID(countryFromHiddenInput['id'], countriesWithAliases), { 'matchedAlias': countryFromHiddenInput['alias'] });

              if (country) {
                self.trigger('displayCountry.where', [country, true]);
              }
            });

            var autocompleterInstructions = $("<div/>"); // Empty div for now

            // Set up the autocompleter - Note placeholder plus must be applied first for correct ordering of bound focus event
            if (Modernizr && Modernizr.input.placeholder) {
              destinationInput.attr('placeholder', destinationsPlaceholderText);
            } else {
              destinationInput.placeholderPlus(destinationsPlaceholderText);
            }
            destinationInput
              .autocomplete(countriesWithAliases, { instructions: autocompleterInstructions })
              // Put in a close button for the mobile users
              .on("autocompleterShown", function (e, autocompleter) {
                if (opt.showPanelClose) {
                  var $closeButton = $("<span/>")
                    .html(opt.panelCloseText)
                    .addClass(panelCloseClass);
                  $closeButton.on("click", function () {
                    destinationInput.triggerHandler("removeAutocompleter");
                  });
                  $(autocompleter).append($closeButton);
                }
              })
              // Fired when the user selects something using the country selector
              .on('itemChosen', function (e, country, textUserEntered, selectedListItem) {
                // If the user selected from the autocompelter, log that this happened
                if (selectedListItem) {
                  self.trigger('logToAnalytics', ["Destination - Selected an autocomplete country", 'D - ' + selectedListItem.text() + ' - ' + textUserEntered]);
                }

                self.trigger('addCountry.where', [country]);
                destinationInput.val('');
              })

              // Listen for when find exact matches is being called, and log appropriately
              .on('findingExactMatchesFor', function (e, text, triggeringAction) {
                self.trigger('logToAnalytics', ["Destination - Return or tab on input no autocomplete", 'D - ' + text]);
              })

              // Listen for errors
              .bind('errorFeedback.autocomplete', function (e, type, details) {
                self.trigger('logToAnalytics', ['Destination - Failed on exact match', 'D - ' + details.join(', ').toLowerCase()]);
                self.trigger('couldNotFindCountriesError', [details]);
              })

              // Clear errors when the user selects the input
              .on('focus', function () {
                self.trigger('removeError', [destinationInput]);
              });

            self.data('destinationInput', destinationInput);
            self.trigger('addListeners.where');
          })

          // Add a country to the interface / form
          .on('addCountry.where', function (e, country) {
            if (utils.isCountryInHiddenInput(country)) {
              self.trigger('logToAnalytics', ["Destination - Tried to re-add country", 'D - ' + country['name']]);
            } else {
              utils.addCountryToHiddenInput(country);
              self.trigger('displayCountry.where', [country]);
              self.trigger('logToAnalytics', ["Destination - Added country", 'D - ' + country['name']]);
            }
          })

          // Adds a country to the "Countries you are travelling to list"
          .on('displayCountry.where', function (e, country, noAnimation) {
            $('<li/>')
              .html(opt.addedCountryListItemTemplate(country))
              .data('countryData', country)
              .appendTo(selectedCountriesElement)
              .hide()
              .fadeIn(noAnimation ? 0 : 500);
          })

          // Add any listeners required for support of country adding / removal. 
          .on('addListeners.where', function () {
            // Listen for clicks on the remove link in the destination list, and remove countries as required
            selectedCountriesElement
              .on('click', 'a', function () {
                var item = $(this).closest('li');
                utils.removeCountryFromHiddenInput(item.data('countryData'));
                self.trigger('logToAnalytics', ["Destination - Removed country", 'D - ' + item.data('countryData')['name']]);
                item.remove();
              });
          })

          // For when the contrySelector couldn't find a match for something the user entered. Checks if the string the error
          // occured for is a custom error location, and whether it should display a custom error messgae
          .on('couldNotFindCountriesError', function (e, countries) {
            var countriesForError = countries.join(', ');

            if (customErrorLocations[countriesForError.toLowerCase()]) {
              var errorMessage = utils.sub(opt.cantFindRegionErrorMessage, {
                region: customErrorLocations[countriesForError.toLowerCase()]
              });
            } else {
              var errorMessage = utils.sub(opt.cantFindCountryErrorMessage, {
                country: countriesForError
              });
            }

            self.trigger('displayError', [self.data('destinationInput'), errorMessage, 'destination']);
            self.data('destinationInput').val('').blur();
          })

        /*
        General triggers section
        */

          // Display an error message. Inserts in to the page using the callback specified in options.
          .on('displayError', function (e, input, message, fieldIdentifier) {
            self.trigger('removeError', [input]);
            var error = opt.insertError(message, input, fieldIdentifier);
            input.data('error', error);
          })

          // Removes an error message
          .on('removeError', function (e, input) {
            if (input.data('error')) {
              input.data('error').remove();
              input.removeData('error');
            }
          })

          // Log a quote panel event to google analytics
          .on('logToAnalytics', function (e, action, label, value) {
            if (typeof googleAnalytics !== 'undefined') {
              googleAnalytics.trackEvent('Quote Panel', action, { 'label': label, 'value': value });
            }
          });

        // On plugin load, fire off init
        self.trigger('init.countryPicker');
      });
    }
  });
})(jQuery);;
/*globals wng:false, jQuery:false */

/*

jquery.quotePanel.js by Jaidev Soin

Version 2 (Includes latest changes adapted from World Nomads quote panel code, and implemented on TID AU)

What plugin does: Provides interactions between the page and the datepicker / autocompleter plugins, and provides analytics logging.

Dependencies: jquery.autocompleteWithPanel.js, jquery.autocomplete.js, jquery.datePickerForSelects.js, jquery.datePicker.js, google_analytics.js.

Triggers you might want to listen for:

displayCountry.where(country)
displayError(input, message, fieldIdentifier)
  
Triggers you might want to use yourself:
 
addCountry.where(country, alias)
removeError(input)
 
Validations performed by this plugin:
Displays an error if the user enters a country it does not recognise
Displays an error if the user tried to add a world region
Displays an error if the user submits the form with no countries added (after checking for text in the country input and trying to add it as a country if present)
Will not allow submission of the form if any errors that it inserted are present
Note: Country errors are cleared when the user focuses on the country input

Naming conventions for the various country data:
Any country data relating to the hidden country input is labeled as such, eg: hiddenCountriesData, or countryFromHiddenInput. This is of the form { id: id, alias: (optional)'alias' }
Anything labeled as "country" is of the form { id: id, name: 'name', aliases: [aliases] }

Destination data to be passed in:
- Destinations supplied in a select with option format: <option val='ID' data-sort='(optional)SORT_INDEX' data-aliases='(optional)ALIASES_DELIMITED_BY_|'>COUNTRY_NAME</option>

Durations:
- Durations are supplied as an object of the form of { years: x, months: y, days: z }
- Only one key/value pair required, but works with all 3
- Increments dates by this value in an inclusive fashion
- Months are incremented based on the calendar, so one month from the 4th March is the 3rd of April (calculated as the 4th of April, -1 day for inclusive)

TODO:
- Might need some extra custom code to handle any validations on the page when this loads - might not, we will see!

*/

(function ($) {

  // Note: these custom errors will only trigger if there is an error in the first place.
  // This means that if the quote panel has a country called "asia" there will be no error.
  var customErrorLocations = {
    "europe": "Europe",
    "pacific": "the Pacific",
    "south pacific": "South Pacific",
    "south america": "South America",
    "north america": "North America",
    "asia": "Asia"
  };

  $.fn.extend({
    quotePanel: function (destinationSelect, destinationAdd, hiddenSelectedCountriesInput, panelCountryGroups, departureDateDay, departureDateMonth, departureDateYear, returnDateDay, returnDateMonth, returnDateYear, selectedCountriesElement, travellerAges, collapseButtons, maxTimeUntilPolicyStart, maxPolicyDurations, opt) {
      opt = $.extend({
        initialiseFromPreviousQuoteCookie: true,    // Whether to use a previous quote cookie (if it exists) to set the initial field values.
        previousQuoteCookieName: "PreviousQuoteDetails",  // The cookie that contains the previous quote data. If null does not look at cookies.
        noDestinationsAddedErrorMessage: "Please enter in a country you are travelling to.", // Error displayed if the user submits the quote without adding a country
        noTravellerAgeSelectedErrorMessage: "Please enter a valid adult traveller age.", // Error displayed if a user submits a quote without at least one traveller age value entered
        cantFindCountryErrorMessage: "Sorry, we don't recognise this country (#{country}). Please try again.",  // Error displayed when the country selector can't find a country user entered
        cantFindRegionErrorMessage: "Please enter which countries in #{region} you are travelling to",          // Error displayed when the country selector knows the user tried to add a region
        panelAlignment: 'left',                     // On which side of the inputs should the autocompleter and date picker align themselves to?
        hiddenInputCountryDelimiter: '|',           // Seperates countries within the hidden input
        hiddenInputAliasDelimiter: ':',             // Seperates country ids from aliases within the hidden country input
        destinationInputId: 'destination-autocomplete', // Id of the country selector input field
        destinationInputClass: 'destination-autocomplete',// Class to apply to the country input field
        destinationInputTabIndex: 1,                // Tab index of the country selector input
        panelCloseClass: 'closePanel',              // Class to apply to 'close' link on both the autocompleter and date picker
        departureDateInputId: 'departure-date',     // Id of the departure date input field
        departureDateInputClass: 'departure-date',  // Class to apply to the departure date input field
        departureDateInputTabIndex: null,           // Tab index of the departure date input
        returnDateInputId: 'return-date',           // Id of the return date input field
        returnDateInputClass: 'return-date',        // Class to apply to the return date input field
        returnDateInputTabIndex: null,              // Tab index of the return date input. null indicates that no index be set.
        departureDateInputHeaderText: "<strong>Select</strong> your <strong>departing</strong> date", // Header to put on the departure date datepicker
        returnDateInputHeaderText: "<strong>Select</strong> your <strong>returning</strong> date",    // Header to put on the return date datepicker
        anchorAutocompleterTo: null,                 // Element to anchor the autocompleter / country picker to - if null defaults to input
        preventManualEntryOnDatePickers: true,       // Prevents manual date entry on datepickers
        dateFormat: 'ddd mmmm yyyy',                 // Date format of 1st Jan 2012
        onSubmitCallback: null,                      // Callback to be fired on submit after all validation hav been passed. Return value affects submit action.
        aliasDelimiter: '|',                         // Delimiter used to split the aliases found in the data attribute on destination 
        collapseClass: 'collapse',                   // Class to apply to a block that needs to be collapsible (hide/show with a slide)
        countryPillow: {
          override: false,
          pillow: {
            displayStyle: 'inline-block',
            cssClass: 'Pill Pill--primary'
          },
          content: {
            text: '',
            cssClass: 'Pill-close icon-tid-close'
          }
        },
        destinationErrorLocator: function (input) {
          return $(input).first().closest('.input-group');
        },
        errorTemplate: "<ul class='input-validation-errors'><li><span>#{message}</span></li></ul>",

        // Insert a country error message into the page. By default, this packages the error message into a ul and inserts it after the input
        //    errorMessage: a country error message to display.
        //    destinationInput: the input that autocompleteWithPanel will be applied to
        //    fieldIdentifier: string representing the field the error is being displayed for. Can be destination, departure-date, return-date
        insertError: function (errorMessage, input, fieldIdentifier) {
          var error = $(utils.sub(opt.errorTemplate, {
            message: errorMessage
          }));
          if (fieldIdentifier === 'destination') {
            error.insertAfter(opt.destinationErrorLocator(input)).hide().fadeIn();
          } else {
            error.insertAfter($(input).first().closest('.input-group')).hide().fadeIn();
          }
          return error;
        },

        // Translate from the passed in countries array (structured for efficiency) to something the autocompleter can understand (structured for readbility)
        //    rawCountries: an array of raw countries
        countriesFromRaw: function (rawCountries) {
          return $.map(rawCountries, function (raw) {
            return { 'id': raw[0], 'name': raw[1], 'aliases': raw[2] || null };
          });
        },

        // Template for displaying a country that the user has selected, this forms the content of an li which is appended to the selectedCountriesElement
        //    country: a country hash, the same as what was created by countriesFromRaw
        //    alias: (optional) the alias that matches what the user was searching for
        addedCountryListItemTemplate: function (country) {
          if (opt.countryPillow.override === true) {
            var text = opt.countryPillow.content.text;
            var cssClass = opt.countryPillow.content.cssClass;
            return utils.sub("#{name}#{aliasOrBlank}<a class='" + cssClass + "'>" + text + "</a>", {
              name: country['name'],
              aliasOrBlank: country['matchedAlias'] ? utils.sub(' (#{alias}) ', { alias: country['matchedAlias'] }) : ' '
            });
          } else {
            return utils.sub("#{name}#{aliasOrBlank}<a>&times;</a>", {
              name: country['name'],
              aliasOrBlank: country['matchedAlias'] ? utils.sub(' (#{alias}) ', { alias: country['matchedAlias'] }) : ' '
            });
          }
        },

        autocompleteWithPanelOptionsOverride: {}, // Should not normally be required, only for forcing custom behavior in non-standard cases
        departingDatePickerOptionsOverride: {},   // Should not normally be required, only for forcing custom behavior in non-standard cases
        returningDatePickerOptionsOverride: {}    // Should not normally be required, only for forcing custom behavior in non-standard cases
      }, opt);

      var utils = {
        // Simple token substitution method for building strings. Usage is of the format: sub("Substitution is #{adjective}", { 'adjective': 'awesome'})
        sub: function (html, values) {
          return html.replace(/#\{(\w*)\}/g, function (token, key) {
            return values[key] || token;
          });
        },

        countriesFromOptions: function (options) {
          return $.map(options, function (option) {
            return {
              'id': option.value,
              'name': option.text,
              'sort': Number(option.getAttribute('data-sort')),
              'aliases': (option.getAttribute('data-aliases') ? option.getAttribute('data-aliases').split(opt.aliasDelimiter) : null)
            };
          });
        },

        // Looks up a country by ID from a supplied country list
        getCountryByID: function (id, countries) {
          for (var i = 0; i < countries.length; i++) {
            if (countries[i]['id'] == id) {
              return countries[i];
            }
          }

          return null;
        },

        /*
        The following utilities are all for modifying the data stored in the hidden country input. This input is
        the record of what countries (and their aliases) the user has nominated they are going to.
        */

        // Adds a country ID and alias (if required) to the hidden input containing what contries the user has selected
        addCountryToHiddenInput: function (countryToAdd) {
          var hiddenCountriesData = utils.readFromHiddenInput();
          hiddenCountriesData.push({ 'id': countryToAdd['id'], 'alias': countryToAdd['matchedAlias'] });

          utils.writeOutToHiddenInput(hiddenCountriesData);
        },

        // Removed a country from the hidden input containing what countries the user has selected
        removeCountryFromHiddenInput: function (countryToRemove) {
          utils.writeOutToHiddenInput($.grep(utils.readFromHiddenInput(), function (country) {
            return country['id'] != countryToRemove['id'];
          }));
        },

        // Returns a count of countries in the hidden input, thus the nunber of countries the user has selected
        numberOfCountriesInHiddenInput: function () {
          return utils.readFromHiddenInput().length;
        },

        // Returns false if traveller ages are empty
        hasTravellerAgeEntered: function () {
          var result = false;

          $.each(travellerAges, function (index, travellerAge) {
            if ($(travellerAge).val() != "" && $.isNumeric($(travellerAge).val())) {
              result = true;
              return false;
            }
          });

          return result;
        },
        // Tests if a specific country has been added to the hidden input already. Lookup is based on ID, alias is ignored
        isCountryInHiddenInput: function (countryToFind) {
          return ($.grep(utils.readFromHiddenInput(), function (country) {
            return country['id'] == countryToFind['id'];
          }).length > 0);
        },

        // Read countries from the hidden input. Data is in the form of { id: x, alias: y }
        readFromHiddenInput: function () {
          var countries = [];
          var hiddenCountriesData = hiddenSelectedCountriesInput.val();

          if (hiddenCountriesData.length > 0) {
            $.each(hiddenCountriesData.split(opt.hiddenInputCountryDelimiter), function (i, hiddenCountriesDataItem) {
              var split = hiddenCountriesDataItem.split(opt.hiddenInputAliasDelimiter);
              countries.push({ 'id': Number(split[0]), 'alias': split[1] || null });
            });
          }

          return countries;
        },

        // Helper utility utilised by the other hidden input modifying utilities to write out to the hidden input.
        writeOutToHiddenInput: function (countries) {
          hiddenSelectedCountriesInput.val($.map(countries, function (country) {
            if (country['alias']) {
              return country['id'] + opt.hiddenInputAliasDelimiter + country['alias'];
            } else {
              return country['id'];
            }
          }).join(opt.hiddenInputCountryDelimiter));
        },

        // Takes a time period of the form { years: x, months: y, days: z } (all values are optional) and returns a 
        // function to increment a date by time period (inclusive)
        dateIncrementFunction: function (period) {
          // Currently this just deals with days, months, and years. If you wanted to deal with a more complex caculation, I suggest 
          // allowing the passed in date to be either an object of this form, or a function. 
          return function (startDate) {
            var year = startDate.getFullYear() + (period.years || 0);
            var month = startDate.getMonth() + (period.months || 0);
            var day = startDate.getDate() + (period.days || 0) - 1;

            return new Date(year, month, day);
          };
        },

        // Looks at all the passed in underwriter durations, returns a function that increments 
        // a given date by the largest duration
        largestUnderwriterDateIncrementFunction: function () {
          var today = new Date();
          return $.map(maxPolicyDurations, function (duration) {
            return utils.dateIncrementFunction(duration);
          }).sort(function (a, b) {
            return b(today) - a(today);
          })[0];
        }
      };

      return this.each(function () {
        var destinationsPlaceholderText;

        var self = $(this)

            .on('init.quotePanel', function () {
              self.data("isValid", true);
              if (opt.initialiseFromPreviousQuoteCookie) {
                self.triggerHandler('setUpInitialData');
              }
              self.triggerHandler('setUpCollapseBlocks');
              self.triggerHandler('setUpWhereTo');
              self.triggerHandler('setUpWhen');
              self.triggerHandler('setUpWho');
            })

          /*
          Initial values section
          */
            // Setting initial values from the previous quote cookie into the where and when fields.
            .on('setUpInitialData', function () {
              var cookie, j;
              var today, cookieDepartureDate, cookieReturnDate, cookieCountryIds, cookieTravellerAges = [];

              if (opt.previousQuoteCookieName) {
                cookie = $.cookie(opt.previousQuoteCookieName);
                if (cookie) {
                  //Format is: SelectedCountries={0},DepartureDate={1},ReturnDate={2},TravellerAge1={3},TravellerAge2={4},);
                  var cookieParts = cookie.split(',');
                  $.each(cookieParts, function (i, cookiePart) {
                    var pair = cookiePart.split('=');
                    var value = pair[1];
                    if (i == 0) {
                      cookieCountryIds = value;
                    } else if (i == 1) {
                      cookieDepartureDate = new Date(value);
                    } else if (i == 2) {
                      cookieReturnDate = new Date(value);
                    } else {
                      // Treat the remaining items as traveller ages.
                      cookieTravellerAges.push(value);
                    }
                  });

                  today = new Date();
                  // Remove time component from today.
                  today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
                  if (cookieDepartureDate >= today) {
                    hiddenSelectedCountriesInput.val(cookieCountryIds);
                    departureDateDay.val(cookieDepartureDate.getDate());
                    departureDateMonth.val(cookieDepartureDate.getMonth() + 1);
                    departureDateYear.val(cookieDepartureDate.getFullYear());
                    returnDateDay.val(cookieReturnDate.getDate());
                    returnDateMonth.val(cookieReturnDate.getMonth() + 1);
                    returnDateYear.val(cookieReturnDate.getFullYear());
                    for (j = 0; j < travellerAges.length && j < cookieTravellerAges.length; j++) {
                      travellerAges.eq(j).val(cookieTravellerAges[j]);
                    }
                  }
                }
              }
            })

            .on('setUpCollapseBlocks', function () {
              // Hook up collapsible blocks within the quote panel.            
              if (collapseButtons) {
                collapseButtons.click(function (e) {
                  collapseButtons.next("." + opt.collapseClass).slideToggle();
                  e.preventDefault();
                });
              }
            })

          /*
          Where to section
          */

            // Initial setup of the destination country selector. This creates the required input and inserts it into the DOM, hides the non JS stuff, 
            // loads any countries found in the hidden country input, and applies any required listeners.
            .on('setUpWhereTo', function () {
              // Enable JS controls and disable non-JS controls.
              hiddenSelectedCountriesInput.removeAttr('disabled');
              destinationSelect.attr('disabled', 'disabled');

              // Get the list of countries from the select
              var options = destinationSelect.children().toArray();
              destinationsPlaceholderText = $(options.shift()).text();
              var countriesWithAliases = utils.countriesFromOptions(options);

              // Create an input to apply autocompleteWithPanel to
              var destinationInput = $('<input/>', {
                'type': 'text',
                'class': opt.destinationInputClass,
                'id': opt.destinationInputId,
                'tabIndex': opt.destinationInputTabIndex
              });

              var destinationInputIcon = $('<span/>', {
                'class': 'tid-icon icon-tid-menu Form-input-icon icon-tid-map-marker'
              });

              // Remove the country fields and replace with the input
              destinationSelect.hide().after(destinationInputIcon).after(destinationInput);
              destinationAdd.remove();

              // Clear country list so we can refill it
              selectedCountriesElement.empty();

              // Add any countries that were found in the hidden input
              $.each(utils.readFromHiddenInput(), function (i, countryFromHiddenInput) {
                var country = $.extend(utils.getCountryByID(countryFromHiddenInput['id'], countriesWithAliases), { 'matchedAlias': countryFromHiddenInput['alias'] });

                if (country) {
                  self.trigger('displayCountry.where', [country, true]);
                }
              });

              // Set up the autocompleter - Note placeholder plus must be applied first for correct ordering of bound focus event
              if (Modernizr && Modernizr.input.placeholder) {
                destinationInput.attr('placeholder', destinationsPlaceholderText);
              } else {
                destinationInput.placeholderPlus(destinationsPlaceholderText);
              }
              destinationInput
                .autocompleteWithPanel(countriesWithAliases, panelCountryGroups, $.extend({
                  alignment: opt.panelAlignment,
                  anchorTo: opt.anchorAutocompleterTo,
                  container: opt.container,
                  scrollTo: selectedCountriesElement
                }, opt.autocompleteOptionsOverride))

                // Fired when the user selects something using the country selector
                .on('itemChosen', function (e, country, textUserEntered, selectedListItem) {
                  // If the user selected from the autocompelter, log that this happened
                  if (selectedListItem) {
                    self.trigger('logToAnalytics', ["Destination - Selected an autocomplete country", 'D - ' + selectedListItem.text() + ' - ' + textUserEntered]);
                  }

                  self.trigger('addCountry.where', [country]);
                  destinationInput.val('');
                })

                // Listen for when find exact matches is being called, and log appropriately
                .on('findingExactMatchesFor', function (e, text, triggeringAction) {
                  self.trigger('logToAnalytics', ["Destination - Return or tab on input no autocomplete", 'D - ' + text]);
                })

                // Listen for errors
                .bind('errorFeedback.autocomplete', function (e, type, details) {
                  self.trigger('logToAnalytics', ['Destination - Failed on exact match', 'D - ' + details.join(', ').toLowerCase()]);
                  self.trigger('couldNotFindCountriesError', [details]);
                })

                // Clear errors when the user selects the input
                .on('focus', function () {
                  self.trigger('removeError', [destinationInput]);
                });

              self.data('destinationInput', destinationInput);
              self.trigger('addListeners.where');
            })

            // Add a country to the interface / form
            .on('addCountry.where', function (e, country) {
              if (utils.isCountryInHiddenInput(country)) {
                self.trigger('logToAnalytics', ["Destination - Tried to re-add country", 'D - ' + country['name']]);
              } else {
                utils.addCountryToHiddenInput(country);
                self.trigger('displayCountry.where', [country]);
                self.trigger('logToAnalytics', ["Destination - Added country", 'D - ' + country['name']]);
              }
            })

            // Adds a country to the "Countries you are travelling to list"
            .on('displayCountry.where', function (e, country, noAnimation) {
              if (opt.countryPillow.override === true) {
                $('<li/>')
                .css('display', opt.countryPillow.pillow.displayStyle)
                .addClass(opt.countryPillow.pillow.cssClass)
                .html(opt.addedCountryListItemTemplate(country))
                .data('countryData', country)
                .appendTo(selectedCountriesElement)
                .fadeIn(noAnimation ? 0 : 500);
              } else {
                $('<li/>')
                .html(opt.addedCountryListItemTemplate(country))
                .data('countryData', country)
                .appendTo(selectedCountriesElement)
                .hide()
                .fadeIn(noAnimation ? 0 : 500);
              }
            })

            // Add any listeners required for support of country adding / removal. 
            .on('addListeners.where', function () {
              // Listen for clicks on the remove link in the destination list, and remove countries as required
              selectedCountriesElement
                .on('click', 'a', function () {
                  var item = $(this).closest('li');
                  utils.removeCountryFromHiddenInput(item.data('countryData'));
                  self.trigger('logToAnalytics', ["Destination - Removed country", 'D - ' + item.data('countryData')['name']]);
                  item.hide();
                  // allow other handlers to detect where the element was - so modals don't close accidentally
                  setTimeout(function () { item.remove(); }, 100);
                });
            })

            // For when the contrySelector couldn't find a match for something the user entered. Checks if the string the error
            // occured for is a custom error location, and whether it should display a custom error messgae
            .on('couldNotFindCountriesError', function (e, countries) {
              var countriesForError = countries.join(', ');
              var errorMessage;

              if (customErrorLocations[countriesForError.toLowerCase()]) {
                errorMessage = utils.sub(opt.cantFindRegionErrorMessage, {
                  region: customErrorLocations[countriesForError.toLowerCase()]
                });
              } else {
                errorMessage = utils.sub(opt.cantFindCountryErrorMessage, {
                  country: countriesForError
                });
              }

              self.trigger('displayError', [self.data('destinationInput'), errorMessage, 'destination']);
              self.data('destinationInput').val('').blur();
            })


          /*
          When section
          */

            // Initial setup of the date pickers. Responsible for creating the required inputs, inserting them into the DOM, and 
            // applying the datePickerForSelects plugin to them with the required options.
            .on('setUpWhen', function () {
              var departureDateInputIcon = $('<span/>', {
                'class': 'tid-icon icon-tid-calendar Form-input-icon'
              });

              // Create inputs to apply datePickerForSelects to
              var departureDateInput = $('<input/>', {
                'type': 'text',
                'class': opt.departureDateInputClass,
                'id': opt.departureDateInputId,
                'tabIndex': opt.departureDateInputTabIndex
              })
              // don't include this in the constructor above
              // since it will invoke the 'autocomplete' plugin
              // on the input (when jQuery version >= 1.8)
              .attr('autocomplete', 'off')
              .insertAfter(departureDateYear)
              .after(departureDateInputIcon);

              var returnDateInputIcon = $('<span/>', {
                'class': 'tid-icon icon-tid-calendar Form-input-icon'
              });

              var returnDateInput = $('<input/>', {
                'type': 'text',
                'class': opt.returnDateInputClass,
                'id': opt.returnDateInputId,
                'tabIndex': opt.returnDateInputTabIndex
              })
              // don't include this in the constructor above 
              // since it will invoke the 'autocomplete' plugin
              // on the input (when jQuery version >= 1.8)
              .attr('autocomplete', 'off')
              .insertAfter(returnDateYear)
              .after(returnDateInputIcon);

              var commonDateOptions = {
                panelCloseClass: opt.panelCloseClass,
                alignment: opt.panelAlignment,
                preventManualEntry: opt.preventManualEntryOnDatePickers,
                dateFormat: opt.dateFormat
              };

              // Set up the datePickerForSelectss
              departureDateInput
                .datePickerForSelects(departureDateDay, departureDateMonth, departureDateYear, $.extend({
                  fieldName: 'departure date',
                  companionPicker: returnDateInput,
                  headerText: opt.departureDateInputHeaderText,
                  maxDate: utils.dateIncrementFunction(maxTimeUntilPolicyStart)(new Date()),
                  container: opt.container
                }, commonDateOptions, opt.departingDatePickerOptionsOverride))

                // Listen for invalid date error
                .bind('displayDateError', function (e, errorMessage, dateEntered) {
                  self.trigger('displayError', [departureDateInput, errorMessage, 'departure-date']);
                })

                // Clear errors when a correct date has been entered
                .bind('removeDateError', function () {
                  self.trigger('removeError', [departureDateInput]);
                });

              returnDateInput
                .datePickerForSelects(returnDateDay, returnDateMonth, returnDateYear, $.extend({
                  fieldName: 'return date',
                  companionPicker: departureDateInput,
                  useCompanionDateAsMin: true,
                  startCalendarAtCompanionDate: true,
                  delayHideOnSelectBy: 500,
                  calculateMaxDateBasedOnCompanion: utils.largestUnderwriterDateIncrementFunction(),
                  headerText: opt.returnDateInputHeaderText,
                  container: opt.container
                }, commonDateOptions, opt.returningDatePickerOptionsOverride))

                // Listen for invalid date error
                .bind('displayDateError', function (e, errorMessage, dateEntered) {
                  self.trigger('displayError', [returnDateInput, errorMessage, 'return-date']);
                })

                // Clear errors when a correct date has been entered
                .bind('removeDateError', function () {
                  self.trigger('removeError', [returnDateInput]);
                });

              self.data('departureDateInput', departureDateInput);
              self.data('returnDateInput', returnDateInput);
            })

                /*
                Who section
                */

                .on('setUpWho', function () {
                  // Clear errors when the user enters a traveller age
                  travellerAges.on('focus', function () {
                    self.trigger('removeError', [travellerAges]);
                  });
                })

          /*
          General triggers section
          */

            // Display an error message. Inserts in to the page using the callback specified in options.
            .on('displayError', function (e, input, message, fieldIdentifier) {
              self.trigger('removeError', [input]);
              var error = opt.insertError(message, input, fieldIdentifier);
              input.data('error', error);
            })

            // Removes an error message
            .on('removeError', function (e, input) {

              if (input.data('error')) {
                input.data('error').remove();
                input.removeData('error');

              }

            })

            // Log a quote panel event to google analytics
            .on('logToAnalytics', function (e, action, label, value) {
              if (typeof googleAnalytics !== 'undefined') {
                wng.googleAnalytics.trackEvent('Quote Panel', action, { 'label': label, 'value': value });
              }
            })

            // Fired when the user submits the quote form. Will abort submit if validations fail.
            .on('submit.quotePanel', function () {
              var inputsToCheckForErrors = $.map(['destinationInput', 'residenceInput', 'departureDateInput', 'returnDateInput'], function (inputName) {
                return self.data(inputName);
              });

              // If there is text in the destination input, attempt to add the country
              if (self.data('destinationInput').val() != destinationsPlaceholderText) {
                // Note that this will add based on the text in the autocompleter, not based on the last
                // item the user highlighted in the autocompelter (if any). 
                self.data('destinationInput').trigger('findExactMatches');
              }

              // Don't proceed if no countries selected
              if (utils.numberOfCountriesInHiddenInput() == 0) {
                self.trigger('displayError', [self.data('destinationInput'), opt.noDestinationsAddedErrorMessage, 'destination']);
              }

              // Don't proceed if there is an error message 
              var inputsWithErrors = $.grep(inputsToCheckForErrors, function (input) {
                return !!input.data('error');
              });

            if (inputsWithErrors.length > 0) {
              return false;
            }

            // Finally fire onsubmit callback, the return of which affects the submit.
              if (opt.onSubmitCallback) {
                return opt.onSubmitCallback();
              }
            })

            .on('validateQuotePanel', function () {
            var inputsToCheckForErrors = $.map(['destinationInput', 'residenceInput', 'departureDateInput', 'returnDateInput'], function (inputName) {
              return self.data(inputName);
            });

            inputsToCheckForErrors.push(travellerAges);

            // If there is text in the destination input, attempt to add the country
            if (self.data('destinationInput').val() != destinationsPlaceholderText) {
              // Note that this will add based on the text in the autocompleter, not based on the last
              // item the user highlighted in the autocompelter (if any). 
              self.data('destinationInput').trigger('findExactMatches');
            }

            // Don't proceed if no countries selected
            if (utils.numberOfCountriesInHiddenInput() == 0) {
              self.trigger('displayError', [self.data('destinationInput'), opt.noDestinationsAddedErrorMessage, 'destination']);
            }

            // Don't proceed unless one traveller age
            if (utils.hasTravellerAgeEntered() === false) {
              self.trigger('displayError', [travellerAges, opt.noTravellerAgeSelectedErrorMessage]);
            }

            // Don't proceed if there is an error message 
            var inputsWithErrors = $.grep(inputsToCheckForErrors, function (input) {
              return !!input.data('error');
            });

            if (inputsWithErrors.length > 0) {
              self.data("isValid", false);
              return false;
            } else {
              self.data("isValid", true);
            }

          });

        // On plugin load, fire off init
        self.trigger('init.quotePanel');
      });
    }
  });
})(jQuery);
;
(function ($) {
  $.fn.extend({
    tooltip: function($tooltipPanel, options) {
      // Options

      var opt = $.extend({
        closeButtonSelector: '.close',       // The selector for the close button on the tooltip panel
        panelShownClass: 'o',                // The class that denotes that the tooltip panel is visible.
        panelHiddenClass: 'i'                // The class that denotes that the tooltip panel is invisible.
      }, options);
      
      // Set up handlers for each JQuery object in the set.

      return this.each(function () {
        
        var $self = $(this)

          .on('init.tooltip', function () {
            $tooltipPanel.hide();
            // Set up the close button.
            var $closeButton = $(opt.closeButtonSelector, $tooltipPanel);
            $closeButton
              .off('click.tooltip')
              .on('click.tooltip', function (e) {
                $tooltipPanel
                  .removeClass(opt.panelHiddenClass)
                  .addClass(opt.panelShownClass)
                  .fadeOut();
                e.preventDefault();
                return false;
              });
          })

          .on('click.tooltip', function (e) {
            $tooltipPanel
              .removeClass(opt.panelShownClass)
              .addClass(opt.panelHiddenClass)
              .fadeIn();
            e.preventDefault();
            return false;
          });

        // On plugin load, fire off init

        $self.trigger('init.tooltip');
      });
    }
  });
})(jQuery);
;
(function ($) {
  $.fn.extend({
    toggleSwitch: function($toggleValueHidden, $manualUpdate, options) {
      // Options

      var opt = $.extend({
        isEditEnabled: true,          // Whether the toggle switch is currently enabled for editing
        switchedOnClass: 'o',         // The class that denotes that the toggle is in the 'on' state.
        switchedOnValue: 'True',      // The hidden field value to set when in switched on state.
        switchedOffValue: 'False'     // The hidden field value to set when in switched off state.
      }, options);
      
      // Set up handlers for each JQuery object in the set.

      return this.each(function () {
        
        var $self = $(this)

          .on('init.toggleSwitch', function () {
            var $this;
            $this = $(this);
            if ($toggleValueHidden.attr("value") === opt.switchedOnValue) {
              $this.addClass(opt.switchedOnClass);
            }
          })

          .on('click.toggleSwitch', function () {
            var $this;
            if (opt.isEditEnabled) {
              $this = $(this);
              if ($toggleValueHidden.attr("value") === opt.switchedOffValue) {
                $this.addClass(opt.switchedOnClass);
                $toggleValueHidden.attr("value", opt.switchedOnValue);
              } else {
                $this.removeClass(opt.switchedOnClass);
                $toggleValueHidden.attr("value", opt.switchedOffValue);
              }
              if ($manualUpdate !== undefined && $manualUpdate !== null) {
                $manualUpdate.click();
              }
            }
          })

          .on('disableEdit.toggleSwitch', function() {
            opt.isEditEnabled = false;
          })

          .on('enableEdit.toggleSwitch', function() {
            opt.isEditEnabled = true;
          });

        // On plugin load, fire off init

        $self.trigger('init.toggleSwitch');
      });
    }
  });
})(jQuery);
;
(function ($) {
  $.fn.extend({
    benefitsPanel: function(options) {
      // Options

      var opt = $.extend({
        outClass: 'o',                                    // The class that denotes an opened out benefit summary block.
        inClass: 'i',                                     // The class that denotes a closed in benefit summary block.
        benefitSelector: '#benefits-list dd',             // Selector for a benefit summary block.
        toggleButtonSelector: 'dt',                       // Selector for the toggle benefit summary block button.
        contractAllLabel: '[ - ] Contract all benefits',  // Text label for the contract-all-benefits button.
        expandAllLabel: '[ + ] Expand all benefits'       // Text label for the expand-all-benefits button.
      }, options);
      
      // Set up handlers for each item JQuery object in the set.

      return this.each(function () {
        
        var $self = $(this)

          .on('init.benefitsPanel', function() {
            $(opt.benefitSelector).hide().slideable({
               "togglerSel": opt.toggleButtonSelector
            });
          })
        
          .on('click.benefitsPanel', function() {
            if ($self.hasClass(opt.outClass)) {
              $(opt.benefitSelector).trigger("slideable.open");
              $self
                .removeClass(opt.outClass)
                .addClass(opt.inClass)
                .html(opt.contractAllLabel);
            }
            else if ($self.hasClass(opt.inClass)) {
              $(opt.benefitSelector).trigger("slideable.close");
              $self
                .removeClass(opt.inClass)
                .addClass(opt.outClass)
                .html(opt.expandAllLabel);
            }
          });
        
        // On plugin load, fire off init

        $self.trigger('init.benefitsPanel');
      });
    }
  });
})(jQuery);
;
(function ($) {
  $.fn.extend({
    donationPanel: function (options) {
      // Options
      
      var opt = $.extend({
        projectImageSelector: '.project',                 // Selector for each donation project image.
        projectSelector: '.item',                         // Selector for each donation project block.
        chosenProjectClass: 'selected'                    // Class that denotes the currently chosen project.
      }, options);

      opt.chosenProjectSelector = "." + opt.chosenProjectClass;
      
      // Set up handlers for each item JQuery object in the set.

      return this.each(function () {

        var $self = $(this)

          .on('init.donationPanel', function () {
            // Mark the chosen project as selected.
            $("input[type='radio']:checked", $self)
              .closest(opt.projectSelector)
              .addClass(opt.chosenProjectClass);

            // Hook up radio click
            $("input[type='radio']", $self)
            .off('click.donationPanel')
            .on('click.donationPanel', function () {
              $(opt.chosenProjectSelector, $self)
                .removeClass(opt.chosenProjectClass);
              $(this)
                .closest(opt.projectSelector)
                .addClass(opt.chosenProjectClass);
            });

            // Hook up project block click
            $(opt.projectSelector, $self)
            .off('click.donationPanel')
            .on('click.donationPanel', function () {
              $(this).children(":radio")[0].click();
            });

            // Hook up project image click.
            $(opt.projectImageSelector, $self)
            .off('click.donationPanel')
            .on('click.donationPanel', function () {
              $(this).siblings(":radio")[0].click();
            });
          });
        
        // On plugin load, fire off init

        $self.trigger('init.donationPanel');
      });
    }
  });
})(jQuery);
;
;(function ($) {
  $.fn.extend({
    slideable: function(opt) {
      var settings = $.extend({
        'togglerSel' : '.header', // selector for the el that triggers toggle
        'traversal' : 'siblings', // where in the dom can we find the toggle trigger?
        'slideSpeed' : 'fast',
        'defaultOpen' : 'important', // class that opens slider by default onload
        'statusOpen'  : 'slideable-open', // classes for the toggle trigger
        'statusClosed' : 'slideable-closed'
      }, opt);
      
      return this.each(function () {
        var $base = $(this);

        $base.on('slideable.open', function () {
          if (!$base.is(":visible"))
             $base.trigger('slideable.toggle');
        })
        .on('slideable.close', function () {
          if ($base.is(":visible"))
             $base.trigger('slideable.toggle');
        })
        .on('slideable.toggle', function() {
          $base.slideToggle(settings.slideSpeed) // might have to refactor that to avoid jumpy animation caused by margins
          // toggle statusOpen / statusClose classes on the toggler
          .data('toggler').toggleClass(
              settings.statusClosed + " " + settings.statusOpen );
        })
        .data('toggler', $($base[settings.traversal](settings.togglerSel))); // the element that triggers the slideToggle

        $base.data('toggler').on('click.slideable', function () {
          $base.trigger('slideable.toggle');
        })
        .css('cursor', 'pointer') // should be done in css but we want a pointer in every case
        .addClass(settings.statusClosed); 

        // slide me up when i am important enough
        if ($base.hasClass(settings.defaultOpen)) 
          $base.trigger('slideable.toggle');
      });
    } 
  });
})(jQuery);
;
; (function ($) {
  $.fn.extend({
    carousel: function (opt) {
      var settings = $.extend({
        'panelWidth' : '100',
        'animationDuration' : '1000',
        'classNav' : 'carouselNav', // Class name for the carousel UL
        'classPrev' : 'carouselPrev', // Class name for the 'previous' LI
        'classNext' : 'carouselNext', // Class name for the 'next' LI
        'classDot' : 'carouselDot' // Class name for the other LIs (the dots)
      }, opt);

      var utils = {
        genMenuMarkup: function (n) {
          var markup = "<ul class='" + settings.classNav + "'><li class='" + settings.classPrev + "'>&lt;</li>";
          for (i = 1; i <= n; i++) {
            if (i == 1) {
              // Chuck active class on the first dot
              markup += "<li class='" + settings.classDot + " active'>" + i + "</li>";
            }
            else {
              markup += "<li class='" + settings.classDot + "'>" + i + "</li>";
            }
          }
          markup += "<li class='" + settings.classNext + "'>&gt;</li></ul>";
          return markup;
        },
        animateTo: function($base) {
          var currentLocation = $base.data('currentLocation');

          // Needs refactor... quickly done for footprints release
          // Remove active class from previously active li and add to new one
          $("." + settings.classNav + " .active").removeClass("active");
          $("." + settings.classNav + " ." + settings.classDot).eq(currentLocation - 1).addClass("active");

          var targetPosition = settings.panelWidth * (currentLocation - 1);
          $base.animate(
            {
              left: -targetPosition
            },
            {
              duration: settings.animationDuration,
              queue: false
            }
          );
        }
      };

      return this.each(function () {
        
        var $base = $(this);

        $base
          // Store some data about the panels in the base object
          .data('$panels', $base.children("li"))
          .data('totalPanels', $base.data('$panels').length)
          .data('currentLocation', 1)

          .on('carousel.init', function () {
            $('.carousel-wrapper').after(utils.genMenuMarkup($base.data('totalPanels')));

            $menuElements = $base.closest("div").siblings("." + settings.classNav).children("li." + settings.classDot);
            $menuElements.each(function (index) {
              $(this).data('position', index + 1);
            });

            // Remove the class from each of the image li's
            $base.children("li").each(function () {
              $image = $(this);
              if($image.hasClass("hideMe")) {
                $(this).removeClass("hideMe");
              }
            });
          })
          .on('carousel.clickDot', function (event, li) {
            $base.data('currentLocation', $(li).data('position'));
            currentLocation = $base.data('currentLocation');
            utils.animateTo($base);
          })
          .on('carousel.clickPrev', function (event, li) {
            
            if ($base.data('currentLocation') == 1) {
              $base.data('currentLocation', $base.data('totalPanels'));
            }
            else {
              $base.data('currentLocation', $base.data('currentLocation') - 1);
            }
            utils.animateTo($base);

          })
          .on('carousel.clickNext', function (event, li) {

            if ($base.data('currentLocation') == $base.data('totalPanels')) {
              $base.data('currentLocation', 1);
            }
            else {
              $base.data('currentLocation', $base.data('currentLocation') + 1);
            }
            utils.animateTo($base);

          })
          .trigger('carousel.init')

          // Traverse to the nav dots
          .closest("div").siblings("." + settings.classNav).children("li." + settings.classDot)

          .on("click", function () {
            $base.trigger('carousel.clickDot', this);
          })
          // Traverse to the Prev button
          .siblings("li." + settings.classPrev)
          .on("click", function () {
            $base.trigger('carousel.clickPrev', this);
          })

          // Traverse to the Next button
          .siblings("li." + settings.classNext)
          
          .on("click", function () {
            $base.trigger('carousel.clickNext', this);
          })
        ;

      });

    }
  });
})(jQuery);
;
/*

jquery.scrollTracking.js by Jaidev Soin

// Note: This tracking acts a bit funny on iPhone due to the way it zooms the screen. Ignore mobile in results.

*/

(function ($) {
  $.fn.extend({
    scrollTracking: function (callback) {
      return this.each(function () {
        var win = $(window);
        var scrollCheck;
        
        var self = $(this)
          .on('init.scrollTracking', function() {
            
            scrollCheck = function() {
              self.triggerHandler('checkPosition');
            };
            
            win.on('resize scroll', scrollCheck);
            
            
            self.triggerHandler('checkPosition');
          })
          
          .on('checkPosition', function() {
            if (!self.data('scrollTrackingComplete')) {
              var bottomOfElement = self.offset().top + self.outerHeight(); // Note this can change between checks due to a dynamic page
              var windowHeight = win.height();
              var scrollTop = win.scrollTop();
              var bottomOfWindow = windowHeight + scrollTop;
            
              if (bottomOfWindow >= bottomOfElement) {
                if (callback && typeof(callback) == 'function') {
                  callback();
                } else {
                  throw new Error('No callback supplied for scrollTracking or callback not a function');
                }
              
                win.off('resize scroll', scrollCheck);
                self.data('scrollTrackingComplete', true);
              }
            }
          });
          
        self.triggerHandler('init.scrollTracking');
      });
    }
  });
})(jQuery);;
// For the JSONP Trustpilot callback function;
var trustpilot_jsonp_callback;

wng.utils = (function (undefined) {
  var
    preload = function(arrayOfImages) {
      $(arrayOfImages).each(function() {
        $('<img/>')[0].src = this;
      });
    },

    navigate = function(url) {
      window.location.replace(url);
    },

    getUrlParamValue = function(key, url) {
      if (url === undefined) {
        url = window.location.search;
      }
      var re = new RegExp("[?|&]" + key + "=(.*?)&");
      var matches = re.exec(url + "&");
      if (!matches || matches.length < 2) {
        return "";
      }
      return decodeURIComponent(matches[1].replace("+", " "));
    },

    getPromoParamFromUrl = function() {
      var regex = RegExp('(?:\\?|^|&)campaign=(.+?)(?:&|$)');
      var promoParam = decodeURI(
      (regex.exec(location.search.toLowerCase()) || [, null])[1]);
      if (promoParam == "null") {
        return '';
      }
      return "campaign=" + promoParam;
    },

    addParamToUrl = function(url, param) {
      var urlSplit,
        baseUrl,
        urlQuery = '',
        paramPairs = [],
        paramPair,
        newParamSplit,
        newKey,
        newValue,
        i;

      urlSplit = url.split('?');
      if (urlSplit === undefined || urlSplit === null || urlSplit.length < 1) {
        return url;
      }
      baseUrl = urlSplit[0];
      if (baseUrl.charAt(baseUrl.length - 1) != '/') {
        baseUrl = baseUrl + '/';
      }
      if (urlSplit.length > 1) {
        urlQuery = urlSplit[1];
        paramPairs = urlQuery.split('&');
      }

      newParamSplit = param.split('=');
      if (newParamSplit === undefined || newParamSplit === null || newParamSplit.length < 2) {
        return url;
      }
      newKey = newParamSplit[0];
      newValue = newParamSplit[1];

      if (paramPairs.length == 0) {
        return baseUrl + '?' + newKey + '=' + newValue;
      } else {
        i = paramPairs.length;
        while (i--) {
          paramPair = paramPairs[i].split('=');

          if (paramPair[0] == newKey) {
            paramPair[1] = newValue;
            paramPairs[i] = paramPair.join('=');
            break;
          }
        }

        if (i < 0) {
          paramPairs[paramPairs.length] = [newKey, newValue].join('=');
        }

        return baseUrl + '?' + paramPairs.join('&');
      }
    },

    disableEnterSubmittingFormsInIE = function() {
      // Special functionality to stop enter submitting forms in <= IE8
      if ($.browser.msie && $.browser.version <= 8) {
        var disableEnterSelector = "form";
        $(disableEnterSelector).bind("keypress", function(e) {
          if (e.keyCode == 13) {
            return false;
          }
        });
      }
    },

    initTrustPilotStats = function() {
      var targetTpContainerClass = 'trustpilot-display';
      var tpJsonpUrl = 'https://s.trustpilot.com/tpelements/8255786/f.jsonp?callback=?';

      trustpilot_jsonp_callback = function(data) {

        var trustScoreRaw,
          trustScoreRating,
          trustScoreHuman,
          numberOfStars,
          $tpStarIcons,
          $tpHuman,
          $tpRating,
          $targetTpContainer;

        trustScoreRaw = data.TrustScore.Score;
        trustScoreRating = trustScoreRaw / 10 + '/10';
        trustScoreHuman = data.TrustScore.Human;
        numberOfStars = data.TrustScore.Stars;

        $tpStarIcons = $('<div/>').addClass('tp-stars stars-' + numberOfStars);
        $tpHuman = $('<span>').html(trustScoreHuman).addClass('tp-human');
        $tpRating = $('<span>').html(trustScoreRating).addClass('tp-rating');
        $targetTpContainer = $('.' + targetTpContainerClass);

        $targetTpContainer.empty()
          .append($tpStarIcons)
          .append($tpHuman)
          .append($tpRating);

      };

      $(function() {
        if ($('.' + targetTpContainerClass).length > 0) {
          $.getJSON(tpJsonpUrl);
        }
      });


    };
    
  return {
    preload: preload,
    navigate: navigate,
    getUrlParamValue: getUrlParamValue,
    getPromoParamFromUrl: getPromoParamFromUrl,
    addParamToUrl: addParamToUrl,
    disableEnterSubmittingFormsInIE: disableEnterSubmittingFormsInIE,
    initTrustPilotStats: initTrustPilotStats
  };
}());;
// CAPITALIZE Input Fields based on class='capitalize'
$(document).on('focusout', '.capitalize', function () {
    var $el = $(this),
        text = $el.val() || '';
    if (text) {
        $el.removeClass('capitalize');
        // Is text all upper case or all lower case
        // We don't want to modify it if people have already capitalized - e.g. O'Reilly, McDonald
        if ((text === text.toLowerCase()) ||
            (text === text.toUpperCase())) {
            var capitalizeText = toTitleCase(text);
            $el.val(capitalizeText);
        }
    }
});

function toTitleCase(str) {
    return str.replace(/\w+/g, function (txt) { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); });
};
/*globals $:false, document:false, wng:true */

wng.animations = (function (undefined) {
  var
    completeInlineLoadingTimeoutHandle,
    $recentlyHiddenButtonArrow,
    completeWhitewashLoadingTimeoutHandle,
    defaultWhitewashLoadingMessage = 'Please wait',
    whitewashCssClass = 'loading-overlay',
    whitewashSpinnerCssClass = 'loading-spinner',
    inlineSpinnerCssClass = 'spinner',
    loadingIndicatorMaxDurationInSeconds = 30,
    htmlSelectorHookForAppendingWhitewashLoadingDivs = '#footer',

  showInlineLoadingIndicator = function ($caller, displayWithinButton) {
    var
      inlineSpinnerHtml = "<span class='" + inlineSpinnerCssClass + "'></span>";

    if ($("." + inlineSpinnerCssClass).length < 1) {
      $caller.after(inlineSpinnerHtml);

      if (displayWithinButton) {
        $recentlyHiddenButtonArrow = $caller.closest('div').find('.icon-tid-button-arrow');
        if ($recentlyHiddenButtonArrow.length > 0) {
          $recentlyHiddenButtonArrow.css('visibility', 'hidden');
        }
      }
    }

    completeInlineLoadingTimeoutHandle = setTimeout(removeInlineLoadingIndicator,
      loadingIndicatorMaxDurationInSeconds * 1000);
  },

  removeInlineLoadingIndicator = function () {
    $("." + inlineSpinnerCssClass).fadeOut();

    if ($recentlyHiddenButtonArrow && $recentlyHiddenButtonArrow.length > 0) {
      $recentlyHiddenButtonArrow.css('visibility', 'visible');
      $recentlyHiddenButtonArrow = null;
    }

    clearTimeout(completeInlineLoadingTimeoutHandle);
  },

  showWhitewashLoadingIndicator = function ($caller, message) {
    var
      loadingMessage,
      $loadingSpinner,
      $loadingWhiteWash,
      $htmlHook;

    loadingMessage = message || defaultWhitewashLoadingMessage;

    $loadingSpinner = $("<div />")
      .addClass(whitewashSpinnerCssClass)
      .html(loadingMessage);

    $loadingWhiteWash = $("<div />")
      .addClass(whitewashCssClass)
      .append($loadingSpinner);

    $htmlHook = $(htmlSelectorHookForAppendingWhitewashLoadingDivs);
    $htmlHook.append($loadingWhiteWash);

    completeWhitewashLoadingTimeoutHandle = setTimeout(removeWhitewashLoadingIndicator,
      loadingIndicatorMaxDurationInSeconds * 1000);
  },

  removeWhitewashLoadingIndicator = function () {
    var
      $content = $(htmlSelectorHookForAppendingWhitewashLoadingDivs);

    $("." + whitewashSpinnerCssClass, $content).remove();
    $("." + whitewashCssClass, $content).remove();

    clearTimeout(completeWhitewashLoadingTimeoutHandle);
  },

  // Move focus to a given form field, and add a css class to use for any css
  // animation/transition
  moveFocus = function (fieldToFocusSelector, focusClass) {
    if ($('.input-validation-errors label, .input-validation-error').length > 0) {
      // don't move focus if there are validation errors
      return;
    }

    $(fieldToFocusSelector)
      .focus()
      .addClass(focusClass);

    setTimeout(function () {
      $(fieldToFocusSelector)
        .removeClass(focusClass);
    }, 2500);
  },

  // Bind the moveFocus function to the click event on a given input/button
  bindMoveFocusOnClick = function (subjectFieldSelector, fieldToFocusSelector, focusClass) {
    $(document).on('click', subjectFieldSelector, function (e) {
      e.preventDefault();
      moveFocus(fieldToFocusSelector, focusClass);
    });
  },

  fadeOut = function ($element, delayMilliseconds) {
    setTimeout(
      function () {
        $element.fadeOut(
          function () {
            $(this).remove();
          });
      },
      delayMilliseconds
    );
  };

  return {
    showInlineLoadingIndicator: showInlineLoadingIndicator,
    removeInlineLoadingIndicator: removeInlineLoadingIndicator,
    showWhitewashLoadingIndicator: showWhitewashLoadingIndicator,
    removeWhitewashLoadingIndicator: removeWhitewashLoadingIndicator,
    moveFocus: moveFocus,
    bindMoveFocusOnClick: bindMoveFocusOnClick,
    fadeOut: fadeOut
  };
}());
;
/*globals wng:false, wng:false, $:false, document:false */

// centralised definition of tracking events for all pages / partials

wng.googleAnalytics.tracking = (function (undefined) {
  var widgetTracking = {};

  widgetTracking.home = function (options) {
    var opt = $.extend({
    }, options);

    // General home page tracking

    $(document).on('mousedown', '[data-analytics-id="home-qqc-open"]', function () {
      wng.googleAnalytics.trackEvent('Home Page', 'Opened QQC');
    });

    $(document).on('closed.zf.reveal', '[data-analytics-id="home-qqc"]', function () {
      wng.googleAnalytics.trackEvent('Home Page', 'Closed QQC');
    });
  };

  widgetTracking.quote = function (options) {
    var opt = $.extend({
    }, options);

    // General quote page tracking

    // Clicked Choose Options button (mousedown to be sure to get in before submit or validation)
    // Use delegation from container selector since some widgets get replaced by ajax
    $(document).on('mousedown', '[data-analytics-id="quote-continue-to-options"]', function () {
      var
        $footerSummary = $(this).closest('[data-analytics-id="quote-footer-summary"]'),
        analyticsLabel = ($footerSummary.length > 0) ? 'Bottom Summary' : 'Top Summary';

      wng.googleAnalytics.trackEvent('Quote Page', 'Clicked Choose Options', {
        'label': analyticsLabel
      });
    });

    // Clicked edit trip button
    $('[data-analytics-id="open-edit-trip"]').on('click.quote', function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Clicked Edit Your Trip');
    });

    // Clicked email your quote submit (mousedown to be sure to get in before submit)
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="quote-trip-summary-section"]').on('mousedown', '[data-analytics-id="email-quote"]', function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Emailed Quote', {
        'label': 'Top Email Quote Widget'
      });
    });

    // Expanded all policy benefits
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="quote-main-section"]').on('click', '[data-analytics-id="expand-all-benefits"]', function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Expanded All Benefits');
    });

    // View the PDS above benefits
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="quote-main-section"]').on('click', '[data-analytics-id="view-pds-top"]', function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Viewed PDS', {
        'label': 'Above Benefits'
      });
    });

    // View the PDS below benefits
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="quote-main-section"]').on('click', '[data-analytics-id="view-pds-bottom"]', function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Viewed PDS', {
        'label': 'Below Benefits'
      });
    });

    // Expanded or collapsd a benefit
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="quote-main-section"]').on('click', '[data-analytics-id="benefits-list"] dt', function () {
      var $benefitName = $(this).find('[data-analytics-id="benefit-title"]');

      wng.googleAnalytics.trackEvent('Quote Page', 'Expanded or Collapsed a Benefit', {
        'label': ($benefitName.length > 0) ? $benefitName.text() : ''
      });
    });

    // Email quote (footer)
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="quote-main-section"]').on('mousedown', '[data-analytics-id="email-quote"]', function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Emailed Quote', {
        'label': 'Bottom Email Quote Widget'
      });
    });

    // Scroll tracking
    $('[data-analytics-id="quote-footer-summary"]').scrollTracking(function () {
      wng.googleAnalytics.trackEvent('Quote Page', 'Saw Bottom Quote Summary');
    });
  };

  widgetTracking.quoteOptions = function (options) {
    var opt = $.extend({
      selectorForContentAjax: '#content',
      promotionCodeContainerId: 'promotion-discount',
      termsAndConditionsClass: 'terms-and-conditions'
    }, options);

    var $recentAjaxCaller = null;

    // 
    // AJAX tracking
    //    

    $(document).on('doAjaxGet.contentAjax doAjaxPost.contentAjax', function (e, $caller) {
      $recentAjaxCaller = $caller;
    });

    $(opt.selectorForContentAjax).on('afterReplaceContent.contentAjax', function (e, data) {
      var
        code,
        i,
        $termsAndConditionsDiv,
        $termsAndConditionsSnowOptErrors,
        $termsAndConditionsOtherErrors,
        $promoCodeDiv,
        $promoCodeErrors,
        $lastTr,
        itemName,
        itemValue;

      for (i = 0; i < data.parts.length; i++) {

        // Track terms and conditions modal
        if (data.parts[i].replaceClass !== undefined &&
            data.parts[i].replaceClass === opt.termsAndConditionsClass) {
          $termsAndConditionsDiv = $('[data-analytics-id="terms-and-conditions"]');
          $termsAndConditionsSnowOptErrors =
            $('[data-analytics-id="terms-and-conditions-snow-check"] .input-validation-errors');
          $termsAndConditionsOtherErrors =
            $('[data-analytics-id="terms-and-conditions-other-check"] .input-validation-errors');

          if ($termsAndConditionsDiv.length > 0 && $termsAndConditionsSnowOptErrors.length > 0) {
            wng.googleAnalytics.trackEvent('Terms and Conditions', 'Failed to check declaration',
              {
                'label': 'No one is doing snow sports'
              }
            );
          }

          if ($termsAndConditionsDiv.length > 0 && $termsAndConditionsOtherErrors.length > 0) {
            wng.googleAnalytics.trackEvent('Terms and Conditions', 'Failed to check declaration',
              {
                'label': 'Standard declarations'
              }
            );
          }

          // Track the modal being shown initially by checking for its content
          // and for zero errors
          if ($termsAndConditionsDiv.length > 0 &&
              $termsAndConditionsDiv.find('.input-validation-errors').length === 0) {
            wng.googleAnalytics.trackVirtualPage('/virtual/purchase_path/terms_and_conditions');
          }
        }

        // Error adding specified item
        if (data.parts[i].replaceId !== undefined &&
            data.parts[i].replaceId == 'specItems') {
          if ($('#specItems .input-validation-errors').length > 0) {
            $lastTr = $('#specItems [data-analytics-id="specified-item-row"]').last();
            itemName = $lastTr.find('[data-analytics-id="item-name"] input').val() || "[No Entry Provided]";
            itemValue = $lastTr.find('[data-analytics-id="item-value"] input').val() || "[No Entry Provided]";
            wng.googleAnalytics.trackEvent('Options Page', 'Failed to Add or Update Specified Item',
              {
                'label': itemName + ' - $' + itemValue
              }
            );
          }
        }

        // Add specified item - when an item is successfully added
        if (data.parts[i].replaceId !== undefined &&
            data.parts[i].replaceId == 'specItems') {
          if ($('#specItems .input-validation-errors').length === 0 &&
              // check that the terms and conditions modal is not showing because
              // the ajax may include a refresh of specified items when in fact the 
              // terms and conditions modal is being shown as the primary action
              $('[data-analytics-id="terms-and-conditions-other-check"]').length === 0) {
            $lastTr = $('#specItems [data-analytics-id="specified-item-row"]').last();
            itemName = $lastTr.find('[data-analytics-id="item-name"] input').val();
            itemValue = $lastTr.find('[data-analytics-id="item-value"] input').val();
            // Check that this is not a new empty row
            if (itemName && itemValue) {
              wng.googleAnalytics.trackEvent('Options Page', 'Added or Updated Specified Item',
                {
                  'label': itemName + ' - $' + itemValue
                }
              );
            }
          }
        }

        if (data.parts[i].replaceId !== undefined &&
            data.parts[i].replaceId === opt.promotionCodeContainerId) {
          $promoCodeDiv = $('[data-analytics-id="promo-code"]');
          $promoCodeErrors = $promoCodeDiv.find('.input-validation-errors');

          // Promo code failed - Fire when a promo code is attempted but fails
          if ($promoCodeDiv.length > 0 && $promoCodeErrors.length > 0 &&
              $recentAjaxCaller.data('analyticsId') === 'update-promo-code') {
            code = $('[data-analytics-id="promo-code-value"]').val() || '';
            wng.googleAnalytics.trackEvent('Options Page', 'Failed on a Promo Code', {
              'label': code.toUpperCase()
            });
          }

          // Promo code success - Fire when a promo code is accepted
          if ($promoCodeDiv.length > 0 && $promoCodeErrors.length === 0 &&
              $recentAjaxCaller.data('analyticsId') === 'update-promo-code') {
            code = $('[data-analytics-id="promo-code-value"]').text() || '';
            wng.googleAnalytics.trackEvent('Options Page', 'Used a Promo Code', {
              'label': code.toUpperCase()
            });
          }

        }

      }
    });

    // 
    // Options tracking
    //

    // Use delegation from container selector since breadcrumb widget gets replaced by ajax
    $('[data-analytics-id="layout-content-container"]').on('mousedown', '[data-analytics-id="breadcrumb-goto-quote"]', function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Clicked Breadcrumb', {
        'label': 'Go back to Quote Page'
      });
    });

    // Clicked Continue button - top (mousedown to be sure to get in before submit or validation)
    $('[data-analytics-id="option-continue-top"]').on('mousedown', function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Clicked Buy Now', {
        'label': 'Top Button'
      });
    });

    // Clicked Continue button - bottom (mousedown to be sure to get in before submit or validation)
    $('[data-analytics-id="option-continue-bottom"]').on('mousedown', function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Clicked Buy Now', {
        'label': 'Bottom Button'
      });
    });

    // Chose to reduce policy excess
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on('change', '[data-analytics-id="option-reduce-policy-excess"]', function () {
      if ($(this).prop('checked')) {
        wng.googleAnalytics.trackEvent('Options Page', 'Reduced Policy Excess');
      }
    });

    // Chose to add snow sports cover
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on('change', '[data-analytics-id="option-add-snow-sports"]', function () {
      if ($(this).prop('checked')) {
        wng.googleAnalytics.trackEvent('Options Page', 'Added Snow Sports Cover');
      }
    });

    // Remove specified item - fire when removing an item is attempted 
    $('[data-analytics-id="options-table"]').on("mousedown", '[data-analytics-id="option-remove-spec-item"]', function () {
      var $tr = $(this).closest('[data-analytics-id="specified-item-row"]');
      var itemName = $tr.find('[data-analytics-id="item-name"] input').val();
      var itemValue = $tr.find('[data-analytics-id="item-value"] input').val();
      // Check that this is not a row with an empty name/value (due to validation error)
      if (itemName && itemValue) {
        wng.googleAnalytics.trackEvent('Options Page', 'Remove Specified Item',
          {
            'label': itemName + ' - $' + itemValue
          }
        );
      }
    });

    // Changed car excess cover value
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on('change', '[data-analytics-id="option-rental-excess"] select', function () {
      var newValue = $(this).find(':selected').text();
      wng.googleAnalytics.trackEvent('Options Page', 'Modified Car Rental Excess', {
        'label': newValue
      });
    });

    // Promo code removed - Fire when the remove promo code button is clicked
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on("mousedown", '[data-analytics-id="promo-code-remove"]', function () {
      var code = $('[data-analytics-id="promo-code-value"]').text().toUpperCase();
      wng.googleAnalytics.trackEvent('Options Page', 'Removed Promo Code', {
        'label': code
      });
    });

    // Footprints tracking

    // Clicked 'View Project' on Footprints on options page
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on('click', '[data-analytics-id="option-donation-project-view"]', function () {
      wng.googleAnalytics.trackEvent('Footprints', 'Clicked View Project', {
        'label': 'Footprints - Options Page'
      });
    });

    // Clicked to select another Footprints project
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on('mousedown', '[data-analytics-id="option-donation-project-change"]', function () {
      wng.googleAnalytics.trackEvent('Footprints', 'Clicked Select Another Project', {
        'label': 'Footprints - Options Page'
      });
    });

    // Chose to donate to footprints
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-table"]').on('change', '[data-analytics-id="option-donation"] select', function () {
      var newValue = $(this).find(':selected').text();
      if (newValue != "0.00") {
        wng.googleAnalytics.trackEvent('Footprints', 'Chose To Donate', {
          'label': 'Footprints - Options Page - Donating $' + newValue
        });
      }
    });

    // Clicked email your quote submit (mousedown to be sure to get in before submit)
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="options-side-bar-content"]').on('mousedown', '[data-analytics-id="email-quote"]', function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Emailed Quote');
    });

    // Scroll tracking

    $('[data-analytics-id="option-rental-excess"]').scrollTracking(function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Saw Rental Excess Option');
    });

    $('[data-analytics-id="option-donation"]').scrollTracking(function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Saw Footprints');
    });

    $('[data-analytics-id="option-total-bottom"]').scrollTracking(function () {
      wng.googleAnalytics.trackEvent('Options Page', 'Saw Bottom Quote Summary');
    });
  };

  widgetTracking.policyDetails = function (options) {
    var
      opt = $.extend({
        selectorForContentAjax: '#detailsForm',
        signInOrForgotPasswordContainerClass: 'sign-in-or-forgot-password',
        verifyExistingMemberContainerClass: 'join-or-sign-in-or-guest'
      }, options);

    $(opt.selectorForContentAjax).on('afterReplaceContent.contentAjax', function (e, data) {
      var
        i,
        $inlineLoginDiv,
        $inlineLoginGlobalErrors,
        $inlineLoginLocalErrors,
        $weThinkYouMightBeAMemberDiv,
        $weThinkYouMightBeAMemberGlobalErrors;

      for (i = 0; i < data.parts.length; i++) {

        // Track inline login failure
        if (data.parts[i].withinClass !== undefined &&
            data.parts[i].withinClass === opt.signInOrForgotPasswordContainerClass) {

          $inlineLoginDiv = $('[data-analytics-id="details-inline-signin"]');
          $inlineLoginGlobalErrors = $('#global-messages');
          $inlineLoginLocalErrors = $inlineLoginDiv.find('.input-validation-errors');
          
          if ($inlineLoginDiv.length > 0 && 
               ($inlineLoginGlobalErrors.length > 0) || ($inlineLoginLocalErrors.length > 0)) {
            wng.googleAnalytics.trackEvent('Your Details Page', 'Failed on Log In', {
              'label': 'From top login panel'
            });
          }
        }

        // Track 'We think you might already be a member
        if (data.parts[i].withinClass !== undefined &&
            data.parts[i].withinClass === opt.verifyExistingMemberContainerClass) {

          $weThinkYouMightBeAMemberDiv = $('[data-analytics-id="details-verify-existing-member"]');
          $weThinkYouMightBeAMemberGlobalErrors = $('#global-messages');

          if ($weThinkYouMightBeAMemberDiv.length > 0 && $weThinkYouMightBeAMemberGlobalErrors.length === 0) {
            wng.googleAnalytics.trackEvent('Your Details Page', 'Prompted to Sign In with corrected password (We think you might be a member panel)');
          }

          if ($weThinkYouMightBeAMemberDiv.length > 0 && $weThinkYouMightBeAMemberGlobalErrors.length > 0) {
            wng.googleAnalytics.trackEvent('Your Details Page', 'Failed on Log In', {
              'label': 'We think you might be a member panel'
            });
          }
        }
      }
    });

    // Use delegation from container selector since breadcrumb widget gets replaced by ajax
    $('[data-analytics-id="layout-content-container"]').on('mousedown', '[data-analytics-id="breadcrumb-goto-quote"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Breadcrumb', {
        'label': 'Go back to Quote Page'
      });
    });

    // Use delegation from container selector since breadcrumb widget gets replaced by ajax
    $('[data-analytics-id="layout-content-container"]').on('mousedown', '[data-analytics-id="breadcrumb-goto-options"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Breadcrumb', {
        'label': 'Go back to Options Page'
      });
    });

    // Log in link (bottom panel) is clicked
    $('[data-analytics-id="details-signin-bottom-section"]').on('mousedown', '[data-analytics-id="details-login-link"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked "Bought with us before? Log In" link', {
        label: "bottom"
      });
    });

    // Forgot password top button is clicked
    $('[data-analytics-id="details-signin-top-section"]').on('mousedown', '[data-analytics-id="details-forgot-password"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Forgot Password', { label: "top" });
    });

    // Forgot password bottom button is clicked
    $('[data-analytics-id="details-signin-bottom-section"]').on('mousedown', '[data-analytics-id="details-forgot-password"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Forgot Password', { label: "bottom" });
    });

    // Forgot password global pop-up button is clicked
    $('body').on('mousedown', '[data-analytics-id="popup-forgot-password"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Forgot Password', { label: "global pop-up" });
    });

    // Reset password top button is clicked
    $('[data-analytics-id="details-signin-top-section"]').on('mousedown', '[data-analytics-id="details-reset-password"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Reset Password', { label: "top" });
    });

    // Reset password global pop-up button is clicked
    $('body').on('mousedown', '[data-analytics-id="popup-reset-password"]', function () {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Clicked Reset Password', { label: "global pop-up" });
    });
  };

  widgetTracking.reviewAndPay = function (options) {
    var
      opt = $.extend({
      }, options);
    
    // Use delegation from container selector since breadcrumb widget gets replaced by ajax
    $('[data-analytics-id="layout-content-container"]').on('mousedown', '[data-analytics-id="breadcrumb-goto-quote"]', function () {
      wng.googleAnalytics.trackEvent('Payment Page', 'Clicked Breadcrumb', {
        'label': 'Go back to Quote Page'
      });
    });

    // Use delegation from container selector since breadcrumb widget gets replaced by ajax
    $('[data-analytics-id="layout-content-container"]').on('mousedown', '[data-analytics-id="breadcrumb-goto-options"]', function () {
      wng.googleAnalytics.trackEvent('Payment Page', 'Clicked Breadcrumb', {
        'label': 'Go back to Options Page'
      });
    });

    // Use delegation from container selector since breadcrumb widget gets replaced by ajax
    $('[data-analytics-id="layout-content-container"]').on('mousedown', '[data-analytics-id="breadcrumb-goto-details"]', function () {
      wng.googleAnalytics.trackEvent('Payment Page', 'Clicked Breadcrumb', {
        'label': 'Go back to Your Details Page'
      });
    });

    if (opt.shouldEmitTrackEventsForSaveAndContinueOptionsOnDetailsPage) {
      wng.googleAnalytics.trackEvent('Your Details Page', 'Purchase type selected (guest/new-member/existing-member)', {
        'label': opt.signedInStateLabel
      });
    }

    // Clicked edit trip button
    $('[data-analytics-id="open-edit-trip"]').on('click.quote', function () {
      wng.googleAnalytics.trackEvent('Payment Page', 'Clicked Edit Your Trip');
    });

    // Clicked edit details (mousedown to be sure to get in before submit)
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="payment-main-section"]').on('mousedown', '[data-analytics-id="edit-details"]', function () {
      wng.googleAnalytics.trackEvent('Payment Page', 'Clicked Edit Your Details');
    });

    // Clicked edit options (mousedown to be sure to get in before submit)
    // Use delegation from container selector since some widgets get replaced by ajax
    $('[data-analytics-id="payment-main-section"]').on('mousedown', '[data-analytics-id="edit-options"]', function () {
      wng.googleAnalytics.trackEvent('Payment Page', 'Clicked Edit Your Options');
    });

  };

  var setup = function (widgetId, options) {
    if (widgetTracking[widgetId]) {
      widgetTracking[widgetId](options);
    } else {
      throw new Error("Tracking is not defined for the identifier " + widgetId);
    }
  };

  return {
    setup: setup
  };
}());
;

/*
  Payform Javascript Library

  URL: https://github.com/jondavidjohn/payform
  Author: Jonathan D. Johnson <me@jondavidjohn.com>
  License: MIT
  Version: 1.4.0
 */

(function() {
  var indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };

  (function(name, definition) {
    if (typeof module !== "undefined" && module !== null) {
      return module.exports = definition();
    } else if (typeof define === 'function' && typeof define.amd === 'object') {
      return define(name, definition);
    } else {
      return this[name] = definition();
    }
  })('payform', function() {
    var _eventNormalize, _getCaretPos, _off, _on, attachEvents, cardFromNumber, cardFromType, defaultFormat, eventList, formatBackCardNumber, formatBackExpiry, formatCardExpiry, formatCardNumber, formatForwardExpiry, formatForwardSlashAndSpace, getDirectionality, hasTextSelected, keyCodes, luhnCheck, payform, reFormatCVC, reFormatCardNumber, reFormatExpiry, replaceFullWidthChars, restrictCVC, restrictCardNumber, restrictExpiry, restrictNumeric;
    _getCaretPos = function(ele) {
      var r, rc, re;
      if (ele.selectionStart != null) {
        return ele.selectionStart;
      } else if (document.selection != null) {
        ele.focus();
        r = document.selection.createRange();
        re = ele.createTextRange();
        rc = re.duplicate();
        re.moveToBookmark(r.getBookmark());
        rc.setEndPoint('EndToStart', re);
        return rc.text.length;
      }
    };
    _eventNormalize = function(listener) {
      return function(e) {
        var newEvt;
        if (e == null) {
          e = window.event;
        }
        if (e.inputType === 'insertCompositionText' && !e.isComposing) {
          return;
        }
        newEvt = {
          target: e.target || e.srcElement,
          which: e.which || e.keyCode,
          type: e.type,
          metaKey: e.metaKey,
          ctrlKey: e.ctrlKey,
          preventDefault: function() {
            if (e.preventDefault) {
              e.preventDefault();
            } else {
              e.returnValue = false;
            }
          }
        };
        return listener(newEvt);
      };
    };
    _on = function(ele, event, listener) {
      if (ele.addEventListener != null) {
        return ele.addEventListener(event, listener, false);
      } else {
        return ele.attachEvent("on" + event, listener);
      }
    };
    _off = function(ele, event, listener) {
      if (ele.removeEventListener != null) {
        return ele.removeEventListener(event, listener, false);
      } else {
        return ele.detachEvent("on" + event, listener);
      }
    };
    payform = {};
    keyCodes = {
      UNKNOWN: 0,
      BACKSPACE: 8,
      PAGE_UP: 33,
      ARROW_LEFT: 37,
      ARROW_RIGHT: 39
    };
    defaultFormat = /(\d{1,4})/g;
    payform.cards = [
      {
        type: 'elo',
        pattern: /^(4011(78|79)|43(1274|8935)|45(1416|7393|763(1|2))|50(4175|6699|67[0-7][0-9]|9000)|627780|63(6297|6368)|650(03([^4])|04([0-9])|05(0|1)|4(0[5-9]|3[0-9]|8[5-9]|9[0-9])|5([0-2][0-9]|3[0-8])|9([2-6][0-9]|7[0-8])|541|700|720|901)|651652|655000|655021)/,
        format: defaultFormat,
        length: [16],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'visaelectron',
        pattern: /^4(026|17500|405|508|844|91[37])/,
        format: defaultFormat,
        length: [16],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'maestro',
        pattern: /^(5018|5020|5038|6304|6390[0-9]{2}|67[0-9]{4})/,
        format: defaultFormat,
        length: [12, 13, 14, 15, 16, 17, 18, 19],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'forbrugsforeningen',
        pattern: /^600/,
        format: defaultFormat,
        length: [16],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'dankort',
        pattern: /^5019/,
        format: defaultFormat,
        length: [16],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'visa',
        pattern: /^4/,
        format: defaultFormat,
        length: [13, 16, 19],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'mastercard',
        pattern: /^(5[1-5][0-9]{4}|677189)|^(222[1-9]|2[3-6]\d{2}|27[0-1]\d|2720)([0-9]{2})/,
        format: defaultFormat,
        length: [16],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'amex',
        pattern: /^3[47]/,
        format: /(\d{1,4})(\d{1,6})?(\d{1,5})?/,
        length: [15],
        cvcLength: [4],
        luhn: true
      }, {
        type: 'hipercard',
        pattern: /^(384100|384140|384160|606282|637095|637568|60(?!11))/,
        format: defaultFormat,
        length: [14, 15, 16, 17, 18, 19],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'dinersclub',
        pattern: /^(36|38|30[0-5])/,
        format: /(\d{1,4})(\d{1,6})?(\d{1,4})?/,
        length: [14],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'discover',
        pattern: /^(6011|65|64[4-9]|622)/,
        format: defaultFormat,
        length: [16],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'unionpay',
        pattern: /^62/,
        format: defaultFormat,
        length: [16, 17, 18, 19],
        cvcLength: [3],
        luhn: false
      }, {
        type: 'jcb',
        pattern: /^35/,
        format: defaultFormat,
        length: [16, 17, 18, 19],
        cvcLength: [3],
        luhn: true
      }, {
        type: 'laser',
        pattern: /^(6706|6771|6709)/,
        format: defaultFormat,
        length: [16, 17, 18, 19],
        cvcLength: [3],
        luhn: true
      }
    ];
    cardFromNumber = function(num) {
      var card, i, len, ref;
      num = (num + '').replace(/\D/g, '');
      ref = payform.cards;
      for (i = 0, len = ref.length; i < len; i++) {
        card = ref[i];
        if (card.pattern.test(num)) {
          return card;
        }
      }
    };
    cardFromType = function(type) {
      var card, i, len, ref;
      ref = payform.cards;
      for (i = 0, len = ref.length; i < len; i++) {
        card = ref[i];
        if (card.type === type) {
          return card;
        }
      }
    };
    getDirectionality = function(target) {
      var style;
      style = getComputedStyle(target);
      return style && style['direction'] || document.dir;
    };
    luhnCheck = function(num) {
      var digit, digits, i, len, odd, sum;
      odd = true;
      sum = 0;
      digits = (num + '').split('').reverse();
      for (i = 0, len = digits.length; i < len; i++) {
        digit = digits[i];
        digit = parseInt(digit, 10);
        if ((odd = !odd)) {
          digit *= 2;
        }
        if (digit > 9) {
          digit -= 9;
        }
        sum += digit;
      }
      return sum % 10 === 0;
    };
    hasTextSelected = function(target) {
      var ref;
      if ((typeof document !== "undefined" && document !== null ? (ref = document.selection) != null ? ref.createRange : void 0 : void 0) != null) {
        if (document.selection.createRange().text) {
          return true;
        }
      }
      return (target.selectionStart != null) && target.selectionStart !== target.selectionEnd;
    };
    replaceFullWidthChars = function(str) {
      var char, chars, fullWidth, halfWidth, i, idx, len, value;
      if (str == null) {
        str = '';
      }
      fullWidth = '\uff10\uff11\uff12\uff13\uff14\uff15\uff16\uff17\uff18\uff19';
      halfWidth = '0123456789';
      value = '';
      chars = str.split('');
      for (i = 0, len = chars.length; i < len; i++) {
        char = chars[i];
        idx = fullWidth.indexOf(char);
        if (idx > -1) {
          char = halfWidth[idx];
        }
        value += char;
      }
      return value;
    };
    reFormatCardNumber = function(e) {
      var cursor;
      cursor = _getCaretPos(e.target);
      if (e.target.value === "") {
        return;
      }
      if (getDirectionality(e.target) === 'ltr') {
        cursor = _getCaretPos(e.target);
      }
      e.target.value = payform.formatCardNumber(e.target.value);
      if (getDirectionality(e.target) === 'ltr' && cursor !== e.target.selectionStart) {
        cursor = _getCaretPos(e.target);
      }
      if (getDirectionality(e.target) === 'rtl' && e.target.value.indexOf('‎\u200e') === -1) {
        e.target.value = '‎\u200e'.concat(e.target.value);
      }
      cursor = _getCaretPos(e.target);
      if ((cursor != null) && cursor !== 0 && e.type !== 'change') {
        return e.target.setSelectionRange(cursor, cursor);
      }
    };
    formatCardNumber = function(e) {
      var card, cursor, digit, length, re, upperLength, value;
      digit = String.fromCharCode(e.which);
      if (!/^\d+$/.test(digit)) {
        return;
      }
      value = e.target.value;
      card = cardFromNumber(value + digit);
      length = (value.replace(/\D/g, '') + digit).length;
      upperLength = 16;
      if (card) {
        upperLength = card.length[card.length.length - 1];
      }
      if (length >= upperLength) {
        return;
      }
      cursor = _getCaretPos(e.target);
      if (cursor && cursor !== value.length) {
        return;
      }
      if (card && card.type === 'amex') {
        re = /^(\d{4}|\d{4}\s\d{6})$/;
      } else {
        re = /(?:^|\s)(\d{4})$/;
      }
      if (re.test(value)) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = value + " " + digit;
        });
      } else if (re.test(value + digit)) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = (value + digit) + " ";
        });
      }
    };
    formatBackCardNumber = function(e) {
      var cursor, value;
      value = e.target.value;
      if (e.which !== keyCodes.BACKSPACE) {
        return;
      }
      cursor = _getCaretPos(e.target);
      if (cursor && cursor !== value.length) {
        return;
      }
      if ((e.target.selectionEnd - e.target.selectionStart) > 1) {
        return;
      }
      if (/\d\s$/.test(value)) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = value.replace(/\d\s$/, '');
        });
      } else if (/\s\d?$/.test(value)) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = value.replace(/\d$/, '');
        });
      }
    };
    reFormatExpiry = function(e) {
      var cursor;
      if (e.target.value === "") {
        return;
      }
      e.target.value = payform.formatCardExpiry(e.target.value);
      if (getDirectionality(e.target) === 'rtl' && e.target.value.indexOf('‎\u200e') === -1) {
        e.target.value = '‎\u200e'.concat(e.target.value);
      }
      cursor = _getCaretPos(e.target);
      if ((cursor != null) && e.type !== 'change') {
        return e.target.setSelectionRange(cursor, cursor);
      }
    };
    formatCardExpiry = function(e) {
      var digit, val;
      digit = String.fromCharCode(e.which);
      if (!/^\d+$/.test(digit)) {
        return;
      }
      val = e.target.value + digit;
      if (/^\d$/.test(val) && (val !== '0' && val !== '1')) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = "0" + val + " / ";
        });
      } else if (/^\d\d$/.test(val)) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = val + " / ";
        });
      }
    };
    formatForwardExpiry = function(e) {
      var digit, val;
      digit = String.fromCharCode(e.which);
      if (!/^\d+$/.test(digit)) {
        return;
      }
      val = e.target.value;
      if (/^\d\d$/.test(val)) {
        return e.target.value = val + " / ";
      }
    };
    formatForwardSlashAndSpace = function(e) {
      var val, which;
      which = String.fromCharCode(e.which);
      if (!(which === '/' || which === ' ')) {
        return;
      }
      val = e.target.value;
      if (/^\d$/.test(val) && val !== '0') {
        return e.target.value = "0" + val + " / ";
      }
    };
    formatBackExpiry = function(e) {
      var cursor, value;
      value = e.target.value;
      if (e.which !== keyCodes.BACKSPACE) {
        return;
      }
      cursor = _getCaretPos(e.target);
      if (cursor && cursor !== value.length) {
        return;
      }
      if (/\d\s\/\s$/.test(value)) {
        e.preventDefault();
        return setTimeout(function() {
          return e.target.value = value.replace(/\d\s\/\s$/, '');
        });
      }
    };
    reFormatCVC = function(e) {
      var cursor;
      if (e.target.value === "") {
        return;
      }
      cursor = _getCaretPos(e.target);
      e.target.value = replaceFullWidthChars(e.target.value).replace(/\D/g, '').slice(0, 4);
      if ((cursor != null) && e.type !== 'change') {
        return e.target.setSelectionRange(cursor, cursor);
      }
    };
    restrictNumeric = function(e) {
      var input;
      if (e.metaKey || e.ctrlKey) {
        return;
      }
      if ([keyCodes.UNKNOWN, keyCodes.ARROW_LEFT, keyCodes.ARROW_RIGHT].indexOf(e.which) > -1) {
        return;
      }
      if (e.which < keyCodes.PAGE_UP) {
        return;
      }
      input = String.fromCharCode(e.which);
      if (!/^\d+$/.test(input)) {
        return e.preventDefault();
      }
    };
    restrictCardNumber = function(e) {
      var card, digit, maxLength, value;
      digit = String.fromCharCode(e.which);
      if (!/^\d+$/.test(digit)) {
        return;
      }
      if (hasTextSelected(e.target)) {
        return;
      }
      value = (e.target.value + digit).replace(/\D/g, '');
      card = cardFromNumber(value);
      maxLength = card ? card.length[card.length.length - 1] : 16;
      if (value.length > maxLength) {
        return e.preventDefault();
      }
    };
    restrictExpiry = function(e) {
      var digit, value;
      digit = String.fromCharCode(e.which);
      if (!/^\d+$/.test(digit)) {
        return;
      }
      if (hasTextSelected(e.target)) {
        return;
      }
      value = e.target.value + digit;
      value = value.replace(/\D/g, '');
      if (value.length > 6) {
        return e.preventDefault();
      }
    };
    restrictCVC = function(e) {
      var digit, val;
      digit = String.fromCharCode(e.which);
      if (!/^\d+$/.test(digit)) {
        return;
      }
      if (hasTextSelected(e.target)) {
        return;
      }
      val = e.target.value + digit;
      if (val.length > 4) {
        return e.preventDefault();
      }
    };
    eventList = {
      cvcInput: [
        {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictNumeric)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictCVC)
        }, {
          eventName: 'paste',
          eventHandler: _eventNormalize(reFormatCVC)
        }, {
          eventName: 'change',
          eventHandler: _eventNormalize(reFormatCVC)
        }, {
          eventName: 'input',
          eventHandler: _eventNormalize(reFormatCVC)
        }
      ],
      expiryInput: [
        {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictNumeric)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictExpiry)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(formatCardExpiry)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(formatForwardSlashAndSpace)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(formatForwardExpiry)
        }, {
          eventName: 'keydown',
          eventHandler: _eventNormalize(formatBackExpiry)
        }, {
          eventName: 'change',
          eventHandler: _eventNormalize(reFormatExpiry)
        }, {
          eventName: 'input',
          eventHandler: _eventNormalize(reFormatExpiry)
        }
      ],
      cardNumberInput: [
        {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictNumeric)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictCardNumber)
        }, {
          eventName: 'keypress',
          eventHandler: _eventNormalize(formatCardNumber)
        }, {
          eventName: 'keydown',
          eventHandler: _eventNormalize(formatBackCardNumber)
        }, {
          eventName: 'paste',
          eventHandler: _eventNormalize(reFormatCardNumber)
        }, {
          eventName: 'change',
          eventHandler: _eventNormalize(reFormatCardNumber)
        }, {
          eventName: 'input',
          eventHandler: _eventNormalize(reFormatCardNumber)
        }
      ],
      numericInput: [
        {
          eventName: 'keypress',
          eventHandler: _eventNormalize(restrictNumeric)
        }, {
          eventName: 'paste',
          eventHandler: _eventNormalize(restrictNumeric)
        }, {
          eventName: 'change',
          eventHandler: _eventNormalize(restrictNumeric)
        }, {
          eventName: 'input',
          eventHandler: _eventNormalize(restrictNumeric)
        }
      ]
    };
    attachEvents = function(input, events, detach) {
      var evt, i, len;
      for (i = 0, len = events.length; i < len; i++) {
        evt = events[i];
        if (detach) {
          _off(input, evt.eventName, evt.eventHandler);
        } else {
          _on(input, evt.eventName, evt.eventHandler);
        }
      }
    };
    payform.cvcInput = function(input) {
      return attachEvents(input, eventList.cvcInput);
    };
    payform.expiryInput = function(input) {
      return attachEvents(input, eventList.expiryInput);
    };
    payform.cardNumberInput = function(input) {
      return attachEvents(input, eventList.cardNumberInput);
    };
    payform.numericInput = function(input) {
      return attachEvents(input, eventList.numericInput);
    };
    payform.detachCvcInput = function(input) {
      return attachEvents(input, eventList.cvcInput, true);
    };
    payform.detachExpiryInput = function(input) {
      return attachEvents(input, eventList.expiryInput, true);
    };
    payform.detachCardNumberInput = function(input) {
      return attachEvents(input, eventList.cardNumberInput, true);
    };
    payform.detachNumericInput = function(input) {
      return attachEvents(input, eventList.numericInput, true);
    };
    payform.parseCardExpiry = function(value) {
      var month, prefix, ref, year;
      value = value.replace(/\s/g, '');
      ref = value.split('/', 2), month = ref[0], year = ref[1];
      if ((year != null ? year.length : void 0) === 2 && /^\d+$/.test(year)) {
        prefix = (new Date).getFullYear();
        prefix = prefix.toString().slice(0, 2);
        year = prefix + year;
      }
      month = parseInt(month.replace(/[\u200e]/g, ""), 10);
      year = parseInt(year, 10);
      return {
        month: month,
        year: year
      };
    };
    payform.validateCardNumber = function(num) {
      var card, ref;
      num = (num + '').replace(/\s+|-/g, '');
      if (!/^\d+$/.test(num)) {
        return false;
      }
      card = cardFromNumber(num);
      if (!card) {
        return false;
      }
      return (ref = num.length, indexOf.call(card.length, ref) >= 0) && (card.luhn === false || luhnCheck(num));
    };
    payform.validateCardExpiry = function(month, year) {
      var currentTime, expiry, ref;
      if (typeof month === 'object' && 'month' in month) {
        ref = month, month = ref.month, year = ref.year;
      }
      if (!(month && year)) {
        return false;
      }
      month = String(month).trim();
      year = String(year).trim();
      if (!/^\d+$/.test(month)) {
        return false;
      }
      if (!/^\d+$/.test(year)) {
        return false;
      }
      if (!((1 <= month && month <= 12))) {
        return false;
      }
      if (year.length === 2) {
        if (year < 70) {
          year = "20" + year;
        } else {
          year = "19" + year;
        }
      }
      if (year.length !== 4) {
        return false;
      }
      expiry = new Date(year, month);
      currentTime = new Date;
      expiry.setMonth(expiry.getMonth() - 1);
      expiry.setMonth(expiry.getMonth() + 1, 1);
      return expiry > currentTime;
    };
    payform.validateCardCVC = function(cvc, type) {
      var card, ref;
      cvc = String(cvc).trim();
      if (!/^\d+$/.test(cvc)) {
        return false;
      }
      card = cardFromType(type);
      if (card != null) {
        return ref = cvc.length, indexOf.call(card.cvcLength, ref) >= 0;
      } else {
        return cvc.length >= 3 && cvc.length <= 4;
      }
    };
    payform.parseCardType = function(num) {
      var ref;
      if (!num) {
        return null;
      }
      return ((ref = cardFromNumber(num)) != null ? ref.type : void 0) || null;
    };
    payform.formatCardNumber = function(num) {
      var card, groups, ref, upperLength;
      num = replaceFullWidthChars(num);
      num = num.replace(/\D/g, '');
      card = cardFromNumber(num);
      if (!card) {
        return num;
      }
      upperLength = card.length[card.length.length - 1];
      num = num.slice(0, upperLength);
      if (card.format.global) {
        return (ref = num.match(card.format)) != null ? ref.join(' ') : void 0;
      } else {
        groups = card.format.exec(num);
        if (groups == null) {
          return;
        }
        groups.shift();
        groups = groups.filter(Boolean);
        return groups.join(' ');
      }
    };
    payform.formatCardExpiry = function(expiry) {
      var mon, parts, sep, year;
      expiry = replaceFullWidthChars(expiry);
      parts = expiry.match(/^\D*(\d{1,2})(\D+)?(\d{1,4})?/);
      if (!parts) {
        return '';
      }
      mon = parts[1] || '';
      sep = parts[2] || '';
      year = parts[3] || '';
      if (year.length > 0) {
        sep = ' / ';
      } else if (sep === ' /') {
        mon = mon.substring(0, 1);
        sep = '';
      } else if (mon.length === 2 || sep.length > 0) {
        sep = ' / ';
      } else if (mon.length === 1 && (mon !== '0' && mon !== '1')) {
        mon = "0" + mon;
        sep = ' / ';
      }
      return mon + sep + year;
    };
    return payform;
  });

}).call(this);
;
// Anonymouse function to set injectHtml function. All other methods are private and not accessable outside of the function
(function() {

	window.cardTokenPayment = {};

	// Add styles
    window.cardTokenPayment.InjectLibrary = function (targetElementId, customOptions) {

		if (!customOptions) throw "options is required.";
        if (!targetElementId) throw "targetElementId is required.";

        // Inject html into target html
        var targetElement = $('#' + targetElementId);
		if (!targetElement) throw "targetElement not found";

		var defaultOptions = getDefaultOptions();
		var options = $.extend({}, defaultOptions, customOptions);
		options.localization = $.extend({}, defaultOptions.localization, customOptions.localization);
		options.fileRefs = $.extend({}, defaultOptions.fileRefs, customOptions.fileRefs);

		// Add styles

		addStyleString(cardDetailsInternalCss, options.fileRefs);
		addStyleString(cardDetailsSpinnerCss, options.fileRefs);
		if (!options.skipDefaultCss) addStyleString(cardDetailsExternalCss, options.fileRefs);
		if (options.customCss) addStyleString(options.customCss, options.fileRefs);

		// Add html
		var htmlToApply = options.customHtml ? options.customHtml : cardDetailsHtml;
    // merge fileRefs and localization into one dictionary for replacement
		var htmlReplacementDict = $.extend({}, options.fileRefs, options.localization);
		addHtmlString(targetElement, htmlToApply, htmlReplacementDict);

		var cardOwnerEl = $('#cardtoken_NameOnCreditCard');
		var cardNumberEl = $('#cardtoken_CreditCardNumber');
		var cvvEl = $("#cardtoken_VerificationNumber");
		var mastercardEl = $("#cardtoken_ImgMasterCardType");
		var visaEl = $("#cardtoken_ImgVisaType");
		var amexEl = $("#cardtoken_ImgAmexType");
		var doPaymentEl = $('#cardtoken_DoPayment');
		var spinnerEl = $('#cardtoken_SpinnerContainer');

		if (!options.showBuyButton) {
			doPaymentEl.css({display: "none"});
		}

		// Populate tender types
		$.each(options.tenderTypes, function (idx, item) {
			if (item.Code === "visa") visaEl.show();
			else if (item.Code === "mc") mastercardEl.show();
			else if (item.Code === "amex") amexEl.show();
		});

		// Populate years dynamically + 5 from now
		var currentYear = (new Date()).getFullYear();
		var expMonthEl = $('#cardtoken_CreditCardExpiryDateMonthId');
		var expYearEl = $('#cardtoken_CreditCardExpiryDateYearId');
		expYearEl.empty();
		expYearEl.append("<option selected='selected'></option>");
		for (var i = 0; i < 5; i++) {
			var optionYear = currentYear + i;
			expYearEl.append($('<option>', {value: optionYear, text: optionYear}));
		}

		// Populate months
		expMonthEl.empty();
		expMonthEl.append("<option selected='selected'></option>");
		$.each(options.months, function (idx, item) {
			expMonthEl.append($('<option>', {value: item.Id, text: item.ShortDescription}));
		});

		$(".button-tertiary.close").click(function () {
			$(".modal-background").css({display: "none"});
		});
		$("#cardtoken_btnEnlargeCardCvv").click(function () {
			$(".modal-background").css({display: "block"});
		});

		if (!options.prefillFullName) $("#cardtoken_btnSetCardholderName").hide();

		$("#cardtoken_btnSetCardholderName").click(function () {
			prefillFullName(options);
		});

		doPaymentEl.click(function (e) {
			window.cardTokenPayment.DoTokenization();
			e.preventDefault();
		});

		var tokenizationInProgress = false;

		window.cardTokenPayment.DoTokenization = function()
		{
			var errorsList = [];

			var isCardNumberValid = payform.validateCardNumber(cardNumberEl.val());
			var isCvvValid = payform.validateCardCVC(cvvEl.val());
			var isNameValid = cardOwnerEl.val().length >= 5;

			//***************************************************
			// Check credit card number
			var ccNumberLabelEl = $(".cc-number");
			if (!isCardNumberValid) {
				errorsList.push("Incorrect card number");
			}
			setElementState(ccNumberLabelEl, isCardNumberValid);

			var ccNameLabelEl = $(".cc-name");
			if (!isNameValid) {
				errorsList.push("Name on card is required");
			}
			setElementState(ccNameLabelEl, isNameValid);

			var ccCvvLabelEl = $(".cc-verification");
			if (!isCvvValid) {
				errorsList.push("Incorrect card Security Code (CVC)");
			}
			setElementState(ccCvvLabelEl, isCvvValid);

			var expMonth = parseInt(expMonthEl.val());
			var expYear = parseInt(expYearEl.val());

			var ccExpDateLabelEl = $(".cc-expiry");
			var isExpDateValid = expMonth && expYear;
			if (isExpDateValid) {
				var cardDate = new Date();
				isExpDateValid = (expYear > cardDate.getFullYear() || (expYear == cardDate.getFullYear() && expMonth >= cardDate.getMonth()));
			}
			if (!isExpDateValid) {
				errorsList.push("Incorrect expiration date");
			}
			setElementState(ccExpDateLabelEl, isExpDateValid);

			var valErrorEl = $(".input-validation-errors");
			valErrorEl.empty();
			var isValid = errorsList.length == 0;
			if (isValid) {
				var cardDetails = {
					Source: options.source,
					TransactionId: options.transactionId,
					CardHolderName: $.trim(cardOwnerEl.val()),
					Pan: $.trim(cardNumberEl.val().replace(/ /g, "")),
					ExpiryMonth: parseInt(expMonthEl.val()),
					ExpiryYear: parseInt(expYearEl.val()),
					Cvv: $.trim(cvvEl.val())
				};

				// Perform tokenization service call
				processPayment(doPaymentEl, options, cardDetails);
			} else {
				displayErrors(errorsList);
			}
			return isValid;
		};

	    function displayError(errorMsg) {
			if (!errorMsg) errorMsg = 'Unknown error. Please check if internet is connected.'
		    var valErrorEl = $(".input-validation-errors");
			valErrorEl.append("<li><label>" + errorMsg + "</label></li>");
	    }

		function displayErrors(errorsList) {
			var valErrorEl = $(".input-validation-errors");
			errorsList.forEach(function (errMsg) {
				valErrorEl.append("<li><label>" + errMsg + "</label></li>");
			});
		}

		cardNumberEl.keyup(function () {
			setCardTransparent(mastercardEl, true);
			setCardTransparent(amexEl, true);
			setCardTransparent(visaEl, true);

			if (payform.parseCardType(cardNumberEl.val()) == 'visa') {
				setCardTransparent(visaEl, false);
			} else if (payform.parseCardType(cardNumberEl.val()) == 'amex') {
				setCardTransparent(amexEl, false);
			} else if (payform.parseCardType(cardNumberEl.val()) == 'mastercard') {
				setCardTransparent(mastercardEl, false);
			}
		});

		function setCardTransparent(el, flag) {
			if (!el) return;
			if (flag) {
				el.addClass('transparentCard');
			} else {
				el.removeClass('transparentCard');
			}
		}

		function setElementState(el, isSuccess) {
			if (!el) return;
			if (isSuccess) {
				el.removeClass('error');
			} else {
				el.addClass('error');
			}
		}

		// processPayment is a private method and is not available outside of the anonymous function
		function processPayment (confirmButton, options, data) {
		    if (tokenizationInProgress) return;
			$.support.cors = true;
		    tokenizationInProgress = true;
			spinnerEl.css({display: "block"});
			$.ajax({
				type: "post",
				dataType: "json",
				contentType: 'application/json',
				url: options.endPoint,
				data: JSON.stringify(data),
				success: function (data, text) {
					// if legit error, then trigger error handling logic
					if (data.errorMessage) {
						displayError(data.errorMessage);
						if (options.onError) options.onError(null, null, null, data);
						return;
					}
					if (options.onSuccess) options.onSuccess(data, text);
				},
				error: function (request, status, error) {
					displayError(data.errorMessage);
					if (options.onError) options.onError(request, status, error, null);
				},
				complete: function(response, result) {
					tokenizationInProgress = false;
					spinnerEl.css({display: "none"});
					if (options.onComplete) options.onComplete(response, result);
				}
			});
		}

		function addHtmlString(el, htmlToApply, fileRefs) {
			htmlToApply = stringInject(htmlToApply, fileRefs);
			el.html(htmlToApply);
		}

		function addStyleString(str, fileRefs) {
			var cssToApply = stringInject(str, fileRefs);
			var node = document.createElement('style');
			node.type = 'text/css';
			node.innerHTML = cssToApply;
			document.body.appendChild(node);
		}

		// injects properties into a string
		function stringInject(str, data) {
			if (typeof str === 'string' && (data instanceof Array)) {

				return str.replace(/({\d})/g, function(i) {
					return data[i.replace(/{/, '').replace(/}/, '')];
				});
			} else if (typeof str === 'string' && (data instanceof Object)) {

				if (Object.keys(data).length === 0) {
					return str;
				}

				for (let key in data) {
					if (!data.hasOwnProperty(key)) continue;
					var regExpStr = `{${key}}`;
					var regExp = new RegExp(regExpStr, 'g');

					// if field is presented
					if (data[key] != undefined)
						str = str.replace(regExp, data[key]);
				}
				return str;
			} else if (typeof str === 'string' && data instanceof Array === false || typeof str === 'string' && data instanceof Object === false) {

				return str;
			} else {

				return false;
			}
		}

	    //request full name
	    function prefillFullName(options) {
			$('#cardtoken_NameOnCreditCard').val(options.prefillFullName);
	    }

		//**********************************************
		// Default options methods
		//**********************************************
		function onSuccessDefault(responseObj, text) {
			console.log('onSuccess', responseObj.token, text);
		}

		function onErrorDefault(response, status, error, data) {
			console.log('onError', response, status, error, data);
		}

		function onCompleteDefault(response, result) {
			console.log('onComplete', response, result);
		}

		function getDefaultOptions() {
			var imgFolder = "/content/scripts/wng.cardToken.jsClient/images/";
			var fontFolder = "/content/scripts/wng.cardToken.jsClient/font/";
			var options = {
				endPoint: "https://cardtoken-api.wng-test.com/cardtoken/tokenise",
				onSuccess: onSuccessDefault,
				onError: onErrorDefault,
				onComplete: onCompleteDefault,
				source: "wng", // default is wng
				skipDefaultCss: false,
				customHtml: null,
				customCss: null,
				showCvcHelp: true,
				showBuyButton: true,
				prefillFullName: null,
				transactionId: new Date().getTime().toString(),
				fileRefs: {
					// images
					"img_tendertype_visa": imgFolder + "tendertype_visa.png",
					"img_tendertype_mc": imgFolder + "tendertype_mc.png",
					"img_tendertype_ame": imgFolder + "tendertype_amex.png",
					"img_cc_amex_large": imgFolder + "cc_amex_large.png",
					"img_cc_other_large": imgFolder + "cc_other_large.png",
					"img_cc_other_small": imgFolder + "cc_other_small.png",
					"img_cc_amex_small": imgFolder + "cc_amex_small.png",
					"img_enlarge": imgFolder + "enlarge.png",
					// fonts
					"font_flama_light_woff": fontFolder + "Flama-Light.woff",
					"font_flama_medium_woff": fontFolder + "Flama-Medium.woff",
					"font_wng_awesome": fontFolder + "wng-awesome",
					// placeholders
					"css_display_cvchint": "block"
				},
				localization: {
					// main payment form (ViewText)
					"PaymentHeaderText": "Secure Payment",
					"PaymentEncryptionText": "This is a 128-Bit SSL encrypted payment",
					"CreditCardAcceptText": "We Accept",
					"CreditCardNameLabel": "Name on card:",
					"CreditCardNameHelpText": "The name on the front of your card",
					"InsertFirstTravellerNameLinkText": "Click to insert name of first traveller",
					"CreditCardNumberLabel": "Credit Card Number:",
					"CreditCardNumberHelpText": "The digits on the front of your credit card",
					"CreditCardExpirationLabel": "Expiration date:",
					"CreditCardExpirationHelpText": "The date your credit card expires. Find this on the front of your credit card",
					"CreditCardSecurityCodeLabel": "Security Code: (CVC/CCV)",
					"CreditCardSecurityCodeHelpText": "Last 3 digits on the back of your Visa or Mastercard or 4 digits on the front of your AMEX card.",
					"CreditCardSecurityCodeHelpTextExcludingAmex": "Last 3 digits on the back of your Visa or Mastercard.",
					"EnlargeButtonText": "enlarge",
					// modal hint window (ModalText)
					"PaymentHintText": "Finding your 3 Digit Security Code",
					"PaymentHintSecurityCodeTypesText": '(or "CVC" or "CVV")',
					"CreditCardLabelForVisaMaster": "Visa or Mastercard",
					"CreditCardVerificationNumberLabel": "For your safety and security, we require that you enter your card's verification number",
					"CreditCardVerificationNumberTextForVisaMaster": "The verification number for visa and mastercard is a 3-digit number printed on the back of your card. It appears  after and to the right of your card number.",
					"CreditCardLabelForAmericanExpress": "American Express",
					"CreditCardVerificationNumberTextForAmericanExpress": "The American Express security code is a 4-digit number  printed on the front of your card. It appears after and to the right of your card number.",
					// buttons
					"BuyNowButtonText": "Buy Now"
				},
				months: [
					{ Id: 1, ShortDescription: "01 - Jan" },
					{ Id: 2, ShortDescription: "02 - Feb" },
					{ Id: 3, ShortDescription: "03 - Mar" },
					{ Id: 4, ShortDescription: "04 - Apr" },
					{ Id: 5, ShortDescription: "05 - May" },
					{ Id: 6, ShortDescription: "06 - Jun" },
					{ Id: 7, ShortDescription: "07 - Jul" },
					{ Id: 8, ShortDescription: "08 - Aug" },
					{ Id: 9, ShortDescription: "09 - Sep" },
					{ Id: 10, ShortDescription: "10 - Oct" },
					{ Id: 11, ShortDescription: "11 - Nov" },
					{ Id: 12, ShortDescription: "12 - Dec" }
				],
				tenderTypes: [
					{ Code: "visa", ShortDescription: "Visa" },
					{ Code: "mc", ShortDescription: "MasterCard" },
					{ Code: "amex", ShortDescription: "American Express" },
				]
			};
			return options;
		}
    };

	// language=CSS
	var cardDetailsInternalCss = `
/* The Modal (background) */
.modal-background {
	display: none; /* Hidden by default */
	position: fixed; /* Stay in place */
	z-index: 1; /* Sit on top */
	left: 0;
	top: 0;
	width: 100%; /* Full width */
	height: 100%; /* Full height */
	overflow: auto; /* Enable scroll if needed */
	background-color: rgb(0,0,0); /* Fallback color */
	background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
}

.transparentCard {
	opacity: 0.2;
}

`;
	// language=CSS
    var cardDetailsSpinnerCss = `
	#cardtoken_SpinnerContainer{
		position: absolute;
		top:0px;
		left:0px;
		width: 100%;
		height: 100%;
		background: black;
		opacity: .5;
	}

	#cardtoken_Spinner {
		position: absolute;
		left: 50%;
		top: 50%;
		height:60px;
		width:60px;
		margin:0px auto;
		-webkit-animation: rotation .6s infinite linear;
		-moz-animation: rotation .6s infinite linear;
		-o-animation: rotation .6s infinite linear;
		animation: rotation .6s infinite linear;
		border-left:6px solid rgba(0,174,239,.15);
		border-right:6px solid rgba(0,174,239,.15);
		border-bottom:6px solid rgba(0,174,239,.15);
		border-top:6px solid rgba(0,174,239,.8);
		border-radius:100%;
	}

	@-webkit-keyframes rotation {
		from {-webkit-transform: rotate(0deg);}
		to {-webkit-transform: rotate(359deg);}
	}
	@-moz-keyframes rotation {
		from {-moz-transform: rotate(0deg);}
		to {-moz-transform: rotate(359deg);}
	}
	@-o-keyframes rotation {
		from {-o-transform: rotate(0deg);}
		to {-o-transform: rotate(359deg);}
	}
	@keyframes rotation {
		from {transform: rotate(0deg);}
		to {transform: rotate(359deg);}
}`;

	// language=CSS
	var cardDetailsExternalCss = `
/**************************************************************************************/
/* Common Content
/**************************************************************************************/

/*! CSS Used fontfaces */
@font-face {
	font-family: "Flama";
	font-style: normal;
	font-weight: 500;
	src: local("Flama Light"), local("Flama-Light"), url({font_flama_light_woff}) format("woff");
}

@font-face {
	font-family: "Flama";
	font-style: normal;
	font-weight: 600;
	src: local("Flama Medium"), local("Flama-Medium"), url({font_flama_medium_woff}) format("woff");
}

@font-face {
	font-family: "FontAwesome";
	src: url('{font_wng_awesome}.eot?1497571805');
	src: url('{font_wng_awesome}.eot?&1497571805#iefix') format('embedded-opentype'),
		 url('{font_wng_awesome}.woff?1497571805') format('woff'),
	     url('{font_wng_awesome}.ttf?1497571805') format('truetype'),
	     url('{font_wng_awesome}.svg?1497571805') format('svg');
}

.button-tertiary {
	-moz-border-radius: 4px;
	-webkit-border-radius: 4px;
	border-radius: 4px;
	font-size: 16px;
	background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2Y0ZjNmMyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2RjZDhkOCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
	background-size: 100%;
	background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #f4f3f3), color-stop(100%, #dcd8d8));
	background-image: -moz-linear-gradient(#f4f3f3, #dcd8d8);
	background-image: -webkit-linear-gradient(#f4f3f3, #dcd8d8);
	background-image: linear-gradient(#f4f3f3, #dcd8d8);
	color: #3a3a3a;
	border: 1px solid #e3e0e0;
	padding: 0px 10px;
}

.button-tertiary:hover {
	border: 1px solid #d2cece;
	background: #e3e0e0;
}

h4 {
	margin-right: 40px;
}

div, span, h3, img, fieldset, label {
	margin: 0;
	padding: 0;
	border: 0;
	font: inherit;
	font-size: 100%;
	vertical-align: baseline;
}

.error {
	font-weight: bold;
	color: #b14f64;
}

input, select {
	font-family: Flama, sans-serif;
	margin-left: 0px;
}

div, span, h2, h4, p {
	margin: 0;
	padding: 0;
	border: 0;
	font: inherit;
	font-size: 100%;
	vertical-align: baseline;
}

.link-like {
	background: none !important;
	border: none;
	padding: 0 !important;
	width: auto;
	font-family: Flama, sans-serif;
	font-size: 16px;
	text-transform: none !important;
	color: #009ed8 !important;
}

.link-like:hover {
	background: none;
	color: #009ed8 !important;
	text-decoration: underline;
	cursor: pointer;
}

.link-like:active {
	color: #00a6a1 !important;
}

button {
	-moz-border-radius: 2px;
	-webkit-border-radius: 2px;
	border-radius: 2px;
	border: none;
	outline: none;
	line-height: 24px;
	padding: 0px 8px;
	font-size: 16px;
	font-family: Flama, sans-serif;
	display: inline-block;
}

button span {
	font-weight: normal;
	font-family: FontAwesome;
	font-size: 13px;
	padding-left: 8px;
	display: inline-block;
}

button:hover {
	cursor: pointer;
}

.buy-button-primary {
	-moz-border-radius: 4px;
	-webkit-border-radius: 4px;
	border-radius: 4px;
	text-shadow: rgba(0, 0, 0, 0.1) 0px 1px 0;
	background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwYTZhMSIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iIzAwNzM3MCIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
	background-size: 100%;
	background-image: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #00a6a1), color-stop(100%, #007370));
	background-image: -moz-linear-gradient(#00a6a1, #007370);
	background-image: -webkit-linear-gradient(#00a6a1, #007370);
	background-image: linear-gradient(#00a6a1, #007370);
	color: #fff;
	border: 1px solid #007370;
	padding: 2px 10px;
	font-size: 18px;
	font-weight: normal;
}

.buy-button-primary:hover {
	background: #005f5c;
}

input[type="text"] {
	-moz-box-sizing: border-box;
	-webkit-box-sizing: border-box;
	box-sizing: border-box;
	-moz-box-shadow: rgba(153, 153, 153, 0.2) 0px 0px 5px;
	-webkit-box-shadow: rgba(153, 153, 153, 0.2) 0px 0px 5px;
	box-shadow: rgba(153, 153, 153, 0.2) 0px 0px 5px;
	-moz-box-shadow: rgba(153, 153, 153, 0.2) 1px 2px 8px inset;
	-webkit-box-shadow: rgba(153, 153, 153, 0.2) 1px 2px 8px inset;
	box-shadow: rgba(153, 153, 153, 0.2) 1px 2px 8px inset;
	-moz-border-radius: 3px;
	-webkit-border-radius: 3px;
	border-radius: 3px;
	float: left;
	clear: both;
	width: 100%;
	border: 1px solid #bcbcbc;
	outline: none;
	padding: 5px 8px;
	color: #3a3a3a;
	position: relative;
	font-size: 18px;
}

input[type="text"]:focus {
	border-color: #00a6a1;
	color: #3a3a3a;
}

::-webkit-input-placeholder {
	color: #ababab !important;
}

::-moz-placeholder {
	color: #ababab !important;
}

:-moz-placeholder {
	color: #ababab !important;
}

:-ms-input-placeholder {
	color: #ababab !important;
}

select {
	-moz-border-radius: 3px;
	-webkit-border-radius: 3px;
	border-radius: 3px;
	border: 1px solid #bcbcbc;
	color: #555555;
	cursor: pointer;
	background-color: white;
	-moz-box-sizing: border-box;
	-webkit-box-sizing: border-box;
	box-sizing: border-box;
	-moz-box-shadow: rgba(153, 153, 153, 0.2) 0px 0px 5px;
	-webkit-box-shadow: rgba(153, 153, 153, 0.2) 0px 0px 5px;
	box-shadow: rgba(153, 153, 153, 0.2) 0px 0px 5px;
	-moz-box-shadow: rgba(153, 153, 153, 0.2) 1px 2px 8px inset;
	-webkit-box-shadow: rgba(153, 153, 153, 0.2) 1px 2px 8px inset;
	box-shadow: rgba(153, 153, 153, 0.2) 1px 2px 8px inset;
	padding: 5px;
	font-size: 16px;
	background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='32' height='24' viewBox='0 0 32 24'><polygon points='0,0 32,0 16,24' style='fill: rgb%28138, 138, 138%29'></polygon></svg>");
	padding-right: 20px;
	background-size: 16px 8px;
	background-repeat: no-repeat;
	background-position: right -1rem center;
	background-origin: content-box;
	-webkit-appearance: none;
	width: 100%;
}

select:focus {
	border-color: #00a6a1;
	color: #3a3a3a;
	outline: none;
}

.input-group {
	margin-bottom: 16px;
	float: left;
	width: 30%;
	margin-right: 3%;
}

.input-group label {
	margin-bottom: 4px;
	display: block;
}

.actions {
	*zoom: 1;
	float: left;
	width: 100%;
	text-align: right;
}

.actions:after {
	content: "";
	display: table;
	clear: both;
}

.actions .input-group {
	float: right;
	width: auto;
	margin-right: 0;
	margin-left: 0;
}

.actions.left .input-group {
	float: left;
	margin-left: 0;
	margin-right: 16px;
}

.tab-content {
	*zoom: 1;
	padding: 16px 16px 0;
	border: 1px solid #dadada;
	background: #f4f3f3;
	border-top: none;
}

.tab-content:after {
	content: "";
	display: table;
	clear: both;
}

/* Validation Error*/
ul, li, label {
	margin: 0;
	padding: 0;
	border: 0;
	font: inherit;
	font-size: 100%;
	vertical-align: baseline;
}

ul {
	list-style: none;
}

.content-area ul {
	margin-bottom: 16px;
	*zoom: 1;
}

.content-area ul:after {
	content: "";
	display: table;
	clear: both;
}

.input-validation-errors {
	margin-bottom: 0.57143em;
	float: left;
	clear: both;
	width: 100%;
	font-weight: bold;
	color: #b14f64;
}

#policy-details fieldset label {
	display: block;
	float: none;
}
/********************************************/
/*			Payment Panel					*/
/********************************************/
.payment-panel .tab-content {
	background: #f4f3f3;
}

#payment-details {
	position: relative;
}

#payment-details.payment-details {
	padding-bottom: 10px;
}

#payment-details label {
	font-size: 18px;
	margin-bottom: 0;
}

#payment-details .help {
	font-size: 11px;
	margin-bottom: 8px;
	display: block;
}

#payment-details .card-types {
	*zoom: 1;
	margin-bottom: 16px;
}

#payment-details .card-types:after {
	content: "";
	display: table;
	clear: both;
}

#payment-details .card-types label {
	margin-bottom: 8px;
}

#payment-details .card-types span {
	float: left;
	margin-right: 8px;
}

#payment-details .info {
	border: none;
	padding: 0;
}

#payment-details .info .encryption {
	color: #333;
	font-size: 13px;
	display: inline;
}

#payment-details .cc-name .name, #payment-details .cc-number input {
	width: 310px;
}

#payment-details .cc-expiry select {
	margin-right: 8px;
	margin-bottom: 0;
}

#payment-details .cc-expiry select:last-child {
	margin-left: 0;
	margin-right: 0;
}

#payment-details .cc-expiry table {
	background-color: #f4f3f3; 
	border: none;
}

#payment-details .cc-expiry td {
	padding: 0;
	background-color: #f4f3f3; 
}

#payment-details .cc-verification {
	*zoom: 1;
	position: relative;
}

#payment-details .cc-verification:after {
	content: "";
	display: table;
	clear: both;
}

#payment-details .cc-verification input {
	float: left;
	width: 77px;
}

#payment-details .cc-verification label {
	display: inline;
}

#payment-details .cc-verification .alternative {
	font-size: 11px;
}

#payment-details .cc-verification .help {
	width: 45%;
}

#payment-details .cc-verification .hint {
	position: absolute;
	right: 0;
	display: {css_display_cvchint};
	top: 0;
	width: 40%;
	height: 56px;
}

#payment-details .cc-verification .hint .other {
	float: left;
	margin-right: 8px;
	background: transparent url("{img_cc_other_small}") no-repeat left top;
	width: 85px;
	height: 56px;
}

#payment-details .cc-verification .hint .amex {
	float: left;
	margin-right: 8px;
	background: transparent url("{img_cc_amex_small}") no-repeat left top;
	width: 100px;
	height: 56px;
}

#payment-details .cc-verification .hint .link-like {
	float: left;
	display: block;
	width: 40%;
	background: transparent url("{img_enlarge}") no-repeat left top !important;
	font-size: 13px;
	font-weight: bold;
}

#payment-details .set-name {
	display: inline;
	margin-left: 8px;
	font-weight: bold;
	font-size: 13px;
}

#payment-details label {
	display: block;
	float: none;
}

#payment-details .input-group {
	*zoom: 1;
	float: none;
	width: auto;
	display: block;
}

#payment-details .input-group:after {
	content: "";
	display: table;
	clear: both;
}

input[type="text"], select {
	border: 1px solid #dadada;
}

input[type="text"]:focus {
	border-color: #00a6a1;
}

/**************************************************************************************/
/* Modal Content
/**************************************************************************************/
.content-area h2 {
	font-size: 26px;
	line-height: 32px;
	margin-bottom: 16px;
}

.modal-content {
	-moz-box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;
	-webkit-box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;
	box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 1px 1px;
	-moz-border-radius: 3px;
	-webkit-border-radius: 3px;
	border-radius: 3px;
	border: 1px solid #bcbcbc;
	position: fixed;
	top: 100px;
	left: 45%;
	overflow: auto;
	padding: 16px 16px 0;
	background: #fff;
	width: 800px;
	margin-left: -400px;
	z-index: 910;
	padding-bottom: 16px;
}

.modal-content h2 {
	font-size: 26px !important;
	line-height: 32px !important;
	margin-bottom: 16px !important;
}

.modal-content h2 {
	font-size: 16px;
	line-height: 5px;
	margin-bottom: 0px;
}

.modal-content .close {
	position: absolute;
	top: 16px;
	right: 16px;
}

.modal-content .close span {
	padding-left: 0;
}

.modal-content.payment-hint {
	width: 550px;
	margin-left: -250px;
}

.modal-content.payment-hint h2 span {
	display: block;
	font-size: 18px;
	line-height: 18px;
}

.modal-content.payment-hint .card-wrapper {
	margin: 0 auto;
	*zoom: 1;
}

.modal-content.payment-hint .card-wrapper:after {
	content: "";
	display: table;
	clear: both;
}

.modal-content.payment-hint .card-wrapper.two {
	width: 500px;
}

.modal-content.payment-hint .card-wrapper .other {
	float: left;
	width: 235px;
	padding-top: 170px;
	background: transparent url("{img_cc_other_large}") no-repeat top left;
}

.modal-content.payment-hint .card-wrapper .amex {
	float: right;
	width: 235px;
	padding-top: 170px;
	background: transparent url("{img_cc_amex_large}") no-repeat top left;
}

/*************************************************************************/        
`;

	// language=HTML
	var cardDetailsHtml = `
<div class="tab-content">
	<div class="error">
		<ul class="input-validation-errors">
			<!-- validation errors go here -->
		</ul>
	</div>
	<div class="credit-card-editor-wrapper payment-details" id="payment-details">

		<h3 class="info">{PaymentHeaderText}<span class="encryption">{PaymentEncryptionText}</span></h3>
		<fieldset>
			<div class="card-types">
				<label>{CreditCardAcceptText}</label>
				<span><img src="{img_tendertype_visa}" alt="Visa" id="cardtoken_ImgVisaType" style="display: none;"></span>
				<span><img src="{img_tendertype_mc}" alt="MasterCard" id="cardtoken_ImgMasterCardType" style="display: none;"></span>
				<span><img src="{img_tendertype_ame}" alt="American Express" id="cardtoken_ImgAmexType" style="display: none;"></span>
			</div>

			<div class="input-group cc-name">
				<label>{CreditCardNameLabel}</label> <span class="help">{CreditCardNameHelpText}</span>
				<input class="name" id="cardtoken_NameOnCreditCard" tabindex="4" type="text" value="">
				<input class="link-like set-name" id="cardtoken_btnSetCardholderName" type="button" tabindex="11"
					   value="< {InsertFirstTravellerNameLinkText}">
			</div>
			<div class="input-group cc-number">
				<label>{CreditCardNumberLabel}</label> <span
				class="help">{CreditCardNumberHelpText}</span>
				<input autocomplete="off" id="cardtoken_CreditCardNumber" tabindex="5" type="text" value="">
			</div>

			<div class="input-group cc-expiry">
				<label>{CreditCardExpirationLabel}</label>
				<span class="help">{CreditCardExpirationHelpText}</span>
				<table>
					<tr><td>
							<select class="month" id="cardtoken_CreditCardExpiryDateMonthId" tabindex="6">
								<option selected="selected"></option>
								<option value="1">01 - Jan</option>
							</select>
						</td>
						<td style="vertical-align: center; text-align: center; width: 20px;">/</td>
						<td>
							<select class="year" id="cardtoken_CreditCardExpiryDateYearId" tabindex="7">
								<option selected="selected"></option>
								<option value="2019">2019</option>
							</select>
						</td>
					</tr>
				</table>
			</div>
			<div class="input-group cc-verification">
				<label>{CreditCardSecurityCodeLabel}</label>
				<span class="help">{CreditCardSecurityCodeHelpText}</span>
				<div class="hint">
					<div class="other"></div>
					<div class="amex"></div>
					<input class="link-like" type="button" id="cardtoken_btnEnlargeCardCvv" value="enlarge">
				</div>
				<input autocomplete="off" id="cardtoken_VerificationNumber" tabindex="8" type="text" value="">
			</div>

		</fieldset>
	</div>

	<div class="actions left">
		<div class="input-group">
			<button class="buy-now buy-button-primary" type="button" tabindex="8" autocomplete="off" id="cardtoken_DoPayment">
				{BuyNowButtonText}<span>&#57364;</span>
			</button>
		</div>
	</div>
	
	<div id="cardtoken_SpinnerContainer" style="display: none;">
		<div id="cardtoken_Spinner"></div>
	</div>
</div>

<div class="modal-background">
	<div class="modal-content payment-hint">

		<h2>{PaymentHintText}<span>{PaymentHintSecurityCodeTypesText}</span></h2>
		<p>{CreditCardVerificationNumberLabel}</p>

		<div class="card-wrapper two">
			<div class="other">
				<h4>{CreditCardLabelForVisaMaster}</h4>
				<p>{CreditCardVerificationNumberTextForVisaMaster}</p>
			</div>
			<div class="amex">
				<h4>{CreditCardLabelForAmericanExpress}</h4>
				<p>{CreditCardVerificationNumberTextForAmericanExpress}</p>
			</div>
		</div>
		<button type="button" class="button-tertiary close">
			<span>&#57360;</span>
		</button>
	</div>
</div>
`;
})();
;
