1(function (global, factory) { 2 typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 typeof define === 'function' && define.amd ? define('GOVUKFrontend', ['exports'], factory) : 4 (factory((global.GOVUKFrontend = {}))); 5}(this, (function (exports) { 'use strict'; 6 7/** 8 * TODO: Ideally this would be a NodeList.prototype.forEach polyfill 9 * This seems to fail in IE8, requires more investigation. 10 * See: https://github.com/imagitama/nodelist-foreach-polyfill 11 */ 12function nodeListForEach (nodes, callback) { 13 if (window.NodeList.prototype.forEach) { 14 return nodes.forEach(callback) 15 } 16 for (var i = 0; i < nodes.length; i++) { 17 callback.call(window, nodes[i], i, nodes); 18 } 19} 20 21// Used to generate a unique string, allows multiple instances of the component without 22// Them conflicting with each other. 23// https://stackoverflow.com/a/8809472 24function generateUniqueID () { 25 var d = new Date().getTime(); 26 if (typeof window.performance !== 'undefined' && typeof window.performance.now === 'function') { 27 d += window.performance.now(); // use high-precision timer if available 28 } 29 return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 30 var r = (d + Math.random() * 16) % 16 | 0; 31 d = Math.floor(d / 16); 32 return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16) 33 }) 34} 35 36(function(undefined) { 37 38// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Object/defineProperty/detect.js 39var detect = ( 40 // In IE8, defineProperty could only act on DOM elements, so full support 41 // for the feature requires the ability to set a property on an arbitrary object 42 'defineProperty' in Object && (function() { 43 try { 44 var a = {}; 45 Object.defineProperty(a, 'test', {value:42}); 46 return true; 47 } catch(e) { 48 return false 49 } 50 }()) 51); 52 53if (detect) return 54 55// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Object.defineProperty&flags=always 56(function (nativeDefineProperty) { 57 58 var supportsAccessors = Object.prototype.hasOwnProperty('__defineGetter__'); 59 var ERR_ACCESSORS_NOT_SUPPORTED = 'Getters & setters cannot be defined on this javascript engine'; 60 var ERR_VALUE_ACCESSORS = 'A property cannot both have accessors and be writable or have a value'; 61 62 Object.defineProperty = function defineProperty(object, property, descriptor) { 63 64 // Where native support exists, assume it 65 if (nativeDefineProperty && (object === window || object === document || object === Element.prototype || object instanceof Element)) { 66 return nativeDefineProperty(object, property, descriptor); 67 } 68 69 if (object === null || !(object instanceof Object || typeof object === 'object')) { 70 throw new TypeError('Object.defineProperty called on non-object'); 71 } 72 73 if (!(descriptor instanceof Object)) { 74 throw new TypeError('Property description must be an object'); 75 } 76 77 var propertyString = String(property); 78 var hasValueOrWritable = 'value' in descriptor || 'writable' in descriptor; 79 var getterType = 'get' in descriptor && typeof descriptor.get; 80 var setterType = 'set' in descriptor && typeof descriptor.set; 81 82 // handle descriptor.get 83 if (getterType) { 84 if (getterType !== 'function') { 85 throw new TypeError('Getter must be a function'); 86 } 87 if (!supportsAccessors) { 88 throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); 89 } 90 if (hasValueOrWritable) { 91 throw new TypeError(ERR_VALUE_ACCESSORS); 92 } 93 Object.__defineGetter__.call(object, propertyString, descriptor.get); 94 } else { 95 object[propertyString] = descriptor.value; 96 } 97 98 // handle descriptor.set 99 if (setterType) { 100 if (setterType !== 'function') { 101 throw new TypeError('Setter must be a function'); 102 } 103 if (!supportsAccessors) { 104 throw new TypeError(ERR_ACCESSORS_NOT_SUPPORTED); 105 } 106 if (hasValueOrWritable) { 107 throw new TypeError(ERR_VALUE_ACCESSORS); 108 } 109 Object.__defineSetter__.call(object, propertyString, descriptor.set); 110 } 111 112 // OK to define value unconditionally - if a getter has been specified as well, an error would be thrown above 113 if ('value' in descriptor) { 114 object[propertyString] = descriptor.value; 115 } 116 117 return object; 118 }; 119}(Object.defineProperty)); 120}) 121.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 122 123(function(undefined) { 124 // Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Function/prototype/bind/detect.js 125 var detect = 'bind' in Function.prototype; 126 127 if (detect) return 128 129 // Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Function.prototype.bind&flags=always 130 Object.defineProperty(Function.prototype, 'bind', { 131 value: function bind(that) { // .length is 1 132 // add necessary es5-shim utilities 133 var $Array = Array; 134 var $Object = Object; 135 var ObjectPrototype = $Object.prototype; 136 var ArrayPrototype = $Array.prototype; 137 var Empty = function Empty() {}; 138 var to_string = ObjectPrototype.toString; 139 var hasToStringTag = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol'; 140 var isCallable; /* inlined from https://npmjs.com/is-callable */ var fnToStr = Function.prototype.toString, tryFunctionObject = function tryFunctionObject(value) { try { fnToStr.call(value); return true; } catch (e) { return false; } }, fnClass = '[object Function]', genClass = '[object GeneratorFunction]'; isCallable = function isCallable(value) { if (typeof value !== 'function') { return false; } if (hasToStringTag) { return tryFunctionObject(value); } var strClass = to_string.call(value); return strClass === fnClass || strClass === genClass; }; 141 var array_slice = ArrayPrototype.slice; 142 var array_concat = ArrayPrototype.concat; 143 var array_push = ArrayPrototype.push; 144 var max = Math.max; 145 // /add necessary es5-shim utilities 146 147 // 1. Let Target be the this value. 148 var target = this; 149 // 2. If IsCallable(Target) is false, throw a TypeError exception. 150 if (!isCallable(target)) { 151 throw new TypeError('Function.prototype.bind called on incompatible ' + target); 152 } 153 // 3. Let A be a new (possibly empty) internal list of all of the 154 // argument values provided after thisArg (arg1, arg2 etc), in order. 155 // XXX slicedArgs will stand in for "A" if used 156 var args = array_slice.call(arguments, 1); // for normal call 157 // 4. Let F be a new native ECMAScript object. 158 // 11. Set the [[Prototype]] internal property of F to the standard 159 // built-in Function prototype object as specified in 15.3.3.1. 160 // 12. Set the [[Call]] internal property of F as described
160in 161 // 15.3.4.5.1. 162 // 13. Set the [[Construct]] internal property of F as described in 163 // 15.3.4.5.2. 164 // 14. Set the [[HasInstance]] internal property of F as described in 165 // 15.3.4.5.3. 166 var bound; 167 var binder = function () { 168 169 if (this instanceof bound) { 170 // 15.3.4.5.2 [[Construct]] 171 // When the [[Construct]] internal method of a function object, 172 // F that was created using the bind function is called with a 173 // list of arguments ExtraArgs, the following steps are taken: 174 // 1. Let target be the value of F's [[TargetFunction]] 175 // internal property. 176 // 2. If target has no [[Construct]] internal method, a 177 // TypeError exception is thrown. 178 // 3. Let boundArgs be the value of F's [[BoundArgs]] internal 179 // property. 180 // 4. Let args be a new list containing the same values as the 181 // list boundArgs in the same order followed by the same 182 // values as the list ExtraArgs in the same order. 183 // 5. Return the result of calling the [[Construct]] internal 184 // method of target providing args as the arguments. 185 186 var result = target.apply( 187 this, 188 array_concat.call(args, array_slice.call(arguments)) 189 ); 190 if ($Object(result) === result) { 191 return result; 192 } 193 return this; 194 195 } else { 196 // 15.3.4.5.1 [[Call]] 197 // When the [[Call]] internal method of a function object, F, 198 // which was created using the bind function is called with a 199 // this value and a list of arguments ExtraArgs, the following 200 // steps are taken: 201 // 1. Let boundArgs be the value of F's [[BoundArgs]] internal 202 // property. 203 // 2. Let boundThis be the value of F's [[BoundThis]] internal 204 // property. 205 // 3. Let target be the value of F's [[TargetFunction]] internal 206 // property. 207 // 4. Let args be a new list containing the same values as the 208 // list boundArgs in the same order followed by the same 209 // values as the list ExtraArgs in the same order. 210 // 5. Return the result of calling the [[Call]] internal method 211 // of target providing boundThis as the this value and 212 // providing args as the arguments. 213 214 // equiv: target.call(this, ...boundArgs, ...args) 215 return target.apply( 216 that, 217 array_concat.call(args, array_slice.call(arguments)) 218 ); 219 220 } 221 222 }; 223 224 // 15. If the [[Class]] internal property of Target is "Function", then 225 // a. Let L be the length property of Target minus the length of A. 226 // b. Set the length own property of F to either 0 or L, whichever is 227 // larger. 228 // 16. Else set the length own property of F to 0. 229 230 var boundLength = max(0, target.length - args.length); 231 232 // 17. Set the attributes of the length own property of F to the values 233 // specified in 15.3.5.1. 234 var boundArgs = []; 235 for (var i = 0; i < boundLength; i++) { 236 array_push.call(boundArgs, '$' + i); 237 } 238 239 // XXX Build a dynamic function with desired amount of arguments is the only 240 // way to set the length property of a function. 241 // In environments where Content Security Policies enabled (Chrome extensions, 242 // for ex.) all use of eval or Function costructor throws an exception. 243 // However in all of these environments Function.prototype.bind exists
244 // and so this code will never be executed. 245 bound = Function('binder', 'return function (' + boundArgs.join(',') + '){ return binder.apply(this, arguments); }')(binder); 246 247 if (target.prototype) { 248 Empty.prototype = target.prototype; 249 bound.prototype = new Empty(); 250 // Clean up dangling references. 251 Empty.prototype = null; 252 } 253 254 // TODO 255 // 18. Set the [[Extensible]] internal property of F to true. 256 257 // TODO 258 // 19. Let thrower be the [[ThrowTypeError]] function Object (13.2.3). 259 // 20. Call the [[DefineOwnProperty]] internal method of F with 260 // arguments "caller", PropertyDescriptor {[[Get]]: thrower, [[Set]]: 261 // thrower, [[Enumerable]]: false, [[Configurable]]: false}, and 262 // false. 263 // 21. Call the [[DefineOwnProperty]] internal method of F with 264 // arguments "arguments", PropertyDescriptor {[[Get]]: thrower, 265 // [[Set]]: thrower, [[Enumerable]]: false, [[Configurable]]: false}, 266 // and false. 267 268 // TODO 269 // NOTE Function objects created using Function.prototype.bind do not 270 // have a prototype property or the [[Code]], [[FormalParameters]], and 271 // [[Scope]] internal properties. 272 // XXX can't delete prototype in pure-js. 273 274 // 22. Return F. 275 return bound; 276 } 277 }); 278}) 279.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 280 281(function(undefined) { 282 283 // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/master/packages/polyfill-library/polyfills/DOMTokenList/detect.js 284 var detect = ( 285 'DOMTokenList' in this && (function (x) { 286 return 'classList' in x ? !x.classList.toggle('x', false) && !x.className : true; 287 })(document.createElement('x')) 288 ); 289 290 if (detect) return 291 292 // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/master/packages/polyfill-library/polyfills/DOMTokenList/polyfill.js 293 (function (global) { 294 var nativeImpl = "DOMTokenList" in global && global.DOMTokenList; 295 296 if ( 297 !nativeImpl || 298 ( 299 !!document.createElementNS && 300 !!document.createElementNS('http://www.w3.org/2000/svg', 'svg') && 301 !(document.createElementNS("http://www.w3.org/2000/svg", "svg").classList instanceof DOMTokenList) 302 ) 303 ) { 304 global.DOMTokenList = (function() { // eslint-disable-line no-unused-vars 305 var dpSupport = true; 306 var defineGetter = function (object, name, fn, configurable) { 307 if (Object.defineProperty) 308 Object.defineProperty(object, name, { 309 configurable: false === dpSupport ? true : !!configurable, 310 get: fn 311 }); 312 313 else object.__defineGetter__(name, fn); 314 }; 315 316 /** Ensure the browser allows Object.defineProperty to be used on native JavaScript objects. */ 317 try { 318 defineGetter({}, "support"); 319 } 320 catch (e) { 321 dpSupport = false; 322 } 323 324 325 var _DOMTokenList = function (el, prop) { 326 var that = this; 327 var tokens = []; 328 var tokenMap = {}; 329 var length = 0; 330 var maxLength = 0; 331 var addIndexGetter = function (i) { 332 defineGetter(that, i, function () { 333 preop(); 334 return tokens[i]; 335 }, false); 336 337 }; 338 var reindex = function () { 339 340 /** Define getter functions for array-like access to the tokenList's contents. */ 341 if (length >= maxLength) 342 for (; maxLength < length; ++maxLength) { 343 addIndexGetter(maxLength); 344 } 345 }; 346 347 /** Helper function called at the start of each class method. Internal use only. */ 348 var preop = function () { 349 var error; 350 var i; 351 var args = arguments; 352 var rSpace = /\s+/; 353 354 /** Validate the token/s passed to an instance method, if any. */ 355 if (args.length) 356 for (i = 0; i < args.length; ++i) 357 if (rSpace.test(args[i])) { 358 error = new SyntaxError('String "' + args[i] + '" ' + "contains" + ' an invali
358d character'); 359 error.code = 5; 360 error.name = "InvalidCharacterError"; 361 throw error; 362 } 363 364 365 /** Split the new value apart by whitespace*/ 366 if (typeof el[prop] === "object") { 367 tokens = ("" + el[prop].baseVal).replace(/^\s+|\s+$/g, "").split(rSpace); 368 } else { 369 tokens = ("" + el[prop]).replace(/^\s+|\s+$/g, "").split(rSpace); 370 } 371 372 /** Avoid treating blank strings as single-item token lists */ 373 if ("" === tokens[0]) tokens = []; 374 375 /** Repopulate the internal token lists */ 376 tokenMap = {}; 377 for (i = 0; i < tokens.length; ++i) 378 tokenMap[tokens[i]] = true; 379 length = tokens.length; 380 reindex(); 381 }; 382 383 /** Populate our internal token list if the targeted attribute of the subject element isn't empty. */ 384 preop(); 385 386 /** Return the number of tokens in the underlying string. Read-only. */ 387 defineGetter(that, "length", function () { 388 preop(); 389 return length; 390 }); 391 392 /** Override the default toString/toLocaleString methods to return a space-delimited list of tokens when typecast. */ 393 that.toLocaleString = 394 that.toString = function () { 395 preop(); 396 return tokens.join(" "); 397 }; 398 399 that.item = function (idx) { 400 preop(); 401 return tokens[idx]; 402 }; 403 404 that.contains = function (token) { 405 preop(); 406 return !!tokenMap[token]; 407 }; 408 409 that.add = function () { 410 preop.apply(that, args = arguments); 411 412 for (var args, token, i = 0, l = args.length; i < l; ++i) { 413 token = args[i]; 414 if (!tokenMap[token]) { 415 tokens.push(token); 416 tokenMap[token] = true; 417 } 418 } 419 420 /** Update the targeted attribute of the attached element if the token list's changed. */ 421 if (length !== tokens.length) { 422 length = tokens.length >>> 0; 423 if (typeof el[prop] === "object") { 424 el[prop].baseVal = tokens.join(" "); 425 } else { 426 el[prop] = tokens.join(" "); 427 } 428 reindex(); 429 } 430 }; 431 432 that.remove = function () { 433 preop.apply(that, args = arguments); 434 435 /** Build a hash of token names to compare against when recollecting our token list. */ 436 for (var args, ignore = {}, i = 0, t = []; i < args.length; ++i) { 437 ignore[args[i]] = true; 438 delete tokenMap[args[i]]; 439 } 440 441 /** Run through our tokens list and reassign only those that aren't defined in the hash declared above. */ 442 for (i = 0; i < tokens.length; ++i) 443 if (!ignore[tokens[i]]) t.push(tokens[i]); 444 445 tokens = t; 446 length = t.length >>> 0; 447 448 /** Update the targeted attribute of the attached element. */ 449 if (typeof el[prop] === "object") { 450 el[prop].baseVal = tokens.join(" "); 451 } else { 452 el[prop] = tokens.join(" "); 453 } 454 reindex(); 455 }; 456 457 that.toggle = function (token, force) { 458 preop.apply(that, [token]); 459 460 /** Token state's being forced. */ 461 if (undefined !== force) { 462 if (force) { 463 that.add(token); 464 return true; 465 } else { 466 that.remove(token); 467 return false; 468 } 469 } 470 471 /** Token already exists in tokenList. Remove it, and return FALSE. */ 472 if (tokenMap[token]) { 473 that.remove(token); 474 return false; 475 } 476 477 /** Otherwise, add the token and return TRUE. */ 478 that.add(token); 479 return true; 480 }; 481 482 return that; 483 }; 484 485 return _DOMTokenList; 486 }()); 487 } 488
489 // Add second argument to native DOMTokenList.toggle() if necessary 490 (function () { 491 var e = document.createElement('span'); 492 if (!('classList' in e)) return; 493 e.classList.toggle('x', false); 494 if (!e.classList.contains('x')) return; 495 e.classList.constructor.prototype.toggle = function toggle(token /*, force*/) { 496 var force = arguments[1]; 497 if (force === undefined) { 498 var add = !this.contains(token); 499 this[add ? 'add' : 'remove'](token); 500 return add; 501 } 502 force = !!force; 503 this[force ? 'add' : 'remove'](token); 504 return force; 505 }; 506 }()); 507 508 // Add multiple arguments to native DOMTokenList.add() if necessary 509 (function () { 510 var e = document.createElement('span'); 511 if (!('classList' in e)) return; 512 e.classList.add('a', 'b'); 513 if (e.classList.contains('b')) return; 514 var native = e.classList.constructor.prototype.add; 515 e.classList.constructor.prototype.add = function () { 516 var args = arguments; 517 var l = arguments.length; 518 for (var i = 0; i < l; i++) { 519 native.call(this, args[i]); 520 } 521 }; 522 }()); 523 524 // Add multiple arguments to native DOMTokenList.remove() if necessary 525 (function () { 526 var e = document.createElement('span'); 527 if (!('classList' in e)) return; 528 e.classList.add('a'); 529 e.classList.add('b'); 530 e.classList.remove('a', 'b'); 531 if (!e.classList.contains('b')) return; 532 var native = e.classList.constructor.prototype.remove; 533 e.classList.constructor.prototype.remove = function () { 534 var args = arguments; 535 var l = arguments.length; 536 for (var i = 0; i < l; i++) { 537 native.call(this, args[i]); 538 } 539 }; 540 }()); 541 542 }(this)); 543 544}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 545 546(function(undefined) { 547 548// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Document/detect.js 549var detect = ("Document" in this); 550 551if (detect) return 552 553// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Document&flags=always 554if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { 555 556 if (this.HTMLDocument) { // IE8 557 558 // HTMLDocument is an extension of Document. If the browser has HTMLDocument but not Document, the former will suffice as an alias for the latter. 559 this.Document = this.HTMLDocument; 560 561 } else { 562 563 // Create an empty function to act as the missing constructor for the document object, attach the document object as its prototype. The function needs to be anonymous else it is hoisted and causes the feature detect to prematurely pass, preventing the assignments below being made. 564 this.Document = this.HTMLDocument = document.constructor = (new Function('return function Document() {}')()); 565 this.Document.prototype = document; 566 } 567} 568 569 570}) 571.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 572 573(function(undefined) { 574 575// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Element/detect.js 576var detect = ('Element' in this && 'HTMLElement' in this); 577 578if (detect) return 579 580// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element&flags=always 581(function () { 582 583 // IE8 584 if (window.Element && !window.HTMLElement) { 585 window.HTMLElement = window.Element; 586 return; 587 } 588 589 // create Element constructor 590 window.Element = window.HTMLElement = new Function('return function Element() {}')(); 591 592 // generate sandboxed iframe 593 var vbody = document.appendChild(document.createElement('body')); 594 var frame = vbody.appendChild(document.createElement('iframe')); 595 596 // use sandboxed iframe to replicate Element functionality 597 var frameDocument = frame.contentWindow.document; 598 var prototype = Element.prototype = frameDocument.appendChild(frameDocument.createElement('*')); 599 var cache = {}; 600 601 // polyfill Element.prototype on an element 602 var shiv = function (element, deep) { 603 var 604 childNodes = element.childNodes || [], 605 index = -1, 606 key, value, childNode; 607 608 if (element.nodeType === 1 && element.constructor !== Element) { 609 element.constructor = Element; 610 611 for (key in cache) { 612 value = cache[key]; 613 element[key] = value; 614 } 615 } 616 617 while (childNode = deep && childNodes[++index]) { 618 shiv(childNode, deep); 619 } 620 621 return element; 622 }; 623 624 var elements = document.getElementsByTagName('*'); 625 var nativeCreateElement = document.createElement; 626 var interval; 627 var loopLimit = 100; 628 629 prototype.attachEvent('onpropertychange', function (event) { 630 var 631 propertyName = event.propertyName, 632 nonValue = !cache.hasOwnProperty(propertyName), 633 newValue = prototype[propertyName], 634 oldValue = cache[propertyName], 635 index = -1, 636 element; 637 638 while (element = elements[++index]) { 639 if (element.nodeType === 1) { 640 if (nonValue || element[propertyName] === oldValue) { 641 element[propertyName] = newValue; 642 } 643 } 644 } 645 646 cache[propertyName] = newValue; 647 }); 648 649 prototype.constructor = Element; 650 651 if (!prototype.hasAttribute) { 652 // <Element>.hasAttribute 653 prototype.hasAttribute = function hasAttribute(name) { 654 return this.getAttribute(name) !== null; 655 }; 656 } 657 658 // Apply Element prototype to the pre-existing DOM as soon as the body element appears. 659 function bodyCheck() { 660 if (!(loopLimit--)) clearTimeout(interval); 661 if (document.body && !document.body.prototype && /(complete|interactive)/.test(document.readyState)) { 662 shiv(document, true); 663 if (interval && document.body.prototype) clearTimeout(interval); 664 return (!!document.body.prototype); 665 } 666 return false; 667 } 668 if (!bodyCheck()) { 669 document.onreadystatechange = bodyCheck; 670 interval = setInterval(bodyCheck, 25); 671 } 672 673 // Apply to any new elements created after load 674 document.createElement = function createElement(nodeName) { 675 var element = nativeCreateElement(String(nodeName).toLowerCase()); 676 return shiv(element); 677 }; 678 679 // remove sandboxed iframe 680 document.removeChild(vbody); 681}()); 682 683}) 684.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 685 686(function(undefined) { 687 688 // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/8717
688a9e04ac7aff99b4980fbedead98036b0929a/packages/polyfill-library/polyfills/Element/prototype/classList/detect.js 689 var detect = ( 690 'document' in this && "classList" in document.documentElement && 'Element' in this && 'classList' in Element.prototype && (function () { 691 var e = document.createElement('span'); 692 e.classList.add('a', 'b'); 693 return e.classList.contains('b'); 694 }()) 695 ); 696 697 if (detect) return 698 699 // Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Element.prototype.classList&flags=always 700 (function (global) { 701 var dpSupport = true; 702 var defineGetter = function (object, name, fn, configurable) { 703 if (Object.defineProperty) 704 Object.defineProperty(object, name, { 705 configurable: false === dpSupport ? true : !!configurable, 706 get: fn 707 }); 708 709 else object.__defineGetter__(name, fn); 710 }; 711 /** Ensure the browser allows Object.defineProperty to be used on native JavaScript objects. */ 712 try { 713 defineGetter({}, "support"); 714 } 715 catch (e) { 716 dpSupport = false; 717 } 718 /** Polyfills a property with a DOMTokenList */ 719 var addProp = function (o, name, attr) { 720 721 defineGetter(o.prototype, name, function () { 722 var tokenList; 723 724 var THIS = this, 725 726 /** Prevent this from firing twice for some reason. What the hell, IE. */ 727 gibberishProperty = "__defineGetter__" + "DEFINE_PROPERTY" + name; 728 if(THIS[gibberishProperty]) return tokenList; 729 THIS[gibberishProperty] = true; 730 731 /** 732 * IE8 can't define properties on native JavaScript objects, so we'll use a dumb hack instead. 733 * 734 * What this is doing is creating a dummy element ("reflection") inside a detached phantom node ("mirror") 735 * that serves as the target of Object.defineProperty instead. While we could simply use the subject HTML 736 * element instead, this would conflict with element types which use indexed properties (such as forms and 737 * select lists). 738 */ 739 if (false === dpSupport) { 740 741 var visage; 742 var mirror = addProp.mirror || document.createElement("div"); 743 var reflections = mirror.childNodes; 744 var l = reflections.length; 745 746 for (var i = 0; i < l; ++i) 747 if (reflections[i]._R === THIS) { 748 visage = reflections[i]; 749 break; 750 } 751 752 /** Couldn't find an element's reflection inside the mirror. Materialise one. */ 753 visage || (visage = mirror.appendChild(document.createElement("div"))); 754 755 tokenList = DOMTokenList.call(visage, THIS, attr); 756 } else tokenList = new DOMTokenList(THIS, attr); 757 758 defineGetter(THIS, name, function () { 759 return tokenList; 760 }); 761 delete THIS[gibberishProperty]; 762 763 return tokenList; 764 }, true); 765 }; 766 767 addProp(global.Element, "classList", "className"); 768 addProp(global.HTMLElement, "classList", "className"); 769 addProp(global.HTMLLinkElement, "relList", "rel"); 770 addProp(global.HTMLAnchorElement, "relList", "rel"); 771 addProp(global.HTMLAreaElement, "relList", "rel"); 772 }(this)); 773 774}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 775 776function Accordion ($module) { 777 this.$module = $module; 778 this.moduleId = $module.getAttribute('id'); 779 this.$sections = $module.querySelectorAll('.govuk-accordion__section'); 780 this.$openAllButton = ''; 781 this.browserSupportsSessionStorage = helper.checkForSessionStorage(); 782 783 this.controlsClass = 'govuk-accordion__controls'; 784 this.openAllClass = 'govuk-accordion__open-all'; 785 this.iconClass = 'govuk-accordion__icon'; 786 787 this.sectionHeaderClass = 'govuk-accordion__section-header'; 788 this.sectionHeaderFocusedClass = 'govuk-accordion__section-header--focused'; 789 this.sectionHeadingClass = 'govuk-accordion__section-heading'; 790 this.sectionSummaryClass = 'govuk-accordion__section-summary'; 791 this.sectionButtonClass = 'govuk-accordion__section-button'; 792 this.sectionExpandedClass = 'govuk-accordion__section--expanded'; 793} 794 795// Initialize component 796Accordion.prototype.init = function () { 797 // Check for module 798 if (!this.$module) { 799 return 800 } 801 802 this.initControls(); 803 804 this.initSectionHeaders(); 805 806 // See if "Open all" button text should be updated 807 var areAllSectionsOpen = this.checkIfAllSectionsOpen(); 808 this.updateOpenAllButton(areAllSectionsOpen); 809}; 810 811// Initialise controls and set attributes 812Accordion.prototype.initControls = function () { 813 // Create "Open all" button and set attributes 814 this.$openAllButton = document.createElement('button'); 815 this.$openAllButton.setAttribute('type', 'button'); 816 this.$openAllButton.innerHTML = 'Open all <span class="govuk-visually-hidden">sections</span>'; 817 this.$openAllButton.setAttribute('class', this.openAllClass); 818 this.$openAllButton.setAttribute('aria-expanded', 'false'); 819 this.$openAllButton.setAttribute('type', 'button'); 820 821 // Create control wrapper and add controls to it 822 var accordionControls = document.createElement('div'); 823 accordionControls.setAttribute('class', this.controlsClass); 824 accordionControls.appendChild(this.$openAllButton); 825 this.$module.insertBefore(accordionControls, this.$module.firstChild); 826 827 // Handle events for the controls 828 this.$openAllButton.addEventListener('click', this.onOpenOrCloseAllToggle.bind(this)); 829}; 830 831// Initialise section headers 832Accordion.prototype.initSectionHeaders = function () { 833 // Loop through section headers 834 nodeListForEach(this.$sections, function ($section, i) { 835 // Set header attributes 836 var header = $section.querySelector('.' + this.sectionHeaderClass); 837 this.initHeaderAttributes(header, i); 838 839 this.setExpanded(this.isExpanded($section), $section); 840 841 // Handle events 842 header.addEventListener('click', this.onSectionToggle.bind(this, $section)); 843 844 // See if there is any state stored in sessionStorage and set the sections to 845 // open or closed. 846 this.setInitialState($section); 847 }.bind(this)); 848}; 849 850// Set individual header attributes 851Accordion.prototype.initHeaderAttributes = function ($headerWrapper, index) { 852 var $module = this; 853 var $span = $headerWrapper.querySelector('.' + this.sectionButtonClass); 854 var $heading = $headerWrapper.querySelector('.' + this.sectionHeadingClass); 855 var $summary = $headerWrapper.querySelector('.' + this.sectionSummaryClass); 856 857 // Copy existing span element to an actual button element, for improved accessibility. 858 var $button = document.createElement('button'); 859 $button.setAttribute('type', 'button'); 860 $button.setAttribute('id', this.moduleId + '-heading-' + (index + 1)); 861 $button.setAttribute('aria-controls', this.moduleId + '-content-' + (index + 1)); 862 863 // Copy all attributes (https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes) from $span to $button 864 for (var i = 0; i < $span.attributes.length; i++) { 865 var attr = $span.attributes.item(i); 866 $button.setAttribute(attr.nodeName, attr.nodeValue); 867 } 868 869 $button.addEventListener('focusin', function (e) { 870 if (!$headerWrapper.classList.contains($module.sectionHeaderFocusedClass)) { 871 $headerWrapper.className += ' ' + $module.sectionHeaderFocusedClass; 872 } 873 }); 874 875 $button.addEventListener('blur', function (e) { 876 $headerWrapper.classList.remove($module.sectionHeaderFocusedClass); 877 }); 878 879 // if (typeof ($summary) !== 'undefined' && $summary !== null) { 880 // $button.setAttribute('aria-describedby', this.moduleId + '-summary-' + (index + 1)); 881 // } 882 883 // $span could contain HTML elements (see https://www.w3.org/TR/2011/WD-html5-20110525/content-models.html#phrasing-content) 884 $button.innerHTML = $span.innerHTML; 885 886 $heading.removeChild($span); 887 $heading.appendChild($button); 888 889 // Add "+/-" icon 890 var icon = document.createElement('span'); 891 icon.className = this.iconClass; 892 icon.setAttribute('aria-hidden', 'true'); 893 894 $heading.appendChild(icon); 895}; 896 897// When section toggled, set and store state 898Accordion.prototype.onSectionToggle = function ($section) { 899 var expanded = this.isExpanded($section); 900 this.setExpanded(!expanded, $section); 901 902 // Store the state in sessionStorage when a change is triggered 903 this.storeState($section); 904}; 905 906// When Open/Close All toggled, set and store state 907Accordion.prototype.onOpenOrCloseAllToggle = function () { 908 var $module = this; 909 var $sections = this.$sections; 910 911 var nowExpanded = !this.checkIfAllSectionsOpen(); 912 913 nodeListForEach($sections, function ($section) { 914 $module.setExpanded(nowExpanded, $section); 915 // Store the state in sessionStorage when a change is triggered 916 $module.storeState($section); 917 }); 918 919 $module.updateOpenAllButton(nowExpanded); 920}; 921 922// Set section attributes when opened/closed 923Accordion.prototype.setExpanded = function (expanded, $section) { 924 var $button = $section.querySelector('.' + this.sectionButtonClass); 925 $button.setAttribute('aria-expanded', expanded); 926 927 if (expanded) { 928 $section.classList.add(this.sectionExpandedClass); 929 } else { 930 $section.classList.remove(this.sectionExpandedClass); 931 } 932 933 // See if "Open all" button text should be updated 934 var areAllSectionsOpen = this.checkIfAllSectionsOpen(); 935 this.updateOpenAllButton(areAllSectionsOpen); 936}; 937 938// Get state of section 939Accordion.prototype.isExpanded = function ($section) { 940 return $section.classList.contains(this.sectionExpandedClass) 941}; 942 943// Check if all sections are open 944Accordion.prototype.checkIfAllSectionsOpen = function () { 945 // Get a count of all the Accordion sections 946 var sectionsCount = this.$sections.length; 947 // Get a count of all Accordion sections that are expanded 948 var expandedSectionCount = this.$module.querySelectorAll('.' + this.sectionExpandedClass).length; 949 var areAllSectionsOpen = sectionsCount === expandedSectionCount; 950 951 return areAllSectionsOpen 952}; 953 954// Update "Open all" button 955Accordion.prototype.updateOpenAllButton = function (expanded) { 956 var newButtonText = expanded ? 'Close all' : 'Open all'; 957 newButtonText += '<span class="govuk-visually-hidden"> sections</span>'; 958 this.$openAllButton.setAttribute('aria-expanded', expanded); 959 this.$openAllButton.innerHTML = newButtonText; 960}; 961 962// Check for `window.sessionStorage`, and that it actually works. 963var helper = { 964 checkForSessionStorage: function () { 965 var testString = 'this is the test string'; 966 var result; 967 try { 968 window.sessionStorage.setItem(testString, testString); 969 result = window.sessionStorage.getItem(testString) === testString.toString(); 970 window.sessionStorage.removeItem(testString); 971 return result 972 } catch (exception) { 973 if ((typeof console === 'undefined' || typeof console.log === 'undefined')) { 974 console.log('Notice: sessionStorage not available.'); 975 } 976 } 977 } 978}; 979 980// Set the state of the accordions in sessionStorage 981Accordion.prototype.storeState = function ($section) { 982 if (this.browserSupportsSessionStorage) { 983 // We need a unique way of identifying each content in the accordion. Since 984 // an `#id` should be unique and an `id` is required for `aria-` attributes 985 // `id` can be safely used. 986 var $button = $section.querySelector('.' + this.sectionButtonClass); 987 988 if ($button) { 989 var contentId = $button.getAttribute('aria-controls'); 990 var contentState = $button.getAttribute('aria-expanded'); 991 992 if (typeof contentId === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) { 993 console.error(new Error('No aria controls present in accordion section heading.')); 994 } 995 996 if (typeof contentState === 'undefined' && (typeof console === 'undefined' || typeof console.log === 'undefined')) { 997 console.error(new Error('No aria expanded present in accordion section heading.')); 998 } 999 1000 // Only set the state when both `contentId` and `contentState` are taken from the DOM. 1001 if (contentId && contentState) { 1002 window.sessionStorage.setItem(contentId, contentState); 1003 } 1004 } 1005 } 1006}; 1007 1008// Read the state of the accordions from sessionStorage 1009Accordion.prototype.setInitialState = function ($section) { 1010 if (this.browserSupportsSessionStorage) { 1011 var $button = $section.querySelector('.' + this.sectionButtonClass); 1012 1013 if ($button) { 1014 var contentId = $button.getAttribute('aria-controls'); 1015 var contentState = contentId ? window.sessionStorage.getItem(contentId) : null; 1016 1017 if (contentState !== null) { 1018 this.setExpanded(contentState === 'true', $section); 1019 } 1020 } 1021 } 1022}; 1023 1024(function(undefined) { 1025 1026// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Window/detect.js 1027var detect = ('Window' in this); 1028 1029if (detect) return 1030 1031// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Window&flags=always 1032if ((typeof WorkerGlobalScope === "undefined") && (typeof importScripts !== "function")) { 1033 (function (global) { 1034 if (global.constructor) { 1035 global.Window = global.constructor; 1036 } else { 1037 (global.Window = global.constructor = new Function('return function Window() {}')()).prototype = this; 1038 } 1039 }(this)); 1040} 1041 1042}) 1043.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 1044 1045(function(undefined) { 1046 1047// Detection from https://github.com/Financial-Times/polyfill-service/blob/master/packages/polyfill-library/polyfills/Event/detect.js 1048var detect = ( 1049 (function(global) { 1050 1051 if (!('Event' in global)) return false; 1052 if (typeof global.Event === 'function') return true; 1053 1054 try { 1055 1056 // In IE 9-11, the Event object exists but cannot be instantiated 1057 new Event('click'); 1058 return true; 1059 } catch(e) { 1060 return false; 1061 } 1062 }(this)) 1063); 1064 1065if (detect) return 1066 1067// Polyfill from https://cdn.polyfill.io/v2/polyfill.js?features=Event&flags=always 1068(function () { 1069 var unlistenableWindowEvents = { 1070 click: 1, 1071 dblclick: 1, 1072 keyup: 1, 1073 keypress: 1, 1074 keydown: 1, 1075 mousedown: 1, 1076 mouseup: 1, 1077 mousemove: 1, 1078 mouseover: 1, 1079 mouseenter: 1, 1080 mouseleave: 1, 1081 mouseout: 1, 1082 storage: 1, 1083 storagecommit: 1, 1084 textinput: 1 1085 }; 1086 1087 // This polyfill depends on availability of `document` so will not run in a worker 1088 // However, we asssume there are no browsers with worker support that lack proper
1089 // support for `Event` within the worker 1090 if (typeof document === 'undefined' || typeof window === 'undefined') return; 1091 1092 function indexOf(array, element) { 1093 var 1094 index = -1, 1095 length = array.length; 1096 1097 while (++index < length) { 1098 if (index in array && array[index] === element) { 1099 return index; 1100 } 1101 } 1102 1103 return -1; 1104 } 1105 1106 var existingProto = (window.Event && window.Event.prototype) || null; 1107 window.Event = Window.prototype.Event = function Event(type, eventInitDict) { 1108 if (!type) { 1109 throw new Error('Not enough arguments'); 1110 } 1111 1112 var event; 1113 // Shortcut if browser supports createEvent 1114 if ('createEvent' in document) { 1115 event = document.createEvent('Event'); 1116 var bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; 1117 var cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; 1118 1119 event.initEvent(type, bubbles, cancelable); 1120 1121 return event; 1122 } 1123 1124 event = document.createEventObject(); 1125 1126 event.type = type; 1127 event.bubbles = eventInitDict && eventInitDict.bubbles !== undefined ? eventInitDict.bubbles : false; 1128 event.cancelable = eventInitDict && eventInitDict.cancelable !== undefined ? eventInitDict.cancelable : false; 1129 1130 return event; 1131 }; 1132 if (existingProto) { 1133 Object.defineProperty(window.Event, 'prototype', { 1134 configurable: false, 1135 enumerable: false, 1136 writable: true, 1137 value: existingProto 1138 }); 1139 } 1140 1141 if (!('createEvent' in document)) { 1142 window.addEventListener = Window.prototype.addEventListener = Document.prototype.addEventListener = Element.prototype.addEventListener = function addEventListener() { 1143 var 1144 element = this, 1145 type = arguments[0], 1146 listener = arguments[1]; 1147 1148 if (element === window && type in unlistenableWindowEvents) { 1149 throw new Error('In IE8 the event: ' + type + ' is not available on the window object. Please see https://github.com/Financial-Times/polyfill-service/issues/317 for more information.'); 1150 } 1151 1152 if (!element._events) { 1153 element._events = {}; 1154 } 1155 1156 if (!element._events[type]) { 1157 element._events[type] = function (event) { 1158 var 1159 list = element._events[event.type].list, 1160 events = list.slice(), 1161 index = -1, 1162 length = events.length, 1163 eventElement; 1164 1165 event.preventDefault = function preventDefault() { 1166 if (event.cancelable !== false) { 1167 event.returnValue = false; 1168 } 1169 }; 1170 1171 event.stopPropagation = function stopPropagation() { 1172 event.cancelBubble = true; 1173 }; 1174 1175 event.stopImmediatePropagation = function stopImmediatePropagation() { 1176 event.cancelBubble = true; 1177 event.cancelImmediate = true; 1178 }; 1179 1180 event.currentTarget = element; 1181 event.relatedTarget = event.fromElement || null; 1182 event.target = event.target || event.srcElement || element; 1183 event.timeStamp = new Date().getTime(); 1184 1185 if (event.clientX) { 1186 event.pageX = event.clientX + document.documentElement.scrollLeft; 1187 event.pageY = event.clientY + document.documentElement.scrollTop; 1188 } 1189 1190 while (++index < length && !event.cancelImmediate) { 1191 if (index in events) { 1192 eventElement = events[index]; 1193 1194 if (indexOf(list, eventElement) !== -1 && typeof eventElement === 'function') { 1195 eventElement.call(element, event); 1196 } 1197 } 1198 } 1199 }; 1200 1201 element._events[type].list = []; 1202 1203 if (element.attachEvent) { 1204 element.attachEvent('on' + type, element._events[type]); 1205 } 1206 } 1207 1208 element._events[type].list.push(listener); 1209 }; 1210 1211 window.removeEventListener = Window.prototype.removeEventListener = Document.prototype.removeEventListener = Element.prototype.removeEventListener = function removeEventListener() { 1212 var 1213 element = this, 1214 type = arguments[0], 1215 listener = arguments[1], 1216 index; 1217 1218 if (element._events && element._events[type] && element._events[type].list) { 1219 index = indexOf(element._events[type].list, listener); 1220 1221 if (index !== -1) { 1222 element._events[type].list.splice(index, 1); 1223 1224 if (!element._events[type].list.length) { 1225 if (element.detachEvent) { 1226 element.detachEvent('on' + type, element._events[type]); 1227 } 1228 delete element._events[type]; 1229 } 1230 } 1231 } 1232 }; 1233 1234 window.dispatchEvent = Window.prototype.dispatchEvent = Document.prototype.dispatchEvent = Element.prototype.dispatchEvent = function dispatchEvent(event) { 1235 if (!arguments.length) { 1236 throw new Error('Not enough arguments'); 1237 } 1238 1239 if (!event || typeof event.type !== 'string') { 1240 throw new Error('DOM Events Exception 0'); 1241 } 1242 1243 var element = this, type = event.type; 1244 1245 try { 1246 if (!event.bubbles) { 1247 event.cancelBubble = true; 1248 1249 var cancelBubbleEvent = function (event) { 1250 event.cancelBubble = true; 1251 1252 (element || window).detachEvent('on' + type, cancelBubbleEvent); 1253 }; 1254 1255 this.attachEvent('on' + type, cancelBubbleEvent); 1256 } 1257 1258 this.fireEvent('on' + type, event); 1259 } catch (error) { 1260 event.target = element; 1261 1262 do { 1263 event.currentTarget = element; 1264 1265 if ('_events' in element && typeof element._events[type] === 'function') { 1266 element._events[type].call(element, event); 1267 } 1268 1269 if (typeof element['on' + type] === 'function') { 1270 element['on' + type].call(element, event); 1271 } 1272 1273 element = element.nodeType === 9 ? element.parentWindow : element.parentNode; 1274 } while (element && !event.cancelBubble); 1275 } 1276 1277 return true; 1278 }; 1279 1280 // Add the DOMContentLoaded Event 1281 document.attachEvent('onreadystatechange', function() { 1282 if (document.readyState === 'complete') { 1283 document.dispatchEvent(new Event('DOMContentLoaded', { 1284 bubbles: true 1285 })); 1286 } 1287 }); 1288 } 1289}()); 1290 1291}) 1292.call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 1293 1294var KEY_SPACE = 32; 1295var DEBOUNCE_TIMEOUT_IN_SECONDS = 1; 1296 1297function Button ($module) { 1298 this.$module = $module; 1299 this.debounceFormSubmitTimer = null; 1300} 1301 1302/**
1303* JavaScript 'shim' to trigger the click event of element(s) when the space key is pressed. 1304* 1305* Created since some Assistive Technologies (for example some Screenreaders) 1306* will tell a user to press space on a 'button', so this functionality needs to be shimmed 1307* See https://github.com/alphagov/govuk_elements/pull/272#issuecomment-233028270 1308* 1309* @param {object} event event 1310*/ 1311Button.prototype.handleKeyDown = function (event) { 1312 // get the target element 1313 var target = event.target; 1314 // if the element has a role='button' and the pressed key is a space, we'll simulate a click 1315 if (target.getAttribute('role') === 'button' && event.keyCode === KEY_SPACE) { 1316 event.preventDefault(); 1317 // trigger the target's click event 1318 target.click(); 1319 } 1320}; 1321 1322/** 1323* If the click quickly succeeds a previous click then nothing will happen. 1324* This stops people accidentally causing multiple form submissions by 1325* double clicking buttons. 1326*/ 1327Button.prototype.debounce = function (event) { 1328 var target = event.target; 1329 // Check the button that is clicked on has the preventDoubleClick feature enabled 1330 if (target.getAttribute('data-prevent-double-click') !== 'true') { 1331 return 1332 } 1333 1334 // If the timer is still running then we want to prevent the click from submitting the form 1335 if (this.debounceFormSubmitTimer) { 1336 event.preventDefault(); 1337 return false 1338 } 1339 1340 this.debounceFormSubmitTimer = setTimeout(function () { 1341 this.debounceFormSubmitTimer = null; 1342 }.bind(this), DEBOUNCE_TIMEOUT_IN_SECONDS * 1000); 1343}; 1344 1345/** 1346* Initialise an event listener for keydown at document level 1347* this will help listening for later inserted elements with a role="button" 1348*/ 1349Button.prototype.init = function () { 1350 this.$module.addEventListener('keydown', this.handleKeyDown); 1351 this.$module.addEventListener('click', this.debounce); 1352}; 1353 1354/** 1355 * JavaScript 'polyfill' for HTML5's <details> and <summary> elements 1356 * and 'shim' to add accessiblity enhancements for all browsers 1357 * 1358 * http://caniuse.com/#feat=details 1359 */ 1360 1361var KEY_ENTER = 13; 1362var KEY_SPACE$1 = 32; 1363 1364// Create a flag to know if the browser supports navtive details 1365var NATIVE_DETAILS = typeof document.createElement('details').open === 'boolean'; 1366 1367function Details ($module) { 1368 this.$module = $module; 1369} 1370 1371/** 1372* Handle cross-modal click events 1373* @param {object} node element 1374* @param {function} callback function 1375*/ 1376Details.prototype.handleInputs = function (node, callback) { 1377 node.addEventListener('keypress', function (event) { 1378 var target = event.target; 1379 // When the key gets pressed - check if it is enter or space 1380 if (event.keyCode === KEY_ENTER || event.keyCode === KEY_SPACE$1) { 1381 if (target.nodeName.toLowerCase() === 'summary') { 1382 // Prevent space from scrolling the page 1383 // and enter from submitting a form 1384 event.preventDefault(); 1385 // Click to let the click event do all the necessary action 1386 if (target.click) { 1387 target.click(); 1388 } else { 1389 // except Safari 5.1 and under don't support .click() here 1390 callback(event); 1391 } 1392 } 1393 } 1394 }); 1395 1396 // Prevent keyup to prevent clicking twice in Firefox when using space key 1397 node.addEventListener('keyup', function (event) { 1398 var target = event.target; 1399 if (event.keyCode === KEY_SPACE$1) { 1400 if (target.nodeName.toLowerCase() === 'summary') { 1401 event.preventDefault(); 1402 } 1403 } 1404 }); 1405 1406 node.addEventListener('click', callback); 1407}; 1408 1409Details.prototype.init = function () { 1410 var $module = this.$module; 1411 1412 if (!$module) { 1413 return 1414 } 1415 1416 // Save shortcuts to the inner summary and content elements 1417 var $summary = this.$summary = $module.getElementsByTagName('summary').item(0); 1418 var $content = this.$content = $module.getElementsByTagName('div').item(0); 1419 1420 // If <details> doesn't have a <summary> and a <div> representing the content 1421 // it means the required HTML structure is not met so the script will stop 1422 if (!$summary || !$content) { 1423 return 1424 } 1425 1426 // If the content doesn't have an ID, assign it one now 1427 // which we'll need for the summary's aria-controls assignment 1428 if (!$content.id) { 1429 $content.id = 'details-content-' + generateUniqueID(); 1430 } 1431 1432 // Add ARIA role="group" to details 1433 $module.setAttribute('role', 'group'); 1434 1435 // Add role=button to summary 1436 // $summary.setAttribute('role', 'button'); 1437 // Removed for WCAG 2.2 1438 1439 // Add aria-controls 1440 $summary.setAttribute('aria-controls', $content.id); 1441 1442 // Set tabIndex so the summary is keyboard accessible for non-native elements 1443 // 1444 // We have to use the camelcase `tabIndex` property as there is a bug in IE6/IE7 when we set the correct attribute lowercase: 1445 // See http://web.archive.org/web/20170120194036/http://www.salience
1445s.com/browserBugs/tabIndex.html for more information. 1446 if (!NATIVE_DETAILS) { 1447 $summary.tabIndex = 0; 1448 } 1449 1450 // Detect initial open state 1451 var openAttr = $module.getAttribute('open') !== null; 1452 if (openAttr === true) { 1453 $summary.setAttribute('aria-expanded', 'true'); 1454 $content.setAttribute('aria-hidden', 'false'); 1455 } else { 1456 $summary.setAttribute('aria-expanded', 'false'); 1457 $content.setAttribute('aria-hidden', 'true'); 1458 if (!NATIVE_DETAILS) { 1459 $content.style.display = 'none'; 1460 } 1461 } 1462 1463 // Bind an event to handle summary elements 1464 this.handleInputs($summary, this.setAttributes.bind(this)); 1465}; 1466 1467/** 1468* Define a statechange function that updates aria-expanded and style.display 1469* @param {object} summary element 1470*/ 1471Details.prototype.setAttributes = function () { 1472 var $module = this.$module; 1473 var $summary = this.$summary; 1474 var $content = this.$content; 1475 1476 var expanded = $summary.getAttribute('aria-expanded') === 'true'; 1477 var hidden = $content.getAttribute('aria-hidden') === 'true'; 1478 1479 $summary.setAttribute('aria-expanded', (expanded ? 'false' : 'true')); 1480 $content.setAttribute('aria-hidden', (hidden ? 'false' : 'true')); 1481 1482 if (!NATIVE_DETAILS) { 1483 $content.style.display = (expanded ? 'none' : ''); 1484 1485 var hasOpenAttr = $module.getAttribute('open') !== null; 1486 if (!hasOpenAttr) { 1487 $module.setAttribute('open', 'open'); 1488 } else { 1489 $module.removeAttribute('open'); 1490 } 1491 } 1492 return true 1493}; 1494 1495/** 1496* Remove the click event from the node element 1497* @param {object} node element 1498*/ 1499Details.prototype.destroy = function (node) { 1500 node.removeEventListener('keypress'); 1501 node.removeEventListener('keyup'); 1502 node.removeEventListener('click'); 1503}; 1504 1505function CharacterCount ($module) { 1506 this.$module = $module; 1507 this.$textarea = $module.querySelector('.govuk-js-character-count'); 1508} 1509 1510CharacterCount.prototype.defaults = { 1511 characterCountAttribute: 'data-maxlength', 1512 wordCountAttribute: 'data-maxwords' 1513}; 1514 1515// Initialize component 1516CharacterCount.prototype.init = function () { 1517 // Check for module 1518 var $module = this.$module; 1519 var $textarea = this.$textarea; 1520 if (!$textarea) { 1521 return 1522 } 1523 1524 // Read options set using dataset ('data-' values) 1525 this.options = this.getDataset($module); 1526 1527 // Determine the limit attribute (characters or words) 1528 var countAttribute = this.defaults.characterCountAttribute; 1529 if (this.options.maxwords) { 1530 countAttribute = this.defaults.wordCountAttribute; 1531 } 1532 1533 // Save the element limit 1534 this.maxLength = $module.getAttribute(countAttribute); 1535 1536 // Check for limit 1537 if (!this.maxLength) { 1538 return 1539 } 1540 1541 // Generate and reference message 1542 var boundCreateCountMessage = this.createCountMessage.bind(this); 1543 this.countMessage = boundCreateCountMessage(); 1544 1545 // If there's a maximum length defined and the count message exists 1546 if (this.countMessage) { 1547 // Remove hard limit if set 1548 $module.removeAttribute('maxlength'); 1549 1550 // Bind event changes to the textarea 1551 var boundChangeEvents = this.bindChangeEvents.bind(this); 1552 boundChangeEvents(); 1553 1554 // Update count message 1555 var boundUpdateCountMessage = this.updateCountMessage.bind(this); 1556 boundUpdateCountMessage(); 1557 } 1558}; 1559 1560// Read data attributes 1561CharacterCount.prototype.getDataset = function (element) { 1562 var dataset = {}; 1563 var attributes = element.attributes; 1564 if (attributes) { 1565 for (var i = 0; i < attributes.length; i++) { 1566 var attribute = attributes[i]; 1567 var match = attribute.name.match(/^data-(.+)/); 1568 if (match) { 1569 dataset[match[1]] = attribute.value; 1570 } 1571 } 1572 } 1573 return dataset 1574}; 1575 1576// Counts characters or words in text 1577CharacterCount.prototype.count = function (text) { 1578 var length; 1579 if (this.options.maxwords) { 1580 var tokens = text.match(/\S+/g) || []; // Matches consecutive non-whitespace chars 1581 length = tokens.length; 1582 } else { 1583 length = text.length; 1584 } 1585 return length 1586}; 1587 1588// Generate count message and bind it to the input 1589// returns reference to the generated element 1590CharacterCount.prototype.createCountMessage = function () { 1591 var countElement = this.$textarea; 1592 var elementId = countElement.id; 1593 // Check for existing info count message 1594 var countMessage = document.getElementById(elementId + '-info'); 1595 // If there is no existing info count message we add one right after the field 1596 if (elementId && !countMessage) { 1597 countElement.insertAdjacentHTML('afterend', '<span id="' + elementId + '-info" class="govuk-hint govuk-character-count__message" aria-live="polite"></span>'); 1598 this.describedBy = countElement.getAttribute('aria-describedby'); 1599 this.describedByInfo = this.describedBy + ' ' + elementId + '-info'; 1600 countElement.setAttribute('aria-describedby', this.describedByInfo); 1601 countMessage = document.getElementById(elementId + '-info'); 1602 } else { 1603 // If there is an existing info count message we move it right after the field 1604 countElement.insertAdjacentElement('afterend', countMessage); 1605 } 1606 return countMessage 1607}; 1608 1609// Bind input propertychange to the elements and update based on the change 1610CharacterCount.prototype.bindChangeEvents = function () { 1611 var $textarea = this.$textarea; 1612 $textarea.addEventListener('keyup', this.checkIfValueChanged.bind(this)); 1613 1614 // Bind focus/blur events to start/stop polling 1615 $textarea.addEventListener('focus', this.handleFocus.bind(this)); 1616 $textarea.addEventListener('blur', this.handleBlur.bind(this)); 1617}; 1618 1619// Speech recognition software such as Dragon NaturallySpeaking will modify the
1620// fields by directly changing its `value`. These changes don't trigger events 1621// in JavaScript, so we need to poll to handle when and if they occur. 1622CharacterCount.prototype.checkIfValueChanged = function () { 1623 if (!this.$textarea.oldValue) this.$textarea.oldValue = ''; 1624 if (this.$textarea.value !== this.$textarea.oldValue) { 1625 this.$textarea.oldValue = this.$textarea.value; 1626 var boundUpdateCountMessage = this.updateCountMessage.bind(this); 1627 boundUpdateCountMessage(); 1628 } 1629}; 1630 1631// Update message box 1632CharacterCount.prototype.updateCountMessage = function () { 1633 var countElement = this.$textarea; 1634 var options = this.options; 1635 var countMessage = this.countMessage; 1636 1637 // Determine the remaining number of characters/words 1638 var currentLength = this.count(countElement.value); 1639 var maxLength = this.maxLength; 1640 var remainingNumber = maxLength - currentLength; 1641 1642 // Set threshold if presented in options 1643 var thresholdPercent = options.threshold ? options.threshold : 0; 1644 var thresholdValue = maxLength * thresholdPercent / 100; 1645 if (thresholdValue > currentLength) { 1646 countMessage.classList.add('govuk-character-count__message--disabled'); 1647 // Ensure threshold is hidden for users of assistive technologies 1648 countMessage.setAttribute('aria-hidden', true); 1649 } else { 1650 countMessage.classList.remove('govuk-character-count__message--disabled'); 1651 // Ensure threshold is visible for users of assistive technologies 1652 countMessage.removeAttribute('aria-hidden'); 1653 } 1654 1655 // Update styles 1656 if (remainingNumber < 0) { 1657 countElement.classList.add('govuk-textarea--error'); 1658 countMessage.classList.remove('govuk-hint'); 1659 countMessage.classList.add('govuk-error-message'); 1660 } else { 1661 countElement.classList.remove('govuk-textarea--error'); 1662 countMessage.classList.remove('govuk-error-message'); 1663 countMessage.classList.add('govuk-hint'); 1664 } 1665 1666 // Update message 1667 var charVerb = 'remaining'; 1668 var charNoun = 'character'; 1669 var displayNumber = remainingNumber; 1670 if (options.maxwords) { 1671 charNoun = 'word'; 1672 } 1673 charNoun = charNoun + ((remainingNumber === -1 || remainingNumber === 1) ? '' : 's'); 1674 1675 charVerb = (remainingNumber < 0) ? 'too many' : 'remaining'; 1676 displayNumber = Math.abs(remainingNumber); 1677 1678 countMessage.innerHTML = 'You have ' + displayNumber + ' ' + charNoun + ' ' + charVerb; 1679}; 1680 1681CharacterCount.prototype.handleFocus = function () { 1682 // Check if value changed on focus 1683 this.valueChecker = setInterval(this.checkIfValueChanged.bind(this), 1000); 1684}; 1685 1686CharacterCount.prototype.handleBlur = function () { 1687 // Cancel value checking on blur 1688 clearInterval(this.valueChecker); 1689}; 1690 1691function Checkboxes ($module) { 1692 this.$module = $module; 1693 this.$inputs = $module.querySelectorAll('input[type="checkbox"]'); 1694} 1695 1696Checkboxes.prototype.init = function () { 1697 var $module = this.$module; 1698 var $inputs = this.$inputs; 1699 1700 /** 1701 * Loop over all items with [data-controls] 1702 * Check if they have a matching conditional reveal 1703 * If they do, assign attributes. 1704 **/ 1705 nodeListForEach($inputs, function ($input) { 1706 var controls = $input.getAttribute('data-aria-controls'); 1707 1708 // Check if input controls anything 1709 // Check if content exists, before setting attributes. 1710 if (!controls || !$module.querySelector('#' + controls)) { 1711 return 1712 } 1713 1714 // If we have content that is controlled, set attributes. 1715 $input.setAttribute('aria-controls', controls); 1716 $input.removeAttribute('data-aria-controls'); 1717 this.setAttributes($input); 1718 }.bind(this)); 1719 1720 // Handle events 1721 $module.addEventListener('click', this.handleClick.bind(this)); 1722}; 1723 1724Checkboxes.prototype.setAttributes = function ($input) { 1725 var inputIsChecked = $input.checked; 1726 $input.setAttribute('aria-expanded', inputIsChecked); 1727 1728 var $content = this.$module.querySelector('#' + $input.getAttribute('aria-controls')); 1729 if ($content) { 1730 $content.classList.toggle('govuk-checkboxes__conditional--hidden', !inputIsChecked); 1731 } 1732}; 1733 1734Checkboxes.prototype.handleClick = function (event) { 1735 var $target = event.target; 1736 1737 // If a checkbox with aria-controls, handle click 1738 var isCheckbox = $target.getAttribute('type') === 'checkbox';
1739 var hasAriaControls = $target.getAttribute('aria-controls'); 1740 if (isCheckbox && hasAriaControls) { 1741 this.setAttributes($target); 1742 } 1743}; 1744 1745(function(undefined) { 1746 1747 // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/detect.js 1748 var detect = ( 1749 'document' in this && "matches" in document.documentElement 1750 ); 1751 1752 if (detect) return 1753 1754 // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/matches/polyfill.js 1755 Element.prototype.matches = Element.prototype.webkitMatchesSelector || Element.prototype.oMatchesSelector || Element.prototype.msMatchesSelector || Element.prototype.mozMatchesSelector || function matches(selector) { 1756 var element = this; 1757 var elements = (element.document || element.ownerDocument).querySelectorAll(selector); 1758 var index = 0; 1759 1760 while (elements[index] && elements[index] !== element) { 1761 ++index; 1762 } 1763 1764 return !!elements[index]; 1765 }; 1766 1767}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 1768 1769(function(undefined) { 1770 1771 // Detection from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/detect.js 1772 var detect = ( 1773 'document' in this && "closest" in document.documentElement 1774 ); 1775 1776 if (detect) return 1777 1778 // Polyfill from https://raw.githubusercontent.com/Financial-Times/polyfill-service/1f3c09b402f65bf6e393f933a15ba63f1b86ef1f/packages/polyfill-library/polyfills/Element/prototype/closest/polyfill.js 1779 Element.prototype.closest = function closest(selector) { 1780 var node = this; 1781 1782 while (node) { 1783 if (node.matches(selector)) return node; 1784 else node = 'SVGElement' in window && node instanceof SVGElement ? node.parentNode : node.parentElement; 1785 } 1786 1787 return null; 1788 }; 1789 1790}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 1791 1792function ErrorSummary ($module) { 1793 this.$module = $module; 1794} 1795 1796ErrorSummary.prototype.init = function () { 1797 var $module = this.$module; 1798 if (!$module) { 1799 return 1800 } 1801 $module.focus(); 1802 1803 $module.addEventListener('click', this.handleClick.bind(this)); 1804}; 1805 1806/** 1807* Click event handler 1808* 1809* @param {MouseEvent} event - Click event 1810*/ 1811ErrorSummary.prototype.handleClick = function (event) { 1812 var target = event.target; 1813 if (this.focusTarget(target)) { 1814 event.preventDefault(); 1815 } 1816}; 1817 1818/** 1819 * Focus the target element 1820 * 1821 * By default, the browser will scroll the target into view. Because our labels 1822 * or legends appear above the input, this means the user will be presented with 1823 * an input without any context, as the label or legend will be off the top of 1824 * the screen. 1825 * 1826 * Manually handling the click event, scrolling the question into view and then 1827 * focussing the element solves this. 1828 * 1829 * This also results in the label and/or legend being announced correctly in 1830 * NVDA (as tested in 2018.3.2) - without this only the field type is announced 1831 * (e.g. "Edit, has autocomplete"). 1832 * 1833 * @param {HTMLElement} $target - Event target 1834 * @returns {boolean} True if the target was able to be focussed 1835 */ 1836ErrorSummary.prototype.focusTarget = function ($target) { 1837 // If the element that was clicked was not a link, return early 1838 if ($target.tagName !== 'A' || $target.href === false) { 1839 return false 1840 } 1841 1842 var inputId = this.getFragmentFromUrl($target.href); 1843 var $input = document.getElementById(inputId); 1844 if (!$input) { 1845 return false 1846 } 1847 1848 var $legendOrLabel = this.getAssociatedLegendOrLabel($input); 1849 if (!$legendOrLabel) { 1850 return false 1851 } 1852 1853 // Scroll the legend or label into view *before* calling focus on the input to 1854 // avoid extra scrolling in browsers that don't support `preventScroll` (which 1855 // at time of writing is most of them...) 1856 $legendOrLabel.scrollIntoView(); 1857 $input.focus({ preventScroll: true });
1858 1859 return true 1860}; 1861 1862/** 1863 * Get fragment from URL 1864 * 1865 * Extract the fragment (everything after the hash) from a URL, but not including 1866 * the hash. 1867 * 1868 * @param {string} url - URL 1869 * @returns {string} Fragment from URL, without the hash 1870 */ 1871ErrorSummary.prototype.getFragmentFromUrl = function (url) { 1872 if (url.indexOf('#') === -1) { 1873 return false 1874 } 1875 1876 return url.split('#').pop() 1877}; 1878 1879/** 1880 * Get associated legend or label 1881 * 1882 * Returns the first element that exists from this list: 1883 * 1884 * - The `<legend>` associated with the closest `<fieldset>` ancestor 1885 * - The first `<label>` that is associated with the input using for="inputId" 1886 * - The closest parent `<label>` 1887 * 1888 * @param {HTMLElement} $input - The input 1889 * @returns {HTMLElement} Associated legend or label, or null if no associated 1890 * legend or label can be found 1891 */ 1892ErrorSummary.prototype.getAssociatedLegendOrLabel = function ($input) { 1893 var $fieldset = $input.closest('fieldset'); 1894 1895 if ($fieldset) { 1896 var legends = $fieldset.getElementsByTagName('legend'); 1897 1898 if (legends.length) { 1899 return legends[0] 1900 } 1901 } 1902 1903 return document.querySelector("label[for='" + $input.getAttribute('id') + "']") || 1904 $input.closest('label') 1905}; 1906 1907function Header ($module) { 1908 this.$module = $module; 1909} 1910 1911Header.prototype.init = function () { 1912 // Check for module 1913 var $module = this.$module; 1914 if (!$module) { 1915 return 1916 } 1917 1918 // Check for button 1919 var $toggleButton = $module.querySelector('.govuk-js-header-toggle'); 1920 if (!$toggleButton) { 1921 return 1922 } 1923 1924 // Handle $toggleButton click events 1925 $toggleButton.addEventListener('click', this.handleClick.bind(this)); 1926}; 1927 1928/** 1929* Toggle class 1930* @param {object} node element 1931* @param {string} className to toggle 1932*/ 1933Header.prototype.toggleClass = function (node, className) { 1934 if (node.className.indexOf(className) > 0) { 1935 node.className = node.className.replace(' ' + className, ''); 1936 } else { 1937 node.className += ' ' + className; 1938 } 1939}; 1940 1941/** 1942* An event handler for click event on $toggleButton 1943* @param {object} event event 1944*/ 1945Header.prototype.handleClick = function (event) { 1946 var $module = this.$module; 1947 var $toggleButton = event.target || event.srcElement; 1948 var $target = $module.querySelector('#' + $toggleButton.getAttribute('aria-controls')); 1949 1950 // If a button with aria-controls, handle click 1951 if ($toggleButton && $target) { 1952 this.toggleClass($target, 'govuk-header__navigation--open'); 1953 this.toggleClass($toggleButton, 'govuk-header__menu-button--open'); 1954 1955 $toggleButton.setAttribute('aria-expanded', $toggleButton.getAttribute('aria-expanded') !== 'true'); 1956 $target.setAttribute('aria-hidden', $target.getAttribute('aria-hidden') === 'false'); 1957 } 1958}; 1959 1960function Radios ($module) { 1961 this.$module = $module; 1962} 1963 1964Radios.prototype.init = function () { 1965 var $module = this.$module; 1966 var $inputs = $module.querySelectorAll('input[type="radio"]'); 1967 1968 /** 1969 * Loop over all items with [data-controls] 1970 * Check if they have a matching conditional reveal 1971 * If they do, assign attributes. 1972 **/ 1973 nodeListForEach($inputs, function ($input) { 1974 var controls = $input.getAttribute('data-aria-controls'); 1975 1976 // Check if input controls anything 1977 // Check if content exists, before setting attributes. 1978 if (!controls || !$module.querySelector('#' + controls)) { 1979 return 1980 } 1981 1982 // If we have content that is controlled, set attributes. 1983 $input.setAttribute('aria-controls', controls); 1984 $input.removeAttribute('data-aria-controls'); 1985 this.setAttributes($input); 1986 }.bind(this)); 1987 1988 // Handle events 1989 $module.addEventListener('click', this.handleClick.bind(this)); 1990}; 1991 1992Radios.prototype.setAttributes = function ($input) { 1993 var $content = document.querySelector('#' + $input.getAttribute('aria-controls')); 1994 1995 if ($content && $content.classList.contains('govuk-radios__conditional')) { 1996 var inputIsChecked = $input.checked; 1997 1998 $input.setAttribute('aria-expanded', inputIsChecked); 1999 2000 $content.classList.toggle('govuk-radios__conditional--hidden', !inputIsChecked); 2001 } 2002}; 2003 2004Radios.prototype.handleClick = function (event) { 2005 var $clickedInput = event.target; 2006 // We only want to handle clicks for radio inputs 2007 if ($clickedInput.type !== 'radio') { 2008 return 2009 } 2010 // Because checking one radio can uncheck a radio in another $module, 2011 // we need to call set attributes on all radios in the same form, or document if they're not in a form. 2012 // 2013 // We also only want radios which have aria-controls, as they support conditional reveals. 2014 var $allInputs = document.querySelectorAll('input[type="radio"][aria-controls]'); 2015 nodeListForEach($allInputs, function ($input) {
2016 // Only inputs with the same form owner should change. 2017 var hasSameFormOwner = ($input.form === $clickedInput.form); 2018 2019 // In radios, only radios with the same name will affect each other. 2020 var hasSameName = ($input.name === $clickedInput.name); 2021 if (hasSameName && hasSameFormOwner) { 2022 this.setAttributes($input); 2023 } 2024 }.bind(this)); 2025}; 2026 2027(function(undefined) { 2028 2029 // Detection from https://github.com/Financial-Times/polyfill-service/pull/1062/files#diff-b09a5d2acf3314b46a6c8f8d0c31b85c 2030 var detect = ( 2031 'Element' in this && "nextElementSibling" in document.documentElement 2032 ); 2033 2034 if (detect) return 2035 2036 2037 (function (global) { 2038 2039 // Polyfill from https://github.com/Financial-Times/polyfill-service/pull/1062/files#diff-404b69b4750d18dea4174930a49170fd 2040 Object.defineProperty(Element.prototype, "nextElementSibling", { 2041 get: function(){ 2042 var el = this.nextSibling; 2043 while (el && el.nodeType !== 1) { el = el.nextSibling; } 2044 return (el.nodeType === 1) ? el : null; 2045 } 2046 }); 2047 2048 }(this)); 2049 2050}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 2051 2052(function(undefined) { 2053 2054 // Detection from https://github.com/Financial-Times/polyfill-service/pull/1062/files#diff-a162235fbc9c0dd40d4032265f44942e 2055 var detect = ( 2056 'Element' in this && 'previousElementSibling' in document.documentElement 2057 ); 2058 2059 if (detect) return 2060 2061 (function (global) { 2062 // Polyfill from https://github.com/Financial-Times/polyfill-service/pull/1062/files#diff-b45a1197b842728cb76b624b6ba7d739 2063 Object.defineProperty(Element.prototype, 'previousElementSibling', { 2064 get: function(){ 2065 var el = this.previousSibling; 2066 while (el && el.nodeType !== 1) { el = el.previousSibling; } 2067 return (el.nodeType === 1) ? el : null; 2068 } 2069 }); 2070 2071 }(this)); 2072 2073}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); 2074 2075function Tabs ($module) { 2076 this.$module = $module; 2077 this.$tabs = $module.querySelectorAll('.govuk-tabs__tab'); 2078 2079 this.keys = { left: 37, right: 39, up: 38, down: 40 }; 2080 this.jsHiddenClass = 'govuk-tabs__panel--hidden'; 2081} 2082 2083Tabs.prototype.init = function () { 2084 if (typeof window.matchMedia === 'function') { 2085 this.setupResponsiveChecks(); 2086 } else { 2087 this.setup(); 2088 } 2089}; 2090 2091Tabs.prototype.setupResponsiveChecks = function () { 2092 this.mql = window.matchMedia('(min-width: 40.0625em)'); 2093 this.mql.addListener(this.checkMode.bind(this)); 2094 this.checkMode(); 2095}; 2096 2097Tabs.prototype.checkMode = function () { 2098 if (this.mql.matches) { 2099 this.setup(); 2100 } else { 2101 this.teardown(); 2102 } 2103}; 2104 2105Tabs.prototype.setup = function () { 2106 var $module = this.$module; 2107 var $tabs = this.$tabs; 2108 var $tabList = $module.querySelector('.govuk-tabs__list'); 2109 var $tabListItems = $module.querySelectorAll('.govuk-tabs__list-item'); 2110 2111 if (!$tabs || !$tabList || !$tabListItems) { 2112 return 2113 } 2114 2115 $tabList.setAttribute('role', 'tablist'); 2116 2117 nodeListForEach($tabListItems, function ($item) { 2118 $item.setAttribute('role', 'presentation'); 2119 }); 2120 2121 nodeListForEach($tabs, function ($tab) { 2122 // Set HTML attributes 2123 this.setAttributes($tab); 2124 2125 // Save bounded functions to use when removing event listeners during teardown 2126 $tab.boundTabClick = this.onTabClick.bind(this); 2127 $tab.boundTabKeydown = this.onTabKeydown.bind(this); 2128 2129 // Handle events 2130 $tab.addEventListener('click', $tab.boundTabClick, true); 2131 $tab.addEventListener('keydown', $tab.boundTabKeydown, true); 2132 2133 // Remove old active panels 2134 this.hideTab($tab); 2135 }.bind(this)); 2136 2137 // Show either the active tab according to the URL's hash or the first tab 2138 var $activeTab = this.getTab(window.location.hash) || this.$tabs[0]; 2139 this.showTab($activeTab); 2140 2141 // Handle hashchange events 2142 $module.boundOnHashChange = this.onHashChange.bind(this); 2143 window.addEventListener('hashchange', $module.boundOnHashChange, true); 2144}; 2145 2146Tabs.prototype.teardown = function () { 2147 var $module = this.$module; 2148 var $tabs = this.$tabs; 2149 var $tabList = $module.querySelector('.govuk-tabs__list'); 2150 var $tabListItems = $module.querySelectorAll('.govuk-tabs__list-item'); 2151 2152 if (!$tabs || !$tabList || !$tabListItems) { 2153 return 2154 } 2155 2156 $tabList.removeAttribute('role'); 2157 2158 nodeListForEach($tabListItems, function ($item) { 2159 $item.removeAttribute('role', 'presentation'); 2160 }); 2161 2162 nodeListForEach($tabs, function ($tab) { 2163 // Remove events 2164 $tab.removeEventListener('click', $tab.boundTabClick, true); 2165 $tab.removeEventListener('keydown', $tab.boundTabKeydown, true); 2166 2167 // Unset HTML attributes 2168 this.unsetAttributes($tab); 2169 }.bind(this)); 2170 2171 // Remove hashchange event handler 2172 window.removeEventListener('hashchange', $module.boundOnHashChange, true); 2173}; 2174 2175Tabs.prototype.onHashChange = function (e) { 2176 var hash = window.location.hash; 2177 var $tabWithHash = this.getTab(hash); 2178 if (!$tabWithHash) { 2179 return 2180 } 2181 2182 // Prevent changing the hash 2183 if (this.changingHash) { 2184 this.changingHash = false; 2185 return 2186 } 2187 2188 // Show either the active tab according to the URL's hash or the first tab 2189 var $previousTab = this.getCurrentTab(); 2190 2191 this.hideTab($previousTab); 2192 this.showTab($tabWithHash); 2193 $tabWithHash.focus(); 2194}; 2195 2196Tabs.prototype.hideTab = function ($tab) { 2197 this.unhighlightTab($tab); 2198 this.hidePanel($tab); 2199}; 2200 2201Tabs.prototype.showTab = function ($tab) { 2202 this.highlightTab($tab); 2203 this.showPanel($tab); 2204}; 2205
2206Tabs.prototype.getTab = function (hash) { 2207 return this.$module.querySelector('.govuk-tabs__tab[href="' + hash + '"]') 2208}; 2209 2210Tabs.prototype.setAttributes = function ($tab) { 2211 // set tab attributes 2212 var panelId = this.getHref($tab).slice(1); 2213 $tab.setAttribute('id', 'tab_' + panelId); 2214 $tab.setAttribute('role', 'tab'); 2215 $tab.setAttribute('aria-controls', panelId); 2216 $tab.setAttribute('aria-selected', 'false'); 2217 $tab.setAttribute('tabindex', '-1'); 2218 2219 // set panel attributes 2220 var $panel = this.getPanel($tab); 2221 $panel.setAttribute('role', 'tabpanel'); 2222 $panel.setAttribute('aria-labelledby', $tab.id); 2223 $panel.classList.add(this.jsHiddenClass); 2224}; 2225 2226Tabs.prototype.unsetAttributes = function ($tab) { 2227 // unset tab attributes 2228 $tab.removeAttribute('id'); 2229 $tab.removeAttribute('role'); 2230 $tab.removeAttribute('aria-controls'); 2231 $tab.removeAttribute('aria-selected'); 2232 $tab.removeAttribute('tabindex'); 2233 2234 // unset panel attributes 2235 var $panel = this.getPanel($tab); 2236 $panel.removeAttribute('role'); 2237 $panel.removeAttribute('aria-labelledby'); 2238 $panel.classList.remove(this.jsHiddenClass); 2239}; 2240 2241Tabs.prototype.onTabClick = function (e) { 2242 if (!e.target.classList.contains('govuk-tabs__tab')) { 2243 // Allow events on child DOM elements to bubble up to tab parent 2244 return false 2245 } 2246 e.preventDefault(); 2247 var $newTab = e.target; 2248 var $currentTab = this.getCurrentTab(); 2249 this.hideTab($currentTab); 2250 this.showTab($newTab); 2251 this.createHistoryEntry($newTab); 2252}; 2253 2254Tabs.prototype.createHistoryEntry = function ($tab) { 2255 var $panel = this.getPanel($tab); 2256 2257 // Save and restore the id 2258 // so the page doesn't jump when a user clicks a tab (which changes the hash) 2259 var id = $panel.id; 2260 $panel.id = ''; 2261 this.changingHash = true; 2262 window.location.hash = this.getHref($tab).slice(1); 2263 $panel.id = id; 2264}; 2265 2266Tabs.prototype.onTabKeydown = function (e) { 2267 switch (e.keyCode) { 2268 case this.keys.left: 2269 case this.keys.up: 2270 this.activatePreviousTab(); 2271 e.preventDefault(); 2272 break 2273 case this.keys.right: 2274 case this.keys.down: 2275 this.activateNextTab(); 2276 e.preventDefault(); 2277 break 2278 } 2279}; 2280 2281Tabs.prototype.activateNextTab = function () { 2282 var currentTab = this.getCurrentTab(); 2283 var nextTabListItem = currentTab.parentNode.nextElementSibling; 2284 if (nextTabListItem) { 2285 var nextTab = nextTabListItem.querySelector('.govuk-tabs__tab'); 2286 } 2287 if (nextTab) { 2288 this.hideTab(currentTab); 2289 this.showTab(nextTab); 2290 nextTab.focus(); 2291 this.createHistoryEntry(nextTab); 2292 } 2293}; 2294 2295Tabs.prototype.activatePreviousTab = function () { 2296 var currentTab = this.getCurrentTab(); 2297 var previousTabListItem = currentTab.parentNode.previousElementSibling; 2298 if (previousTabListItem) { 2299 var previousTab = previousTabListItem.querySelector('.govuk-tabs__tab'); 2300 } 2301 if (previousTab) { 2302 this.hideTab(currentTab); 2303 this.showTab(previousTab); 2304 previousTab.focus(); 2305 this.createHistoryEntry(previousTab); 2306 } 2307}; 2308 2309Tabs.prototype.getPanel = function ($tab) { 2310 var $panel = this.$module.querySelector(this.getHref($tab)); 2311 return $panel 2312}; 2313 2314Tabs.prototype.showPanel = function ($tab) { 2315 var $panel = this.getPanel($tab); 2316 $panel.classList.remove(this.jsHiddenClass); 2317}; 2318 2319Tabs.prototype.hidePanel = function (tab) { 2320 var $panel = this.getPanel(tab); 2321 $panel.classList.add(this.jsHiddenClass); 2322}; 2323 2324Tabs.prototype.unhighlightTab = function ($tab) { 2325 $tab.setAttribute('aria-selected', 'false'); 2326 $tab.parentNode.classList.remove('govuk-tabs__list-item--selected'); 2327 $tab.setAttribute('tabindex', '-1'); 2328}; 2329 2330Tabs.prototype.highlightTab = function ($tab) { 2331 $tab.setAttribute('aria-selected', 'true'); 2332 $tab.parentNode.classList.add('govuk-tabs__list-item--selected'); 2333 $tab.setAttribute('tabindex', '0'); 2334}; 2335 2336Tabs.prototype.getCurrentTab = function () { 2337 return this.$module.querySelector('.govuk-tabs__list-item--selected .govuk-tabs__tab') 2338}; 2339 2340// this is because IE doesn't always return the actual value but a relative full path 2341// should be a utility function most prob
2342// http://labs.thesedays.com/blog/2010/01/08/getting-the-href-value-with-jquery-in-ie/ 2343Tabs.prototype.getHref = function ($tab) { 2344 var href = $tab.getAttribute('href'); 2345 var hash = href.slice(href.indexOf('#'), href.length); 2346 return hash 2347}; 2348 2349function initAll (options) { 2350 // Set the options to an empty object by default if no options are passed. 2351 options = typeof options !== 'undefined' ? options : {}; 2352 2353 // Allow the user to initialise GOV.UK Frontend in only certain sections of the page 2354 // Defaults to the entire document if nothing is set. 2355 var scope = typeof options.scope !== 'undefined' ? options.scope : document; 2356 2357 var $buttons = scope.querySelectorAll('[data-module="govuk-button"]'); 2358 nodeListForEach($buttons, function ($button) { 2359 new Button($button).init(); 2360 }); 2361 2362 var $accordions = scope.querySelectorAll('[data-module="govuk-accordion"]'); 2363 nodeListForEach($accordions, function ($accordion) { 2364 new Accordion($accordion).init(); 2365 }); 2366 2367 var $details = scope.querySelectorAll('[data-module="govuk-details"]'); 2368 nodeListForEach($details, function ($detail) { 2369 new Details($detail).init(); 2370 }); 2371 2372 var $characterCounts = scope.querySelectorAll('[data-module="govuk-character-count"]'); 2373 nodeListForEach($characterCounts, function ($characterCount) { 2374 new CharacterCount($characterCount).init(); 2375 }); 2376 2377 var $checkboxes = scope.querySelectorAll('[data-module="govuk-checkboxes"]'); 2378 nodeListForEach($checkboxes, function ($checkbox) { 2379 new Checkboxes($checkbox).init(); 2380 }); 2381 2382 // Find first error summary module to enhance. 2383 var $errorSummary = scope.querySelector('[data-module="govuk-error-summary"]'); 2384 new ErrorSummary($errorSummary).init(); 2385 2386 // Find first header module to enhance. 2387 var $toggleButton = scope.querySelector('[data-module="govuk-header"]'); 2388 new Header($toggleButton).init(); 2389 2390 var $radios = scope.querySelectorAll('[data-module="govuk-radios"]'); 2391 nodeListForEach($radios, function ($radio) { 2392 new Radios($radio).init(); 2393 }); 2394 2395 var $tabs = scope.querySelectorAll('[data-module="govuk-tabs"]'); 2396 nodeListForEach($tabs, function ($tabs) { 2397 new Tabs($tabs).init(); 2398 }); 2399} 2400 2401exports.initAll = initAll; 2402exports.Accordion = Accordion; 2403exports.Button = Button; 2404exports.Details = Details; 2405exports.CharacterCount = CharacterCount; 2406exports.Checkboxes = Checkboxes; 2407exports.ErrorSummary = ErrorSummary; 2408exports.Header = Header; 2409exports.Radios = Radios; 2410exports.Tabs = Tabs; 2411 2412})));
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.