vendor: 5,328 bytes, lines 1-153
1/*! 2 * Web Experience Toolkit (WET) / Boîte à outils de l'expérience Web (BOEW) 3 * wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 4 * v4.0.88.1 - 2025-06-04 5 * 6 */ 7 8/*! Modernizr (Custom Build) | MIT & BSD */ 9/*! @license DOMPurify 3.2.6 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.6/LICENSE */ 10 11(function (global, factory) { 12 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 13 typeof define === 'function' && define.amd ? define(factory) : 14 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.DOMPurify = factory()); 15})(this, (function () { 'use strict'; 16 17 const { 18 entries, 19 setPrototypeOf, 20 isFrozen, 21 getPrototypeOf, 22 getOwnPropertyDescriptor 23 } = Object; 24 let { 25 freeze, 26 seal, 27 create 28 } = Object; // eslint-disable-line import/no-mutable-exports 29 let { 30 apply, 31 construct 32 } = typeof Reflect !== 'undefined' && Reflect; 33 if (!freeze) { 34 freeze = function freeze(x) { 35 return x; 36 }; 37 } 38 if (!seal) { 39 seal = function seal(x) { 40 return x; 41 }; 42 } 43 if (!apply) { 44 apply = function apply(fun, thisValue, args) { 45 return fun.apply(thisValue, args); 46 }; 47 } 48 if (!construct) { 49 construct = function construct(Func, args) { 50 return new Func(...args); 51 }; 52 } 53 const arrayForEach = unapply(Array.prototype.forEach); 54 const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf); 55 const arrayPop = unapply(Array.prototype.pop); 56 const arrayPush = unapply(Array.prototype.push); 57 const arraySplice = unapply(Array.prototype.splice); 58 const stringToLowerCase = unapply(String.prototype.toLowerCase); 59 const stringToString = unapply(String.prototype.toString); 60 const stringMatch = unapply(String.prototype.match); 61 const stringReplace = unapply(String.prototype.replace); 62 const stringIndexOf = unapply(String.prototype.indexOf); 63 const stringTrim = unapply(String.prototype.trim); 64 const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty); 65 const regExpTest = unapply(RegExp.prototype.test); 66 const typeErrorCreate = unconstruct(TypeError); 67 /** 68 * Creates a new function that calls the given function with a specified thisArg and arguments. 69 * 70 * @param func - The function to be wrapped and called. 71 * @returns A new function that calls the given function with a specified thisArg and arguments. 72 */ 73 function unapply(func) { 74 return function (thisArg) { 75 if (thisArg instanceof RegExp) { 76 thisArg.lastIndex = 0; 77 } 78 for (var _len = arguments.length, args = new Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 79 args[_key - 1] = arguments[_key]; 80 } 81 return apply(func, thisArg, args); 82 }; 83 } 84 /** 85 * Creates a new function that constructs an instance of the given constructor function with the provided arguments. 86 * 87 * @param func - The constructor function to be wrapped and called. 88 * @returns A new function that constructs an instance of the given constructor function with the provided arguments. 89 */ 90 function unconstruct(func) { 91 return function () { 92 for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 93 args[_key2] = arguments[_key2]; 94 } 95 return construct(func, args); 96 }; 97 } 98 /** 99 * Add properties to a lookup table 100 * 101 * @param set - The set to which elements will be added. 102 * @param array - The array containing elements to be added to the set. 103 * @param transformCaseFunc - An optional function to transform the case of each element before adding to the set. 104 * @returns The modified set with added elements. 105 */ 106 function addToSet(set, array) { 107 let transformCaseFunc = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : stringToLowerCase; 108 if (setPrototypeOf) { 109 // Make 'in' and truthy checks like Boolean(set.constructor) 110 // independent of any properties defined on Object.prototype. 111 // Prevent prototype setters from intercepting set as a this value. 112 setPrototypeOf(set, null); 113 } 114 let l = array.length; 115 while (l--) { 116 let element = array[l]; 117 if (typeof element === 'string') { 118 const lcElement = transformCaseFunc(element); 119 if (lcElement !== element) { 120 // Config presets (e.g. tags.js, attrs.js) are immutable. 121 if (!isFrozen(array)) { 122 array[l] = lcElement; 123 } 124 element = lcElement; 125 } 126 } 127 set[element] = true; 128 } 129 return set; 130 } 131 /** 132 * Clean up an array to harden against CSPP 133 * 134 * @param array - The array to be cleaned. 135 * @returns The cleaned version of the array 136 */ 137 function cleanArray(array) { 138 for (let index = 0; index < array.length; index++) { 139 const isPropertyExist = objectHasOwnProperty(array, index); 140 if (!isPropertyExist) { 141 array[index] = null; 142 } 143 } 144 return array; 145 } 146 /** 147 * Shallow clone an object 148 * 149 * @param object - The object to be cloned. 150 * @returns A new object that copies the original. 151 */ 152 function clone(object) { 153 const newObject = create(null);
vendor: 4,348 bytes, lines 154-205
154 for (const [property, value] of entries(object)) { 155 const isPropertyExist = objectHasOwnProperty(object, property); 156 if (isPropertyExist) { 157 if (Array.isArray(value)) { 158 newObject[property] = cleanArray(value); 159 } else if (value && typeof value === 'object' && value.constructor === Object) { 160 newObject[property] = clone(value); 161 } else { 162 newObject[property] = value; 163 } 164 } 165 } 166 return newObject; 167 } 168 /** 169 * This method automatically checks if the prop is function or getter and behaves accordingly. 170 * 171 * @param object - The object to look up the getter function in its prototype chain. 172 * @param prop - The property name for which to find the getter function. 173 * @returns The getter function found in the prototype chain or a fallback function. 174 */ 175 function lookupGetter(object, prop) { 176 while (object !== null) { 177 const desc = getOwnPropertyDescriptor(object, prop); 178 if (desc) { 179 if (desc.get) { 180 return unapply(desc.get); 181 } 182 if (typeof desc.value === 'function') { 183 return unapply(desc.value); 184 } 185 } 186 object = getPrototypeOf(object); 187 } 188 function fallbackValue() { 189 return null; 190 } 191 return fallbackValue; 192 } 193 194 const html$1 = freeze(['a', 'abbr', 'acronym', 'address', 'area', 'article', 'aside', 'audio', 'b', 'bdi', 'bdo', 'big', 'blink', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'content', 'data', 'datalist', 'dd', 'decorator', 'del', 'details', 'dfn', 'dialog', 'dir', 'div', 'dl', 'dt', 'element', 'em', 'fieldset', 'figcaption', 'figure', 'font', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'main', 'map', 'mark', 'marquee', 'menu', 'menuitem', 'meter', 'nav', 'nobr', 'ol', 'optgroup', 'option', 'output', 'p', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'section', 'select', 'shadow', 'small', 'source', 'spacer', 'span', 'strike', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'tt', 'u', 'ul', 'var', 'video', 'wbr']); 195 const svg$1 = freeze(['svg', 'a', 'altglyph', 'altglyphdef', 'altglyphitem', 'animatecolor', 'animatemotion', 'animatetransform', 'circle', 'clippath', 'defs', 'desc', 'ellipse', 'filter', 'font', 'g', 'glyph', 'glyphref', 'hkern', 'image', 'line', 'lineargradient', 'marker', 'mask', 'metadata', 'mpath', 'path', 'pattern', 'polygon', 'polyline', 'radialgradient', 'rect', 'stop', 'style', 'switch', 'symbol', 'text', 'textpath', 'title', 'tref', 'tspan', 'view', 'vkern']); 196 const svgFilters = freeze(['feBlend', 'feColorMatrix', 'feComponentTransfer', 'feComposite', 'feConvolveMatrix', 'feDiffuseLighting', 'feDisplacementMap', 'feDistantLight', 'feDropShadow', 'feFlood', 'feFuncA', 'feFuncB', 'feFuncG', 'feFuncR', 'feGaussianBlur', 'feImage', 'feMerge', 'feMergeNode', 'feMorphology', 'feOffset', 'fePointLight', 'feSpecularLighting', 'feSpotLight', 'feTile', 'feTurbulence']); 197 // List of SVG elements that are disallowed by default. 198 // We still need to know them so that we can do namespace 199 // checks properly in case one wants to add them to 200 // allow-list. 201 const svgDisallowed = freeze(['animate', 'color-profile', 'cursor', 'discard', 'font-face', 'font-face-format', 'font-face-name', 'font-face-src', 'font-face-uri', 'foreignobject', 'hatch', 'hatchpath', 'mesh', 'meshgradient', 'meshpatch', 'meshrow', 'missing-glyph', 'script', 'set', 'solidcolor', 'unknown', 'use']); 202 const mathMl$1 = freeze(['math', 'menclose', 'merror', 'mfenced', 'mfrac', 'mglyph', 'mi', 'mlabeledtr', 'mmultiscripts', 'mn', 'mo', 'mover', 'mpadded', 'mphantom', 'mroot', 'mrow', 'ms', 'mspace', 'msqrt', 'mstyle', 'msub', 'msup', 'msubsup', 'mtable', 'mtd', 'mtext', 'mtr', 'munder', 'munderover', 'mprescripts']); 203 // Similarly to SVG, we want to know all MathML elements, 204 // even those that we disallow by default. 205 const mathMlDisallowed = freeze(['maction', 'maligngroup', 'malignmark', 'mlongdiv', 'mscarries', 'mscarry', 'msgroup', 'mstack', 'msline', 'msrow', 'semantics', 'annotation', 'annotation-xml', 'mprescripts', 'n
205one']); 206 const text = freeze(['#text']); 207 208 const html = freeze(['accept', 'action', 'align', 'alt', 'autocapitalize', 'autocomplete', 'autopictureinpicture', 'autoplay', 'background', 'bgcolor', 'border', 'capture', 'cellpadding', 'cellspacing', 'checked', 'cite', 'class', 'clear', 'color', 'cols', 'colspan', 'controls', 'controlslist', 'coords', 'crossorigin', 'datetime', 'decoding', 'default', 'dir', 'disabled', 'disablepictureinpicture', 'disableremoteplayback', 'download', 'draggable', 'enctype', 'enterkeyhint', 'face', 'for', 'headers', 'height', 'hidden', 'high', 'href', 'hreflang', 'id', 'inputmode', 'integrity', 'ismap', 'kind', 'label', 'lang', 'list', 'loading', 'loop', 'low', 'max', 'maxlength', 'media', 'method', 'min', 'minlength', 'multiple', 'muted', 'name', 'nonce', 'noshade', 'novalidate', 'nowrap', 'open', 'optimum', 'pattern', 'placeholder', 'playsinline', 'popover', 'popovertarget', 'popovertargetaction', 'poster', 'preload', 'pubdate', 'radiogroup', 'readonly', 'rel', 'required', 'rev', 'reversed', 'role', 'rows', 'rowspan', 'spellcheck', 'scope', 'selected', 'shape', 'size', 'sizes', 'span', 'srclang', 'start', 'src', 'srcset', 'step', 'style', 'summary', 'tabindex', 'title', 'translate', 'type', 'usemap', 'valign', 'value', 'width', 'wrap', 'xmlns', 'slot']); 209 const svg = freeze(['accent-height', 'accumulate', 'additive', 'alignment-baseline', 'amplitude', 'ascent', 'attributename', 'attributetype', 'azimuth', 'basefrequency', 'baseline-shift', 'begin', 'bias', 'by', 'class', 'clip', 'clippathunits', 'clip-path', 'clip-rule', 'color', 'color-interpolation', 'color-interpolation-filters', 'color-profile', 'color-rendering', 'cx', 'cy', 'd', 'dx', 'dy', 'diffuseconstant', 'direction', 'display', 'divisor', 'dur', 'edgemode', 'elevation', 'end', 'exponent', 'fill', 'fill-opacity', 'fill-rule', 'filter', 'filterunits', 'flood-color', 'flood-opacity', 'font-family', 'font-size', 'font-size-adjust', 'font-stretch', 'font-style', 'font-variant', 'font-weight', 'fx', 'fy', 'g1', 'g2', 'glyph-name', 'glyphref', 'gradientunits', 'gradienttransform', 'height', 'href', 'id', 'image-rendering', 'in', 'in2', 'intercept', 'k', 'k1', 'k2', 'k3', 'k4', 'kerning', 'keypoints', 'keysplines', 'keytimes', 'lang', 'lengthadjust', 'letter-spacing', 'kernelmatrix', 'kernelunitlength', 'lighting-color', 'local', 'marker-end', 'marker-mid', 'marker-start', 'markerheight', 'markerunits', 'markerwidth', 'maskcontentunits', 'maskunits', 'max', 'mask', 'media', 'method', 'mode', 'min', 'name', 'numoctaves', 'offset', 'operator', 'opacity', 'order', 'orient', 'orientation', 'origin', 'overflow', 'paint-order', 'path', 'pathlength', 'patterncontentunits', 'patterntransform', 'patternunits', 'points', 'preservealpha', 'preserveaspectratio', 'primitiveunits', 'r', 'rx', 'ry', 'radius', 'refx', 'refy', 'repeatcount', 'repeatdur', 'restart', 'result', 'rotate', 'scale', 'seed', 'shape-rendering', 'slope', 'specularconstant', 'specularexponent', 'spreadmethod', 'startoffset', 'stddeviation', 'stitchtiles', 'stop-color', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap', 'stroke-linejoin', 'stroke-miterlimit', 'stroke-opacity', 'stroke', 'stroke-width', 'style', 'surfacescale', 'systemlanguage', 'tabindex', 'tablevalues', 'targetx', 'targety', 'transform', 'transform-origin', 'text-anchor', 'text-decoration', 'text-rendering', 'textlength', 'type', 'u1', 'u2', 'unicode', 'values', 'viewbox', 'visibility', 'version', 'vert-adv-y', 'vert-origin-x', 'vert-origin-y', 'width', 'word-spacing', 'wrap', 'writing-mode', 'xchannelselector', 'ychannelselector', 'x', 'x1', 'x2', 'xmlns', 'y', 'y1', 'y2', 'z', 'zoomandpan']); 210 const mathMl = freeze(['accent', 'accentunder', 'align', 'bevelled', 'close', 'columnsalign', 'columnlines', 'columnspan', 'denomalign', 'depth', 'dir', 'display', 'displaystyle', 'encoding', 'fence', 'frame', 'height', 'href', 'id', 'largeop', 'length', 'linethickness', 'lspace', 'lquote', 'mathbackground', 'mathcolor', 'mathsize', 'mathvariant', 'maxsize', 'minsize', 'movablelimits', 'notation', 'numalign', 'open', 'rowalign', 'rowlines', 'rowspacing', 'rowspan', 'rspace', 'rquote', 'scriptlevel', 'scriptminsize', 'scriptsizemultiplier', 'selection', 'separator', 'separators', 'stretchy', 'subscriptshift', 'supscriptshift', 'symmetric', 'voffset', 'width', 'xmlns']); 211 const xml = freeze(['xlink:href', 'xml:id', 'xlink:title', 'xml:space', 'xmlns:xlink']); 212 213 // eslint-disable-next-line unicorn/better-regex 214 const MUSTACHE_EXPR = seal(/\{\{[\w\W]*|[\w\W]*\}\}/gm); // Specify template detection regex for SAFE_FOR_TEMPLATES mode 215 const ERB_EXPR = seal(/<%[\w\W]*|[\w\W]*%>/gm); 216 const TMPLIT_EXPR = seal(/\$\{[\w\W]*/gm); // eslint-disable-line unicorn/better-regex 217 const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); // eslint-disable-line no-useless-escape 218 const ARIA_ATTR = seal(/^aria-[\-\w]+$/); // eslint-disable-line no-useless-escape
vendor: 7,928 bytes, lines 219-426
219 const IS_ALLOWED_URI = seal(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i // eslint-disable-line no-useless-escape 220 ); 221 const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); 222 const ATTR_WHITESPACE = seal(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g // eslint-disable-line no-control-regex 223 ); 224 const DOCTYPE_NAME = seal(/^html$/i); 225 const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); 226 227 var EXPRESSIONS = /*#__PURE__*/Object.freeze({ 228 __proto__: null, 229 ARIA_ATTR: ARIA_ATTR, 230 ATTR_WHITESPACE: ATTR_WHITESPACE, 231 CUSTOM_ELEMENT: CUSTOM_ELEMENT, 232 DATA_ATTR: DATA_ATTR, 233 DOCTYPE_NAME: DOCTYPE_NAME, 234 ERB_EXPR: ERB_EXPR, 235 IS_ALLOWED_URI: IS_ALLOWED_URI, 236 IS_SCRIPT_OR_DATA: IS_SCRIPT_OR_DATA, 237 MUSTACHE_EXPR: MUSTACHE_EXPR, 238 TMPLIT_EXPR: TMPLIT_EXPR 239 }); 240 241 /* eslint-disable @typescript-eslint/indent */ 242 // https://developer.mozilla.org/en-US/docs/Web/API/Node/nodeType 243 const NODE_TYPE = { 244 element: 1, 245 attribute: 2, 246 text: 3, 247 cdataSection: 4, 248 entityReference: 5, 249 // Deprecated 250 entityNode: 6, 251 // Deprecated 252 progressingInstruction: 7, 253 comment: 8, 254 document: 9, 255 documentType: 10, 256 documentFragment: 11, 257 notation: 12 // Deprecated 258 }; 259 const getGlobal = function getGlobal() { 260 return typeof window === 'undefined' ? null : window; 261 }; 262 /** 263 * Creates a no-op policy for internal use only. 264 * Don't export this function outside this module! 265 * @param trustedTypes The policy factory. 266 * @param purifyHostElement The Script element used to load DOMPurify (to determine policy name suffix). 267 * @return The policy created (or null, if Trusted Types 268 * are not supported or creating the policy failed). 269 */ 270 const _createTrustedTypesPolicy = function _createTrustedTypesPolicy(trustedTypes, purifyHostElement) { 271 if (typeof trustedTypes !== 'object' || typeof trustedTypes.createPolicy !== 'function') { 272 return null; 273 } 274 // Allow the callers to control the unique policy name 275 // by adding a data-tt-policy-suffix to the script element with the DOMPurify. 276 // Policy creation with duplicate names throws in Trusted Types. 277 let suffix = null; 278 const ATTR_NAME = 'data-tt-policy-suffix'; 279 if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { 280 suffix = purifyHostElement.getAttribute(ATTR_NAME); 281 } 282 const policyName = 'dompurify' + (suffix ? '#' + suffix : ''); 283 try { 284 return trustedTypes.createPolicy(policyName, { 285 createHTML(html) { 286 return html; 287 }, 288 createScriptURL(scriptUrl) { 289 return scriptUrl; 290 } 291 }); 292 } catch (_) { 293 // Policy creation failed (most likely another DOMPurify script has 294 // already run). Skip creating the policy, as this will only cause errors 295 // if TT are enforced. 296 console.warn('TrustedTypes policy ' + policyName + ' could not be created.'); 297 return null; 298 } 299 }; 300 const _createHooksMap = function _createHooksMap() { 301 return { 302 afterSanitizeAttributes: [], 303 afterSanitizeElements: [], 304 afterSanitizeShadowDOM: [], 305 beforeSanitizeAttributes: [], 306 beforeSanitizeElements: [], 307 beforeSanitizeShadowDOM: [], 308 uponSanitizeAttribute: [], 309 uponSanitizeElement: [], 310 uponSanitizeShadowNode: [] 311 }; 312 }; 313 function createDOMPurify() { 314 let window = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : getGlobal(); 315 const DOMPurify = root => createDOMPurify(root); 316 DOMPurify.version = '3.2.6'; 317 DOMPurify.removed = []; 318 if (!window || !window.document || window.document.nodeType !== NODE_TYPE.document || !window.Element) { 319 // Not running in a browser, provide a factory function 320 // so that you can pass your own Window 321 DOMPurify.isSupported = false; 322 return DOMPurify; 323 } 324 let { 325 document 326 } = window; 327 const originalDocument = document; 328 const currentScript = originalDocument.currentScript; 329 const { 330 DocumentFragment, 331 HTMLTemplateElement, 332 Node, 333 Element, 334 NodeFilter, 335 NamedNodeMap = window.NamedNodeMap || window.MozNamedAttrMap, 336 HTMLFormElement, 337 DOMParser, 338 trustedTypes 339 } = window; 340 const ElementPrototype = Element.prototype; 341 const cloneNode = lookupGetter(ElementPrototype, 'cloneNode'); 342 const remove = lookupGetter(ElementPrototype, 'remove'); 343 const getNextSibling = lookupGetter(ElementPrototype, 'nextSibling'); 344 const getChildNodes = lookupGetter(ElementPrototype, 'childNodes'); 345 const getParentNode = lookupGetter(ElementPrototype, 'parentNode'); 346 // As per issue #47, the web-components registry is inherited by a 347 // new document created via createHTMLDocument. As per the spec 348 // (http://w3c.github.io/webcomponents/spec/custom/#creating-and-passing-registries) 349 // a new empty registry is used when creating a template contents owner 350 // document, so we use that as our parent document to ensure nothing 351 // is inherited. 352 if (typeof HTMLTemplateElement === 'function') { 353 const template = document.createElement('template'); 354 if (template.content && template.content.ownerDocument) { 355 document = template.content.ownerDocument; 356 } 357 } 358 let trustedTypesPolicy; 359 let emptyHTML = ''; 360 const { 361 implementation, 362 createNodeIterator, 363 createDocumentFragment, 364 getElementsByTagName 365 } = document; 366 const { 367 importNode 368 } = originalDocument; 369 let hooks = _createHooksMap(); 370 /** 371 * Expose whether this browser supports running the full DOMPurify. 372 */ 373 DOMPurify.isSupported = typeof entries === 'function' && typeof getParentNode === 'function' && implementation && implementation.createHTMLDocument !== undefined; 374 const { 375 MUSTACHE_EXPR, 376 ERB_EXPR, 377 TMPLIT_EXPR, 378 DATA_ATTR, 379 ARIA_ATTR, 380 IS_SCRIPT_OR_DATA, 381 ATTR_WHITESPACE, 382 CUSTOM_ELEMENT 383 } = EXPRESSIONS; 384 let { 385 IS_ALLOWED_URI: IS_ALLOWED_URI$1 386 } = EXPRESSIONS; 387 /** 388 * We consider the elements and attributes below to be safe. Ideally 389 * don't add any new ones but feel free to remove unwanted ones. 390 */ 391 /* allowed element names */ 392 let ALLOWED_TAGS = null; 393 const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); 394 /* Allowed attribute names */ 395 let ALLOWED_ATTR = null; 396 const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); 397 /* 398 * Configure how DOMPurify should handle custom elements and their attributes as well as customized built-in elements. 399 * @property {RegExp|Function|null} tagNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any custom elements) 400 * @property {RegExp|Function|null} attributeNameCheck one of [null, regexPattern, predicate]. Default: `null` (disallow any attributes not on the allow list) 401 * @property {boolean} allowCustomizedBuiltInElements allow custom elements derived from built-ins if they pass CUSTOM_ELEMENT_HANDLING.tagNameCheck. Default: `false`. 402 */ 403 let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { 404 tagNameCheck: { 405 writable: true, 406 configurable: false, 407 enumerable: true, 408 value: null 409 }, 410 attributeNameCheck: { 411 writable: true, 412 configurable: false, 413 enumerable: true, 414 value: null 415 }, 416 allowCustomizedBuiltInElements: { 417 writable: true, 418 configurable: false, 419 enumerable: true, 420 value: false 421 } 422 })); 423 /* Explicitly forbidden tags (overrides ALLOWED_TAGS/ADD_TAGS) */ 424 let FORBID_TAGS = null; 425 /* Explicitly forbidden attributes (overrides ALLOWED_ATTR/ADD_ATTR) */ 426 let FORBID_ATTR = null
vendor: 9,205 bytes, lines 426-577
426; 427 /* Decide if ARIA attributes are okay */ 428 let ALLOW_ARIA_ATTR = true; 429 /* Decide if custom data attributes are okay */ 430 let ALLOW_DATA_ATTR = true; 431 /* Decide if unknown protocols are okay */ 432 let ALLOW_UNKNOWN_PROTOCOLS = false; 433 /* Decide if self-closing tags in attributes are allowed. 434 * Usually removed due to a mXSS issue in jQuery 3.0 */ 435 let ALLOW_SELF_CLOSE_IN_ATTR = true; 436 /* Output should be safe for common template engines. 437 * This means, DOMPurify removes data attributes, mustaches and ERB 438 */ 439 let SAFE_FOR_TEMPLATES = false; 440 /* Output should be safe even for XML used within HTML and alike. 441 * This means, DOMPurify removes comments when containing risky content. 442 */ 443 let SAFE_FOR_XML = true; 444 /* Decide if document with <html>... should be returned */ 445 let WHOLE_DOCUMENT = false; 446 /* Track whether config is already set on this instance of DOMPurify. */ 447 let SET_CONFIG = false; 448 /* Decide if all elements (e.g. style, script) must be children of 449 * document.body. By default, browsers might move them to document.head */ 450 let FORCE_BODY = false; 451 /* Decide if a DOM `HTMLBodyElement` should be returned, instead of a html 452 * string (or a TrustedHTML object if Trusted Types are supported). 453 * If `WHOLE_DOCUMENT` is enabled a `HTMLHtmlElement` will be returned instead 454 */ 455 let RETURN_DOM = false; 456 /* Decide if a DOM `DocumentFragment` should be returned, instead of a html 457 * string (or a TrustedHTML object if Trusted Types are supported) */ 458 let RETURN_DOM_FRAGMENT = false; 459 /* Try to return a Trusted Type object instead of a string, return a string in 460 * case Trusted Types are not supported */ 461 let RETURN_TRUSTED_TYPE = false; 462 /* Output should be free from DOM clobbering attacks? 463 * This sanitizes markups named with colliding, clobberable built-in DOM APIs. 464 */ 465 let SANITIZE_DOM = true; 466 /* Achieve full DOM Clobbering protection by isolating the namespace of named 467 * properties and JS variables, mitigating attacks that abuse the HTML/DOM spec rules. 468 * 469 * HTML/DOM spec rules that enable DOM Clobbering: 470 * - Named Access on Window (§7.3.3) 471 * - DOM Tree Accessors (§3.1.5) 472 * - Form Element Parent-Child Relations (§4.10.3) 473 * - Iframe srcdoc / Nested WindowProxies (§4.8.5) 474 * - HTMLCollection (§4.2.10.2) 475 * 476 * Namespace isolation is implemented by prefixing `id` and `name` attributes 477 * with a constant string, i.e., `user-content-` 478 */ 479 let SANITIZE_NAMED_PROPS = false; 480 const SANITIZE_NAMED_PROPS_PREFIX = 'user-content-'; 481 /* Keep element content when removing element? */ 482 let KEEP_CONTENT = true; 483 /* If a `Node` is passed to sanitize(), then performs sanitization in-place instead 484 * of importing it into a new Document and returning a sanitized copy */ 485 let IN_PLACE = false; 486 /* Allow usage of profiles like html, svg and mathMl */ 487 let USE_PROFILES = {}; 488 /* Tags to ignore content of when KEEP_CONTENT is true */ 489 let FORBID_CONTENTS = null; 490 const DEFAULT_FORBID_CONTENTS = addToSet({}, ['annotation-xml', 'audio', 'colgroup', 'desc', 'foreignobject', 'head', 'iframe', 'math', 'mi', 'mn', 'mo', 'ms', 'mtext', 'noembed', 'noframes', 'noscript', 'plaintext', 'script', 'style', 'svg', 'template', 'thead', 'title', 'video', 'xmp']); 491 /* Tags that are safe for data: URIs */ 492 let DATA_URI_TAGS = null; 493 const DEFAULT_DATA_URI_TAGS = addToSet({}, ['audio', 'video', 'img', 'source', 'image', 'track']); 494 /* Attributes safe for values like "javascript:" */ 495 let URI_SAFE_ATTRIBUTES = null; 496 const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ['alt', 'class', 'for', 'id', 'label', 'name', 'pattern', 'placeholder', 'role', 'summary', 'title', 'value', 'style', 'xmlns']); 497 const MATHML_NAMESPACE = 'http://www.w3.org/1998/Math/MathML'; 498 const SVG_NAMESPACE = 'http://www.w3.org/2000/svg'; 499 const HTML_NAMESPACE = 'http://www.w3.org/1999/xhtml'; 500 /* Document namespace */ 501 let NAMESPACE = HTML_NAMESPACE; 502 let IS_EMPTY_INPUT = false; 503 /* Allowed XHTML+XML namespaces */ 504 let ALLOWED_NAMESPACES = null; 505 const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); 506 let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ['mi', 'mo', 'mn', 'ms', 'mtext']); 507 let HTML_INTEGRATION_POINTS = addToSet({}, ['annotation-xml']); 508 // Certain elements are allowed in both SVG and HTML 509 // namespace. We need to specify them explicitly 510 // so that they don't get erroneously deleted from 511 // HTML namespace. 512 const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ['title', 'style', 'font', 'a', 'script']); 513 /* Parsing of strict XHTML documents */ 514 let PARSER_MEDIA_TYPE = null; 515 const SUPPORTED_PARSER_MEDIA_TYPES = ['application/xhtml+xml', 'text/html']; 516 const DEFAULT_PARSER_MEDIA_TYPE = 'text/html'; 517 let transformCaseFunc = null; 518 /* Keep a reference to config to pass to hooks */ 519 let CONFIG = null; 520 /* Ideally, do not touch anything below this line */ 521 /* ______________________________________________ */ 522 const formElement = document.createElement('form'); 523 const isRegexOrFunction = function isRegexOrFunction(testValue) { 524 return testValue instanceof RegExp || testValue instanceof Function; 525 }; 526 /** 527 * _parseConfig 528 * 529 * @param cfg optional config literal 530 */ 531 // eslint-disable-next-line complexity 532 const _parseConfig = function _parseConfig() { 533 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 534 if (CONFIG && CONFIG === cfg) { 535 return; 536 } 537 /* Shield configuration object from tampering */ 538 if (!cfg || typeof cfg !== 'object') { 539 cfg = {}; 540 } 541 /* Shield configuration object from prototype pollution */ 542 cfg = clone(cfg); 543 PARSER_MEDIA_TYPE = 544 // eslint-disable-next-line unicorn/prefer-includes 545 SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; 546 // HTML tags and attributes are not case-sensitive, converting to lowercase. Keeping XHTML as is. 547 transformCaseFunc = PARSER_MEDIA_TYPE === 'application/xhtml+xml' ? stringToString : stringToLowerCase; 548 /* Set configuration parameters */ 549 ALLOWED_TAGS = objectHasOwnProperty(cfg, 'ALLOWED_TAGS') ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; 550 ALLOWED_ATTR = objectHasOwnProperty(cfg, 'ALLOWED_ATTR') ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; 551 ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, 'ALLOWED_NAMESPACES') ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; 552 URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, 'ADD_URI_SAFE_ATTR') ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES; 553 DATA_URI_TAGS = objectHasOwnProperty(cfg, 'ADD_DATA_URI_TAGS') ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS; 554 FORBID_CONTENTS = objectHasOwnProperty(cfg, 'FORBID_CONTENTS') ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; 555 FORBID_TAGS = objectHasOwnProperty(cfg, 'FORBID_TAGS') ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({}); 556 FORBID_ATTR = objectHasOwnProperty(cfg, 'FORBID_ATTR') ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({}); 557 USE_PROFILES = objectHasOwnProperty(cfg, 'USE_PROFILES') ? cfg.USE_PROFILES : false; 558 ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; // Default true 559 ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; // Default true 560 ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; // Default false 561 ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; // Default true 562 SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; // Default false 563 SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; // Default true 564 WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; // Default false 565 RETURN_DOM = cfg.RETURN_DOM || false; // Default false 566 RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; // Default false 567 RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; // Default false 568 FORCE_BODY = cfg.FORCE_BODY || false; // Default false 569 SANITIZE_DOM = cfg.SANITIZE_DOM !== false; // Default true 570 SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; // Default false 571 KEEP_CONTENT = cfg.KEEP_CONTENT !== false; // Default true 572 IN_PLACE = cfg.IN_PLACE || false; // Default false 573 IS_ALLOWED_URI$1 = cfg.ALLOWED_URI_REGEXP || IS_ALLOWED_URI; 574 NAMESPACE = cfg.NAMESPACE || HTML_NAMESPACE; 575 MATHML_TEXT_INTEGRATION_POINTS = cfg.MATHML_TEXT_INTEGRATION_POINTS || MATHML_TEXT_INTEGRATION_POINTS; 576 HTML_INTEGRATION_POINTS = cfg.HTML_INTEGRATION_POINTS || HTML_INTEGRATION_POINTS; 577 CUSTOM_ELEMENT_HANDLING = cfg.CUSTOM_ELEMENT_HANDLING ||
vendor: 1,622 bytes, lines 577-616
577{}; 578 if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck)) { 579 CUSTOM_ELEMENT_HANDLING.tagNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.tagNameCheck; 580 } 581 if (cfg.CUSTOM_ELEMENT_HANDLING && isRegexOrFunction(cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)) { 582 CUSTOM_ELEMENT_HANDLING.attributeNameCheck = cfg.CUSTOM_ELEMENT_HANDLING.attributeNameCheck; 583 } 584 if (cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements === 'boolean') { 585 CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = cfg.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements; 586 } 587 if (SAFE_FOR_TEMPLATES) { 588 ALLOW_DATA_ATTR = false; 589 } 590 if (RETURN_DOM_FRAGMENT) { 591 RETURN_DOM = true; 592 } 593 /* Parse profile info */ 594 if (USE_PROFILES) { 595 ALLOWED_TAGS = addToSet({}, text); 596 ALLOWED_ATTR = []; 597 if (USE_PROFILES.html === true) { 598 addToSet(ALLOWED_TAGS, html$1); 599 addToSet(ALLOWED_ATTR, html); 600 } 601 if (USE_PROFILES.svg === true) { 602 addToSet(ALLOWED_TAGS, svg$1); 603 addToSet(ALLOWED_ATTR, svg); 604 addToSet(ALLOWED_ATTR, xml); 605 } 606 if (USE_PROFILES.svgFilters === true) { 607 addToSet(ALLOWED_TAGS, svgFilters); 608 addToSet(ALLOWED_ATTR, svg); 609 addToSet(ALLOWED_ATTR, xml); 610 } 611 if (USE_PROFILES.mathMl === true) { 612 addToSet(ALLOWED_TAGS, mathMl$1); 613 addToSet(ALLOWED_ATTR, mathMl); 614 addToSet(ALLOWED_ATTR, xml); 615 } 616 }
vendor: 5,664 bytes, lines 617-744
617 /* Merge configuration parameters */ 618 if (cfg.ADD_TAGS) { 619 if (ALLOWED_TAGS === DEFAULT_ALLOWED_TAGS) { 620 ALLOWED_TAGS = clone(ALLOWED_TAGS); 621 } 622 addToSet(ALLOWED_TAGS, cfg.ADD_TAGS, transformCaseFunc); 623 } 624 if (cfg.ADD_ATTR) { 625 if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { 626 ALLOWED_ATTR = clone(ALLOWED_ATTR); 627 } 628 addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); 629 } 630 if (cfg.ADD_URI_SAFE_ATTR) { 631 addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); 632 } 633 if (cfg.FORBID_CONTENTS) { 634 if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { 635 FORBID_CONTENTS = clone(FORBID_CONTENTS); 636 } 637 addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); 638 } 639 /* Add #text in case KEEP_CONTENT is set to true */ 640 if (KEEP_CONTENT) { 641 ALLOWED_TAGS['#text'] = true; 642 } 643 /* Add html, head and body to ALLOWED_TAGS in case WHOLE_DOCUMENT is true */ 644 if (WHOLE_DOCUMENT) { 645 addToSet(ALLOWED_TAGS, ['html', 'head', 'body']); 646 } 647 /* Add tbody to ALLOWED_TAGS in case tables are permitted, see #286, #365 */ 648 if (ALLOWED_TAGS.table) { 649 addToSet(ALLOWED_TAGS, ['tbody']); 650 delete FORBID_TAGS.tbody; 651 } 652 if (cfg.TRUSTED_TYPES_POLICY) { 653 if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== 'function') { 654 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); 655 } 656 if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== 'function') { 657 throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); 658 } 659 // Overwrite existing TrustedTypes policy. 660 trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; 661 // Sign local variables required by `sanitize`. 662 emptyHTML = trustedTypesPolicy.createHTML(''); 663 } else { 664 // Uninitialized policy, attempt to initialize the internal dompurify policy. 665 if (trustedTypesPolicy === undefined) { 666 trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); 667 } 668 // If creating the internal policy succeeded sign internal variables. 669 if (trustedTypesPolicy !== null && typeof emptyHTML === 'string') { 670 emptyHTML = trustedTypesPolicy.createHTML(''); 671 } 672 } 673 // Prevent further manipulation of configuration. 674 // Not available in IE8, Safari 5, etc. 675 if (freeze) { 676 freeze(cfg); 677 } 678 CONFIG = cfg; 679 }; 680 /* Keep track of all possible SVG and MathML tags 681 * so that we can perform the namespace checks 682 * correctly. */ 683 const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); 684 const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); 685 /** 686 * @param element a DOM element whose namespace is being checked 687 * @returns Return false if the element has a 688 * namespace that a spec-compliant parser would never 689 * return. Return true otherwise. 690 */ 691 const _checkValidNamespace = function _checkValidNamespace(element) { 692 let parent = getParentNode(element); 693 // In JSDOM, if we're inside shadow DOM, then parentNode 694 // can be null. We just simulate parent in this case. 695 if (!parent || !parent.tagName) { 696 parent = { 697 namespaceURI: NAMESPACE, 698 tagName: 'template' 699 }; 700 } 701 const tagName = stringToLowerCase(element.tagName); 702 const parentTagName = stringToLowerCase(parent.tagName); 703 if (!ALLOWED_NAMESPACES[element.namespaceURI]) { 704 return false; 705 } 706 if (element.namespaceURI === SVG_NAMESPACE) { 707 // The only way to switch from HTML namespace to SVG 708 // is via <svg>. If it happens via any other tag, then 709 // it should be killed. 710 if (parent.namespaceURI === HTML_NAMESPACE) { 711 return tagName === 'svg'; 712 } 713 // The only way to switch from MathML to SVG is via` 714 // svg if parent is either <annotation-xml> or MathML 715 // text integration points. 716 if (parent.namespaceURI === MATHML_NAMESPACE) { 717 return tagName === 'svg' && (parentTagName === 'annotation-xml' || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); 718 } 719 // We only allow elements that are defined in SVG 720 // spec. All others are disallowed in SVG namespace. 721 return Boolean(ALL_SVG_TAGS[tagName]); 722 } 723 if (element.namespaceURI === MATHML_NAMESPACE) { 724 // The only way to switch from HTML namespace to MathML 725 // is via <math>. If it happens via any other tag, then 726 // it should be killed. 727 if (parent.namespaceURI === HTML_NAMESPACE) { 728 return tagName === 'math'; 729 } 730 // The only way to switch from SVG to MathML is via 731 // <math> and HTML integration points 732 if (parent.namespaceURI === SVG_NAMESPACE) { 733 return tagName === 'math' && HTML_INTEGRATION_POINTS[parentTagName]; 734 } 735 // We only allow elements that are defined in MathML 736 // spec. All others are disallowed in MathML namespace. 737 return Boolean(ALL_MATHML_TAGS[tagName]); 738 } 739 if (element.namespaceURI === HTML_NAMESPACE) { 740 // The only way to switch from SVG to HTML is via 741 // HTML integration points, and from MathML to HTML 742 // is via MathML text integration points 743 if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { 744 return false;
745 } 746 if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { 747 return false; 748 } 749 // We disallow tags that are specific for MathML 750 // or SVG and should never appear in HTML namespace 751 return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); 752 } 753 // For XHTML and XML documents that support custom namespaces 754 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && ALLOWED_NAMESPACES[element.namespaceURI]) { 755 return true; 756 } 757 // The code should never reach this place (this means 758 // that the element somehow got namespace that is not 759 // HTML, SVG, MathML or allowed via ALLOWED_NAMESPACES). 760 // Return false just in case. 761 return false; 762 }; 763 /** 764 * _forceRemove 765 * 766 * @param node a DOM node 767 */ 768 const _forceRemove = function _forceRemove(node) { 769 arrayPush(DOMPurify.removed, { 770 element: node 771 }); 772 try {
vendor: 6,975 bytes, lines 773-940
773 // eslint-disable-next-line unicorn/prefer-dom-node-remove 774 getParentNode(node).removeChild(node); 775 } catch (_) { 776 remove(node); 777 } 778 }; 779 /** 780 * _removeAttribute 781 * 782 * @param name an Attribute name 783 * @param element a DOM node 784 */ 785 const _removeAttribute = function _removeAttribute(name, element) { 786 try { 787 arrayPush(DOMPurify.removed, { 788 attribute: element.getAttributeNode(name), 789 from: element 790 }); 791 } catch (_) { 792 arrayPush(DOMPurify.removed, { 793 attribute: null, 794 from: element 795 }); 796 } 797 element.removeAttribute(name); 798 // We void attribute values for unremovable "is" attributes 799 if (name === 'is') { 800 if (RETURN_DOM || RETURN_DOM_FRAGMENT) { 801 try { 802 _forceRemove(element); 803 } catch (_) {} 804 } else { 805 try { 806 element.setAttribute(name, ''); 807 } catch (_) {} 808 } 809 } 810 }; 811 /** 812 * _initDocument 813 * 814 * @param dirty - a string of dirty markup 815 * @return a DOM, filled with the dirty markup 816 */ 817 const _initDocument = function _initDocument(dirty) { 818 /* Create a HTML document */ 819 let doc = null; 820 let leadingWhitespace = null; 821 if (FORCE_BODY) { 822 dirty = '<remove></remove>' + dirty; 823 } else { 824 /* If FORCE_BODY isn't used, leading whitespace needs to be preserved manually */ 825 const matches = stringMatch(dirty, /^[\r\n\t ]+/); 826 leadingWhitespace = matches && matches[0]; 827 } 828 if (PARSER_MEDIA_TYPE === 'application/xhtml+xml' && NAMESPACE === HTML_NAMESPACE) { 829 // Root of XHTML doc must contain xmlns declaration (see https://www.w3.org/TR/xhtml1/normative.html#strict) 830 dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + '</body></html>'; 831 } 832 const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; 833 /* 834 * Use the DOMParser API by default, fallback later if needs be 835 * DOMParser not work for svg when has multiple root element. 836 */ 837 if (NAMESPACE === HTML_NAMESPACE) { 838 try { 839 doc = new DOMParser().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); 840 } catch (_) {} 841 } 842 /* Use createHTMLDocument in case DOMParser is not available */ 843 if (!doc || !doc.documentElement) { 844 doc = implementation.createDocument(NAMESPACE, 'template', null); 845 try { 846 doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; 847 } catch (_) { 848 // Syntax error if dirtyPayload is invalid xml 849 } 850 } 851 const body = doc.body || doc.documentElement; 852 if (dirty && leadingWhitespace) { 853 body.insertBefore(document.createTextNode(leadingWhitespace), body.childNodes[0] || null); 854 } 855 /* Work on whole document or just its body */ 856 if (NAMESPACE === HTML_NAMESPACE) { 857 return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? 'html' : 'body')[0]; 858 } 859 return WHOLE_DOCUMENT ? doc.documentElement : body; 860 }; 861 /** 862 * Creates a NodeIterator object that you can use to traverse filtered lists of nodes or elements in a document. 863 * 864 * @param root The root element or node to start traversing on. 865 * @return The created NodeIterator 866 */ 867 const _createNodeIterator = function _createNodeIterator(root) { 868 return createNodeIterator.call(root.ownerDocument || root, root, 869 // eslint-disable-next-line no-bitwise 870 NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_COMMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_PROCESSING_INSTRUCTION | NodeFilter.SHOW_CDATA_SECTION, null); 871 }; 872 /** 873 * _isClobbered 874 * 875 * @param element element to check for clobbering attacks 876 * @return true if clobbered, false if safe 877 */ 878 const _isClobbered = function _isClobbered(element) { 879 return element instanceof HTMLFormElement && (typeof element.nodeName !== 'string' || typeof element.textContent !== 'string' || typeof element.removeChild !== 'function' || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== 'function' || typeof element.setAttribute !== 'function' || typeof element.namespaceURI !== 'string' || typeof element.insertBefore !== 'function' || typeof element.hasChildNodes !== 'function'); 880 }; 881 /** 882 * Checks whether the given object is a DOM node. 883 * 884 * @param value object to check whether it's a DOM node 885 * @return true is object is a DOM node 886 */ 887 const _isNode = function _isNode(value) { 888 return typeof Node === 'function' && value instanceof Node; 889 }; 890 function _executeHooks(hooks, currentNode, data) { 891 arrayForEach(hooks, hook => { 892 hook.call(DOMPurify, currentNode, data, CONFIG); 893 }); 894 } 895 /** 896 * _sanitizeElements 897 * 898 * @protect nodeName 899 * @protect textContent 900 * @protect removeChild 901 * @param currentNode to check for permission to exist 902 * @return true if node was killed, false if left alive 903 */ 904 const _sanitizeElements = function _sanitizeElements(currentNode) { 905 let content = null; 906 /* Execute a hook if present */ 907 _executeHooks(hooks.beforeSanitizeElements, currentNode, null); 908 /* Check if element is clobbered or can clobber */ 909 if (_isClobbered(currentNode)) { 910 _forceRemove(currentNode); 911 return true; 912 } 913 /* Now let's check the element's type and name */ 914 const tagName = transformCaseFunc(currentNode.nodeName); 915 /* Execute a hook if present */ 916 _executeHooks(hooks.uponSanitizeElement, currentNode, { 917 tagName, 918 allowedTags: ALLOWED_TAGS 919 }); 920 /* Detect mXSS attempts abusing namespace confusion */ 921 if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) { 922 _forceRemove(currentNode); 923 return true; 924 } 925 /* Remove any occurrence of processing instructions */ 926 if (currentNode.nodeType === NODE_TYPE.progressingInstruction) { 927 _forceRemove(currentNode); 928 return true; 929 } 930 /* Remove any kind of possibly harmful comments */ 931 if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) { 932 _forceRemove(currentNode); 933 return true; 934 } 935 /* Remove element if anything forbids its presence */ 936 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { 937 /* Check if we have a custom element to handle */ 938 if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { 939 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { 940 return false;
vendor: 4,716 bytes, lines 941-1017
941 } 942 if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { 943 return false; 944 } 945 } 946 /* Keep content except for bad-listed elements */ 947 if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { 948 const parentNode = getParentNode(currentNode) || currentNode.parentNode; 949 const childNodes = getChildNodes(currentNode) || currentNode.childNodes; 950 if (childNodes && parentNode) { 951 const childCount = childNodes.length; 952 for (let i = childCount - 1; i >= 0; --i) { 953 const childClone = cloneNode(childNodes[i], true); 954 childClone.__removalCount = (currentNode.__removalCount || 0) + 1; 955 parentNode.insertBefore(childClone, getNextSibling(currentNode)); 956 } 957 } 958 } 959 _forceRemove(currentNode); 960 return true; 961 } 962 /* Check whether element has a valid namespace */ 963 if (currentNode instanceof Element && !_checkValidNamespace(currentNode)) { 964 _forceRemove(currentNode); 965 return true; 966 } 967 /* Make sure that older browsers don't get fallback-tag mXSS */ 968 if ((tagName === 'noscript' || tagName === 'noembed' || tagName === 'noframes') && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { 969 _forceRemove(currentNode); 970 return true; 971 } 972 /* Sanitize element content to be template-safe */ 973 if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { 974 /* Get the element's text content */ 975 content = currentNode.textContent; 976 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { 977 content = stringReplace(content, expr, ' '); 978 }); 979 if (currentNode.textContent !== content) { 980 arrayPush(DOMPurify.removed, { 981 element: currentNode.cloneNode() 982 }); 983 currentNode.textContent = content; 984 } 985 } 986 /* Execute a hook if present */ 987 _executeHooks(hooks.afterSanitizeElements, currentNode, null); 988 return false; 989 }; 990 /** 991 * _isValidAttribute 992 * 993 * @param lcTag Lowercase tag name of containing element. 994 * @param lcName Lowercase attribute name. 995 * @param value Attribute value. 996 * @return Returns true if `value` is valid, otherwise false. 997 */ 998 // eslint-disable-next-line complexity 999 const _isValidAttribute = function _isValidAttribute(lcTag, lcName, value) { 1000 /* Make sure attribute cannot clobber */ 1001 if (SANITIZE_DOM && (lcName === 'id' || lcName === 'name') && (value in document || value in formElement)) { 1002 return false; 1003 } 1004 /* Allow valid data-* attributes: At least one character after "-" 1005 (https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes) 1006 XML-compatible (https://html.spec.whatwg.org/multipage/infrastructure.html#xml-compatible and http://www.w3.org/TR/xml/#d0e804) 1007 We don't need to check the value; it's always URI safe. */ 1008 if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR, lcName)) ; else if (!ALLOWED_ATTR[lcName] || FORBID_ATTR[lcName]) { 1009 if ( 1010 // First condition does a very basic check if a) it's basically a valid custom element tagname AND 1011 // b) if the tagName passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck 1012 // and c) if the attribute name passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.attributeNameCheck 1013 _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName)) || 1014 // Alternative, second condition checks if it's an `is`-attribute, AND 1015 // the value passes whatever the user has configured for CUSTOM_ELEMENT_HANDLING.tagNameCheck 1016 lcName === 'is' && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value))) ; else { 1017 return false;
vendor: 15,033 bytes, lines 1018-1395
1018 } 1019 /* Check value is safe. First, is attr inert? If so, is safe */ 1020 } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if ((lcName === 'src' || lcName === 'xlink:href' || lcName === 'href') && lcTag !== 'script' && stringIndexOf(value, 'data:') === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA, stringReplace(value, ATTR_WHITESPACE, ''))) ; else if (value) { 1021 return false; 1022 } else ; 1023 return true; 1024 }; 1025 /** 1026 * _isBasicCustomElement 1027 * checks if at least one dash is included in tagName, and it's not the first char 1028 * for more sophisticated checking see https://github.com/sindresorhus/validate-element-name 1029 * 1030 * @param tagName name of the tag of the node to sanitize 1031 * @returns Returns true if the tag name meets the basic criteria for a custom element, otherwise false. 1032 */ 1033 const _isBasicCustomElement = function _isBasicCustomElement(tagName) { 1034 return tagName !== 'annotation-xml' && stringMatch(tagName, CUSTOM_ELEMENT); 1035 }; 1036 /** 1037 * _sanitizeAttributes 1038 * 1039 * @protect attributes 1040 * @protect nodeName 1041 * @protect removeAttribute 1042 * @protect setAttribute 1043 * 1044 * @param currentNode to sanitize 1045 */ 1046 const _sanitizeAttributes = function _sanitizeAttributes(currentNode) { 1047 /* Execute a hook if present */ 1048 _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); 1049 const { 1050 attributes 1051 } = currentNode; 1052 /* Check if we have attributes; if not we might have a text node */ 1053 if (!attributes || _isClobbered(currentNode)) { 1054 return; 1055 } 1056 const hookEvent = { 1057 attrName: '', 1058 attrValue: '', 1059 keepAttr: true, 1060 allowedAttributes: ALLOWED_ATTR, 1061 forceKeepAttr: undefined 1062 }; 1063 let l = attributes.length; 1064 /* Go backwards over all attributes; safely remove bad ones */ 1065 while (l--) { 1066 const attr = attributes[l]; 1067 const { 1068 name, 1069 namespaceURI, 1070 value: attrValue 1071 } = attr; 1072 const lcName = transformCaseFunc(name); 1073 const initValue = attrValue; 1074 let value = name === 'value' ? initValue : stringTrim(initValue); 1075 /* Execute a hook if present */ 1076 hookEvent.attrName = lcName; 1077 hookEvent.attrValue = value; 1078 hookEvent.keepAttr = true; 1079 hookEvent.forceKeepAttr = undefined; // Allows developers to see this is a property they can set 1080 _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); 1081 value = hookEvent.attrValue; 1082 /* Full DOM Clobbering protection via namespace isolation, 1083 * Prefix id and name attributes with `user-content-` 1084 */ 1085 if (SANITIZE_NAMED_PROPS && (lcName === 'id' || lcName === 'name')) { 1086 // Remove the attribute with this value 1087 _removeAttribute(name, currentNode); 1088 // Prefix the value and later re-create the attribute with the sanitized value 1089 value = SANITIZE_NAMED_PROPS_PREFIX + value; 1090 } 1091 /* Work around a security issue with comments inside attributes */ 1092 if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|title)/i, value)) { 1093 _removeAttribute(name, currentNode); 1094 continue; 1095 } 1096 /* Did the hooks approve of the attribute? */ 1097 if (hookEvent.forceKeepAttr) { 1098 continue; 1099 } 1100 /* Did the hooks approve of the attribute? */ 1101 if (!hookEvent.keepAttr) { 1102 _removeAttribute(name, currentNode); 1103 continue; 1104 } 1105 /* Work around a security issue in jQuery 3.0 */ 1106 if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { 1107 _removeAttribute(name, currentNode); 1108 continue; 1109 } 1110 /* Sanitize attribute content to be template-safe */ 1111 if (SAFE_FOR_TEMPLATES) { 1112 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { 1113 value = stringReplace(value, expr, ' '); 1114 }); 1115 } 1116 /* Is `value` valid for this attribute? */ 1117 const lcTag = transformCaseFunc(currentNode.nodeName); 1118 if (!_isValidAttribute(lcTag, lcName, value)) { 1119 _removeAttribute(name, currentNode); 1120 continue; 1121 } 1122 /* Handle attributes that require Trusted Types */ 1123 if (trustedTypesPolicy && typeof trustedTypes === 'object' && typeof trustedTypes.getAttributeType === 'function') { 1124 if (namespaceURI) ; else { 1125 switch (trustedTypes.getAttributeType(lcTag, lcName)) { 1126 case 'TrustedHTML': 1127 { 1128 value = trustedTypesPolicy.createHTML(value); 1129 break; 1130 } 1131 case 'TrustedScriptURL': 1132 { 1133 value = trustedTypesPolicy.createScriptURL(value); 1134 break; 1135 } 1136 } 1137 } 1138 } 1139 /* Handle invalid data-* attribute set by try-catching it */ 1140 if (value !== initValue) { 1141 try { 1142 if (namespaceURI) { 1143 currentNode.setAttributeNS(namespaceURI, name, value); 1144 } else { 1145 /* Fallback to setAttribute() for browser-unrecognized namespaces e.g. "x-schema". */ 1146 currentNode.setAttribute(name, value); 1147 } 1148 if (_isClobbered(currentNode)) { 1149 _forceRemove(currentNode); 1150 } else { 1151 arrayPop(DOMPurify.removed); 1152 } 1153 } catch (_) { 1154 _removeAttribute(name, currentNode); 1155 } 1156 } 1157 } 1158 /* Execute a hook if present */ 1159 _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); 1160 }; 1161 /** 1162 * _sanitizeShadowDOM 1163 * 1164 * @param fragment to iterate over recursively 1165 */ 1166 const _sanitizeShadowDOM = function _sanitizeShadowDOM(fragment) { 1167 let shadowNode = null; 1168 const shadowIterator = _createNodeIterator(fragment); 1169 /* Execute a hook if present */ 1170 _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); 1171 while (shadowNode = shadowIterator.nextNode()) { 1172 /* Execute a hook if present */ 1173 _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); 1174 /* Sanitize tags and elements */ 1175 _sanitizeElements(shadowNode); 1176 /* Check attributes next */ 1177 _sanitizeAttributes(shadowNode); 1178 /* Deep shadow DOM detected */ 1179 if (shadowNode.content instanceof DocumentFragment) { 1180 _sanitizeShadowDOM(shadowNode.content); 1181 } 1182 } 1183 /* Execute a hook if present */ 1184 _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); 1185 }; 1186 // eslint-disable-next-line complexity 1187 DOMPurify.sanitize = function (dirty) { 1188 let cfg = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 1189 let body = null; 1190 let importedNode = null; 1191 let currentNode = null; 1192 let returnNode = null; 1193 /* Make sure we have a string to sanitize. 1194 DO NOT return early, as this will return the wrong type if 1195 the user has requested a DOM object rather than a string */ 1196 IS_EMPTY_INPUT = !dirty; 1197 if (IS_EMPTY_INPUT) { 1198 dirty = '<!-->'; 1199 } 1200 /* Stringify, in case dirty is an object */ 1201 if (typeof dirty !== 'string' && !_isNode(dirty)) { 1202 if (typeof dirty.toString === 'function') { 1203 dirty = dirty.toString(); 1204 if (typeof dirty !== 'string') { 1205 throw typeErrorCreate('dirty is not a string, aborting'); 1206 } 1207 } else { 1208 throw typeErrorCreate('toString is not a function'); 1209 } 1210 } 1211 /* Return dirty HTML if DOMPurify cannot run */ 1212 if (!DOMPurify.isSupported) { 1213 return dirty; 1214 } 1215 /* Assign config vars */ 1216 if (!SET_CONFIG) { 1217 _parseConfig(cfg); 1218 } 1219 /* Clean up removed elements */ 1220 DOMPurify.removed = []; 1221 /* Check if dirty is correctly typed for IN_PLACE */ 1222 if (typeof dirty === 'string') { 1223 IN_PLACE = false; 1224 } 1225 if (IN_PLACE) { 1226 /* Do some early pre-sanitization to avoid unsafe root nodes */ 1227 if (dirty.nodeName) { 1228 const tagName = transformCaseFunc(dirty.nodeName); 1229 if (!ALLOWED_TAGS[tagName] || FORBID_TAGS[tagName]) { 1230 throw typeErrorCreate('root node is forbidden and cannot be sanitized in-place'); 1231 } 1232 } 1233 } else if (dirty instanceof Node) { 1234 /* If dirty is a DOM element, append to an empty document to avoid 1235 elements being stripped by the parser */ 1236 body = _initDocument('<!---->'); 1237 importedNode = body.ownerDocument.importNode(dirty, true); 1238 if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === 'BODY') { 1239 /* Node is already a body, use as is */ 1240 body = importedNode; 1241 } else if (importedNode.nodeName === 'HTML') { 1242 body = importedNode; 1243 } else { 1244 // eslint-disable-next-line unicorn/prefer-dom-node-append 1245 body.appendChild(importedNode); 1246 } 1247 } else { 1248 /* Exit directly if we have nothing to do */ 1249 if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && 1250 // eslint-disable-next-line unicorn/prefer-includes 1251 dirty.indexOf('<') === -1) { 1252 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; 1253 } 1254 /* Initialize the document to work on */ 1255 body = _initDocument(dirty); 1256 /* Check we have a DOM node from the data */ 1257 if (!body) { 1258 return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ''; 1259 } 1260 } 1261 /* Remove first element node (ours) if FORCE_BODY is set */ 1262 if (body && FORCE_BODY) { 1263 _forceRemove(body.firstChild); 1264 } 1265 /* Get node iterator */ 1266 const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); 1267 /* Now start iterating over the created document */ 1268 while (currentNode = nodeIterator.nextNode()) { 1269 /* Sanitize tags and elements */ 1270 _sanitizeElements(currentNode); 1271 /* Check attributes next */ 1272 _sanitizeAttributes(currentNode); 1273 /* Shadow DOM detected, sanitize it */ 1274 if (currentNode.content instanceof DocumentFragment) { 1275 _sanitizeShadowDOM(currentNode.content); 1276 } 1277 } 1278 /* If we sanitized `dirty` in-place, return it. */ 1279 if (IN_PLACE) { 1280 return dirty; 1281 } 1282 /* Return sanitized string or DOM */ 1283 if (RETURN_DOM) { 1284 if (RETURN_DOM_FRAGMENT) { 1285 returnNode = createDocumentFragment.call(body.ownerDocument); 1286 while (body.firstChild) { 1287 // eslint-disable-next-line unicorn/prefer-dom-node-append 1288 returnNode.appendChild(body.firstChild); 1289 } 1290 } else { 1291 returnNode = body; 1292 } 1293 if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { 1294 /* 1295 AdoptNode() is not used because internal state is not reset 1296 (e.g. the past names map of a HTMLFormElement), this is safe 1297 in theory but we would rather not risk another attack vector. 1298 The state that is cloned by importNode() is explicitly defined 1299 by the specs. 1300 */ 1301 returnNode = importNode.call(originalDocument, returnNode, true); 1302 } 1303 return returnNode; 1304 } 1305 let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; 1306 /* Serialize doctype if allowed */ 1307 if (WHOLE_DOCUMENT && ALLOWED_TAGS['!doctype'] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { 1308 serializedHTML = '<!DOCTYPE ' + body.ownerDocument.doctype.name + '>\n' + serializedHTML; 1309 } 1310 /* Sanitize final string template-safe */ 1311 if (SAFE_FOR_TEMPLATES) { 1312 arrayForEach([MUSTACHE_EXPR, ERB_EXPR, TMPLIT_EXPR], expr => { 1313 serializedHTML = stringReplace(serializedHTML, expr, ' '); 1314 }); 1315 } 1316 return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; 1317 }; 1318 DOMPurify.setConfig = function () { 1319 let cfg = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 1320 _parseConfig(cfg); 1321 SET_CONFIG = true; 1322 }; 1323 DOMPurify.clearConfig = function () { 1324 CONFIG = null; 1325 SET_CONFIG = false; 1326 }; 1327 DOMPurify.isValidAttribute = function (tag, attr, value) { 1328 /* Initialize shared config vars if necessary. */ 1329 if (!CONFIG) { 1330 _parseConfig({}); 1331 } 1332 const lcTag = transformCaseFunc(tag); 1333 const lcName = transformCaseFunc(attr); 1334 return _isValidAttribute(lcTag, lcName, value); 1335 }; 1336 DOMPurify.addHook = function (entryPoint, hookFunction) { 1337 if (typeof hookFunction !== 'function') { 1338 return; 1339 } 1340 arrayPush(hooks[entryPoint], hookFunction); 1341 }; 1342 DOMPurify.removeHook = function (entryPoint, hookFunction) { 1343 if (hookFunction !== undefined) { 1344 const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); 1345 return index === -1 ? undefined : arraySplice(hooks[entryPoint], index, 1)[0]; 1346 } 1347 return arrayPop(hooks[entryPoint]); 1348 }; 1349 DOMPurify.removeHooks = function (entryPoint) { 1350 hooks[entryPoint] = []; 1351 }; 1352 DOMPurify.removeAllHooks = function () { 1353 hooks = _createHooksMap(); 1354 }; 1355 return DOMPurify; 1356 } 1357 var purify = createDOMPurify(); 1358 1359 return purify; 1360 1361})); 1362//# sourceMappingURL=purify.js.map 1363 1364/** 1365 * jQuery fixes 1366 * 1367 * Note: The following analysis and/or changes was required also to be applied in our code base: 1368 * - Ensure there is no JSONP ajax call 1369 * - Ensure to not use self closing tag when a closing tag is usually needed. 1370 * ex: `<div />` need to change for `<div></div>` 1371 * This is applicable to all tags except the one in this list: area|br|col|embed|hr|img|input|link|meta|param 1372 * - Review how `<tr>`, `<td>`, `<script>` and `<link>` are inserted with `$()`. 1373 * Their insertion need to be completed differently like the by using the javascript DOM interface. 1374 * - All content that related to jQuery DOM manipulation are sanitized with DOMPurify 1375 * - You can't use jQuery for parsing XML document, you can use DOMParser() as an alternative 1376 * - Ensure AJAX and fetch are sanitized before they response are used 1377 */ 1378/* 1379 1380// Test case for the jQuery fixes bellow 1381 1382Test: jQuery() 1383 1384 - Run in your browser console 1385 > $( "<p>abc<iframe//src=jAva	script:alert(3)>def</p>" ).appendTo( "#wb-cont" ); 1386 result: No iframe inserted around the heading level 1 1387 1388Test: 1389jQuery.html() 1390jQuery.append() 1391jQuery.prepend() 1392jQuery.before() 1393jQuery.after() 1394 1395 - Use the data-ajax demo page: /demos/data-ajax/data-ajax-en.html
1396 - Insert the following in the ajaxed in file: <p>abc<iframe//src=jAva	script:alert(3)>def</p> 1397 - Check if the dirty HTML are cleaned for the example: "append", "prepend", "before", "after" 1398 - The data-ajax-replace will also test the jQuery.html() function. 1399 1400Test: jQuery.replaceWith() 1401 1402 - Run in your browser console 1403 > $("#wb-cont").replaceWith( "<p>abc<iframe//src=jAva	script:alert(3)>def</p>" ); 1404 result: No iframe inserted around the heading level 1 1405 1406Test: jQuery.parseHTML() 1407 1408 - Run in your browser console, pass unsafe HTML and it should return sanitized HTML 1409 > jQuery.parseHTML( "<p>abc<iframe//src=jAva	script:alert(3)>def</p>" ); 1410 result: return an array with 1 element, [ p ] 1411 1412Test: jQuery.htmlPrefilter() 1413 1414 - Run in your browser console 1415 > jQuery.htmlPrefilter( "<tr/>" ); 1416 result: return <tr/> 1417 1418Test: jQuery.extend 1419 1420 - Run in your browser console 1421 > var dataFromAPI = JSON.parse( '{ "new": "property", "__proto__": { "polluted": "true" } }' ); 1422 > var myObject = jQuery.extend( '{ "new": "property" }', dataFromAPI ); 1423 > console.log( myObject.polluted ); 1424 result: > undefined 1425 1426*/ 1427 1428( function( jQuery, DOMPurify, window ) { 1429"use strict"; 1430 1431/** 1432 * Copied from: https://git.drupalcode.org/project/drupal/-/commit/d2f26902ef5d6de58caaf2a7c766eb2115b1c17e 1433 * 1434 * This is almost verbatim copied from jQuery 3.4.0. 1435 * 1436 * Now compatible with jQuery 4. 1437 * 1438 */ 1439jQuery.extend = jQuery.fn.extend = function() { 1440 var options, name, src, copy, copyIsArray, clone, 1441 target = arguments[ 0 ] || {}, 1442 i = 1, 1443 length = arguments.length, 1444 deep = false; 1445 1446 // Handle a deep copy situation 1447 if ( typeof target === "boolean" ) { 1448 deep = target; 1449 1450 // Skip the boolean and the target 1451 target = arguments[ i ] || {}; 1452 i++; 1453 } 1454 1455 // Handle case when target is a string or something (possible in deep copy) 1456 if ( typeof target !== "object" && typeof target !== "function" ) { 1457 target = {}; 1458 } 1459 1460 // Extend jQuery itself if only one argument is passed 1461 if ( i === length ) { 1462 target = this; 1463 i--; 1464 } 1465 1466 for ( ; i < length; i++ ) { 1467 1468 // Only deal with non-null/undefined values 1469 options = arguments[ i ]; 1470 if ( options !== undefined || options !== null ) { 1471 1472 // Extend the base object 1473 for ( name in options ) { 1474 copy = options[ name ]; 1475 1476 // Prevent Object.prototype pollution 1477 // Prevent never-ending loop 1478 if ( name === "__proto__" || target === copy ) { 1479 continue; 1480 } 1481 1482 // Recurse if we're merging plain objects or arrays 1483 if ( deep && copy && ( jQuery.isPlainObject( copy ) || 1484 ( copyIsArray = Array.isArray( copy ) ) ) ) { 1485 src = target[ name ]; 1486 1487 // Ensure proper type for the source value 1488 if ( copyIsArray && !Array.isArray( src ) ) { 1489 clone = []; 1490 } else if ( !copyIsArray && !jQuery.isPlainObject( src ) ) { 1491 clone = {}; 1492 } else { 1493 clone = src; 1494 } 1495 copyIsArray = false; 1496 1497 // Never move original objects, clone them 1498 target[ name ] = jQuery.extend( deep, clone, copy ); 1499 1500 // Don't bring in undefined values 1501 } else if ( copy !== undefined ) { 1502 target[ name ] = copy; 1503 } 1504 } 1505 } 1506 } 1507 1508 // Return the modified object 1509 return target; 1510}; 1511 1512/** 1513 * Security advisories: https://github.com/jquery/jquery/security/advisories/GHSA-gxr4-xjj5-5px2 1514 */ 1515jQuery.htmlPrefilter = function( html ) { 1516 return html; 1517}; 1518 1519/** 1520 * Security advisories: https://github.com/jquery/jquery/security/advisories/GHSA-jpcq-cgw6-v4j6 1521 * 1522 * This implementation leverage DOMPurify for filtering every string prior DOM manipulation by jQuery 1523 * 1524 */ 1525 1526// START: add hooks to DOMPurify to allow external links when they meet certain conditions as defined here: https://owasp.org/www-community/attacks/Reverse_Tabnabbing 1527DOMPurify.addHook( "beforeSanitizeAttributes", function( node ) { 1528 1529 // Add "data-wb-external-link" to all <a> with a target="_blank" and rel="noreferrer" 1530 if ( 1531 node.tagName === "A" && 1532 node.getAttribute( "target" ) && 1533 node.getAttribute( "target" ) === "_blank" && 1534 node.getAttribute( "rel" ) && 1535 node.relList.contains( "noreferrer" ) 1536 ) { 1537 node.setAttribute( "data-wb-external-link", "true" ); 1538 } 1539} ); 1540 1541DOMPurify.addHook( "afterSanitizeAttributes", function( node ) { 1542 1543 // Put back the target="_blank" to all <a> with attribute "data-wb-external-link" 1544 if ( node.tagName === "A" && node.getAttribute( "data-wb-external-link" ) ) { 1545 node.setAttribute( "target", "_blank" ); 1546 node.removeAttribute( "data-wb-external-link" ); 1547 } 1548} ); 1549 1550// END 1551 1552var localParseHTML = jQuery.parseHTML, 1553 append = jQuery.fn.append, 1554 prepend = jQuery.fn.prepend, 1555 before = jQuery.fn.before, 1556 after = jQuery.fn.after, 1557 replaceWith = jQuery.fn.replaceWith, 1558 jqInit = jQuery.fn.init, 1559 dataTableAllowedTag = [ 1560 "<tbody/>", 1561 "<tr/>", 1562 "<td />", 1563 "<td/>" 1564 ], 1565 sanitize = function( html ) { 1566 1567 // Add an exception for DataTable plugin 1568 if ( window.DataTable && dataTableAllowedTag.indexOf( html ) !== -1 ) { 1569 return html; 1570 } 1571 1572 return DOMPurify.sanitize( html ); 1573 }; 1574 1575jQuery.parseHTML = function( data, context, keepScripts ) { 1576 return localParseHTML( sanitize( data ), context, keepScripts ); 1577}; 1578 1579jQuery.domManip = null; 1580 1581jQuery.append = jQuery.fn.append = function() { 1582 var args = arguments, 1583 value = args[ 0 ]; 1584 if ( typeof value === "string" ) { 1585 value = sanitize( value ); 1586 args[ 0 ] = value; 1587 } 1588 return append.apply( this, args ); 1589}; 1590 1591jQuery.prepend = jQuery.fn.prepend = function() { 1592 var args = arguments, 1593 value = args[ 0 ]; 1594 if ( typeof value === "string" ) { 1595 value = sanitize( value ); 1596 args[ 0 ] = value; 1597 } 1598 return prepend.apply( this, args ); 1599}; 1600 1601jQuery.before = jQuery.fn.before = function() { 1602 var args = arguments, 1603 value = args[ 0 ]; 1604 if ( typeof value === "string" ) { 1605 value = sanitize( value ); 1606 args[ 0 ] = value; 1607 } 1608 return before.apply( this, args ); 1609}; 1610 1611jQuery.after = jQuery.fn.after = function() { 1612 var args = arguments, 1613 value = args[ 0 ]; 1614 if ( typeof value === "string" ) { 1615 value = sanitize( value ); 1616 args[ 0 ] = value; 1617 } 1618 return after.apply( this, args ); 1619}; 1620 1621jQuery.replaceWith = jQuery.fn.replaceWith = function() { 1622 var args = arguments, 1623 value = args[ 0 ]; 1624 if ( typeof value === "string" ) { 1625 value = sanitize( value ); 1626 args[ 0 ] = value; 1627 } 1628 return replaceWith.apply( this, args ); 1629}; 1630 1631jQuery.fn.init = function( selector, context, root ) { 1632 if ( typeof selector === "string" ) { 1633 selector = sanitize( selector ); 1634 } 1635 return new jqInit( selector, context, root ); 1636}; 1637 1638jQuery.html = function( value ) { 1639 return jQuery.html( sanitize( value ) ); 1640}; 1641 1642} )( jQuery, DOMPurify, window ); 1643 1644/* Modernizr (Custom Build) | MIT & BSD 1645 * Build: https://modernizr.com/download/#-elem_details-elem_progress_meter-mathml-cors-load-mq-css3-input-inputtypes-svg-cssclasses-csstransitions-fontface-backgroundsize-borderimage-teststyles-testprops-testallprops-hasevents-prefixes-domprefixes 1646 */ 1647;window.Modernizr=function(e,t,n){function L(e){f.cssText=e}function A(e,t){return L(p.join(e+";")+(t||""))}function O(e,t){return typeof e===t}function M(e,t){return!!~(""+e).indexOf(t)}function _(e,t){for(var r in e){var i=e[r];if(!M(i,"-")&&f[i]!==n)return t=="pfx"?i:!0}return!1}function D(e,t,r){for(var i in e){var s=t[e[i]];if(s!==n)return r===!1?e[i]:O(s,"function")?s.bind(r||t):s}return!1}function P(e,t,n){var r=e.charAt(0).toUpperCase()+e.slice(1),i=(e+" "+v.join(r+" ")+r).split(" ");return O(t,"string")||O(t,"undefined")?_(i,t):(i=(e+" "+m.join(r+" ")+r).split(" "),D(i,t,n))}function H(){i.input=function(n){for(var r=0,i=n.length;r<i;r++)w[n[r]]=n[r]in l;return w.list&&(w.list=!!t.createElement("datalist")&&!!e.HTMLDataListElement),w}("autocomplete autofocus list placeholder max min multiple pattern required step".split(" ")),i.inputtypes=function(e){for(var r=0,i,s,u,a=e.length;r<a;r++)l.setAttribute("type",s=e[r]),i=l.type!=="text",i&&(l.value=c,l.style.cssText="position:absolute;visibility:hidden;",/^range$/.test(s)&&l.style.WebkitAppearance!==n?(o.appendChild(l),u=t.defaultView,i=u.getComputedStyle&&u.getComputedStyle(l,null).WebkitAppearance!=="textfield"&&l.offsetHeight!==0,o.removeChild(l)):/^(search|tel)$/.test(s)||(/^(url|email)$/.test(s)?i=l.checkValidity&&l.checkValidity()===!1:i=l.value!=c)),b[e[r]]=!!i;return b}("search tel url email datetime date month week time datetime-local number range color".split(" "))}
vendor: 5,597 bytes, line 1647
1647var r="2.8.3",i={},s=!0,o=t.documentElement,u="modernizr",a=t.createElement(u),f=a.style,l=t.createElement("input"),c=":)",h={}.toString,p=" -webkit- -moz- -o- -ms- ".split(" "),d="Webkit Moz O ms",v=d.split(" "),m=d.toLowerCase().split(" "),g={svg:"http://www.w3.org/2000/svg"},y={},b={},w={},E=[],S=E.slice,x,T=function(e,n,r,i){var s,a,f,l,c=t.createElement("div"),h=t.body,p=h||t.createElement("body");if(parseInt(r,10))while(r--)f=t.createElement("div"),f.id=i?i[r]:u+(r+1),c.appendChild(f);return s=["­",'<style id="s',u,'">',e,"</style>"].join(""),c.id=u,(h?c:p).innerHTML+=s,p.appendChild(c),h||(p.style.background="",p.style.overflow="hidden",l=o.style.overflow,o.style.overflow="hidden",o.appendChild(p)),a=n(c,e),h?c.parentNode.removeChild(c):(p.parentNode.removeChild(p),o.style.overflow=l),!!a},N=function(t){var n=e.matchMedia||e.msMatchMedia;if(n)return n(t)&&n(t).matches||!1;var r;return T("@media "+t+" { #"+u+" { position: absolute; } }",function(t){r=(e.getComputedStyle?getComputedStyle(t,null):t.currentStyle)["position"]=="absolute"}),r},C={}.hasOwnProperty,k;!O(C,"undefined")&&!O(C.call,"undefined")?k=function(e,t){return C.call(e,t)}:k=function(e,t){return t in e&&O(e.constructor.prototype[t],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(t){var n=this;if(typeof n!="function")throw new TypeError;var r=S.call(arguments,1),i=function(){if(this instanceof i){var e=function(){};e.prototype=n.prototype;var s=new e,o=n.apply(s,r.concat(S.call(arguments)));return Object(o)===o?o:s}return n.apply(t,r.concat(S.call(arguments)))};return i}),y.backgroundsize=function(){return P("backgroundSize")},y.borderimage=function(){return P("borderImage")},y.csstransitions=function(){return P("transition")},y.fontface=function(){var e;return T('@font-face {font-family:"font";src:url("https://")}',function(n,r){var i=t.getElementById("smodernizr"),s=i.sheet||i.styleSheet,o=s?s.cssRules&&s.cssRules[0]?s.cssRules[0].cssText:s.cssText||"":"";e=/src/i.test(o)&&o.indexOf(r.split(" ")[0])===0}),e},y.svg=function(){return!!t.createElementNS&&!!t.createElementNS(g.svg,"svg").createSVGRect};for(var B in y)k(y,B)&&(x=B.toLowerCase(),i[x]=y[B](),E.push((i[x]?"":"no-")+x));return i.input||H(),i.addTest=function(e,t){if(typeof e=="object")for(var r in e)k(e,r)&&i.addTest(r,e[r]);else{e=e.toLowerCase();if(i[e]!==n)return i;t=typeof t=="function"?t():t,typeof s!="undefined"&&s&&(o.className+=" "+(t?"":"no-")+e),i[e]=t}return i},L(""),a=l=null,i._version=r,i._prefixes=p,i._domPrefixes=m,i._cssomPrefixes=v,i.mq=N,i.testProp=function(e){return _([e])},i.testAllProps=P,i.testStyles=T,o.className=o.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(s?" js "+E.join(" "):""),i}(this,this.document),function(e,t,n){function r(e){return"[object Function]"==d.call(e)}function i(e){return"string"==typeof e}function s(){}function o(e){return!e||"loaded"==e||"complete"==e||"uninitialized"==e}function u(){var e=v.shift();m=1,e?e.t?h(function(){("c"==e.t?k.injectCss:k.injectJs)(e.s,0,e.a,e.x,e.e,1)},0):(e(),u()):m=0}function a(e,n,r,i,s,a,f){function l(t){if(!d&&o(c.readyState)&&(w.r=d=1,!m&&u(),c.onload=c.onreadystatechange=null,t)){"img"!=e&&h(function(){b.removeChild(c)},50);for(var r in T[n])T[n].hasOwnProperty(r)&&T[n][r].onload()}}var f=f||k.errorTimeout,c=t.createElement(e),d=0,g=0,w={t:r,s:n,e:s,a:a,x:f};1===T[n]&&(g=1,T[n]=[]),"object"==e?c.data=n:(c.src=n,c.type=e),c.width=c.height="0",c.onerror=c.onload=c.onreadystatechange=function(){l.call(this,g)},v.splice(i,0,w),"img"!=e&&(g||2===T[n]?(b.insertBefore(c,y?null:p),h(l,f)):T[n].push(c))}function f(e,t,n,r,s){return m=0,t=t||"j",i(e)?a("c"==t?E:w,e,t,this.i++,n,r,s):(v.splice(this.i++,0,e),1==v.length&&u()),this}function l(){var e=k;return e.loader={load:f,i:0},e}var c=t.documentElement,h=e.setTimeout,p=t.getElementsByTagName("script")[0],d={}.toString,v=[],m=0,g="MozAppearance"in c.style,y=g&&!!t.createRange().compareNode,b=y?c:p.parentNode,c=e.opera&&"[object Opera]"==d.call(e.opera),c=!!t.attachEvent&&!c,w=g?"object":c?"script":"img",E=c?"script":w,S=Array.isArray||function(e){return"[object Array]"==d.call(e)},x=[],T={},N={timeout:function(e,t){return t.length&&(e.timeout=t[0]),e}},C,k;k=function(e){function t(e){var e=e.split("!"),t=x.length,n=e.pop(),r=e.length,n={url:n,origUrl:n,prefixes:e},i,s,o;for(s=0;s<r;s++)o=e[s].split("="),(i=N[o.shift()])&&(n=i(n,o));for(s=0;s<t;s++)n=x[s](n);return n}function o(e,i,s,o,u){var a=t(e),f=a.autoCallback;a.url.split(".").pop().split("?").shift(),a.bypass||(i&&(i=r(i)?i:i[e]||i[o]||i[e.split("/").pop().split("?")[0]]),a.instead?a.instead(e,i,s,o,u):(T[a.url]?a.noexec=!0:T[a.url]=1,s.load(a.url,a.forceCSS||!a.forceJS&&"css"==a.url.split(".").pop().split("?").shift()?"c":n,a.noexec,a.attrs,a.timeout),(r(i)||r(f))&&s.load(function(){l(),i&&i(a.origUrl,u,o),f&&f(a.origUrl,u,o),T[a.url]=2})))}function u(e,t){function n(e,n){if(e){if(i(e))n||(f=function(){var e=[].slice.call(arguments);l.apply(this,e),c()}),o(e,f,t,0,u);else if(Object(e)===e)for(p in h=function(){var t=0,n;for(n in e)e.hasOwnProperty(n)&&t++;return t}(),e)e.hasOwnProperty(p)&&(!n&&!--h&&(r(f)?f=function(){var e=[].slice.call(arguments);l.apply(this,e),c()}:f[p]=function(e){return function(){var t=[].slice.call(arguments);e&&e.apply(this,t),c()}}(l[p])),o(e[p],f,t,p,u))}else!n&&c()}var u=!!e.test,a=e.load||e.both,f=e.callback||s,l=f,c=e.complete||s,h,p;n(u?e.yep:e.nope,!!a),a&&n(a)}var a,f,c=this.yepnope.loader;if(i(e))o(e,0,c,0);else if(S(e))for(a=0;a<e.length;a++)f=e[a],i(f)?o(f,0,c,0):S(f)?k(f):Object(f)===f&&u(f,c);else Object(e)===e&&u(e,c)}
1647,k.addPrefix=function(e,t){N[e]=t},k.addFilter=function(e){x.push(e)},k.errorTimeout=1e4,null==t.readyState&&t.addEventListener&&(t.readyState="loading",t.addEventListener("DOMContentLoaded",C=function(){t.removeEventListener("DOMContentLoaded",C,0),t.readyState="complete"},0)),e.yepnope=l(),e.yepnope.executeStack=u,e.yepnope.injectJs=function(e,n,r,i,a,f){var l=t.createElement("script"),c,d,i=i||k.errorTimeout;l.src=e;for(d in r)l.setAttribute(d,r[d]);n=f?u:n||s,l.onreadystatechange=l.onload=function(){!c&&o(l.readyState)&&(c=1,n(),l.onload=l.onreadystatechange=null)},h(function(){c||(c=1,n(1))},i),a?l.onload():p.parentNode.insertBefore(l,p)},e.yepnope.injectCss=function(e,n,r,i,o,a){var i=t.createElement("link"),f,n=a?u:n||s;i.href=e,i.rel="stylesheet",i.type="text/css";for(f in r)i.setAttribute(f,r[f]);o||(p.parentNode.insertBefore(i,p),h(n,0))}}(this,document),Modernizr.load=function(){yepnope.apply(window,[].slice.call(arguments,0))},Modernizr.addTest("details",function(){var e=document,t=e.createElement("details"),n,r,i;return"open"in t?(r=e.body||function(){var t=e.documentElement;return n=!0,t.insertBefore(e.createElement("body"),t.firstElementChild||t.firstChild)}(),t.innerHTML="<summary>a</summary>b",t.style.display="block",r.appendChild(t),i=t.offsetHeight,t.open=!0,i=i!=t.offsetHeight,r.removeChild(t),n&&r.parentNode.removeChild(r),i):!1}),Modernizr.addTest("progressbar",function(){return document.createElement("progress").max!==undefined}),Modernizr.addTest("meter",function(){return document.createElement("meter").max!==undefined}),Modernizr.addTest("mathml",function(){var e=!1;if(document.createElementNS){var t="http://www.w3.org/1998/Math/MathML",n=document.createElement("div");n.style.position="absolute";var r=n.appendChild(document.createElementNS(t,"math")).appendChild(document.createElementNS(t,"mfrac"));r.appendChild(document.createElementNS(t,"mi")).appendChild(document.createTextNode("xx")),r.appendChild(document.createElementNS(t,"mi")).appendChild(document.createTextNode("yy")),document.body.appendChild(n),e=n.offsetHeight>n.offsetWidth}return e}),Modernizr.addTest("cors",!!(window.XMLHttpRequest&&"withCredentials"in new XMLHttpRequest)); 1648 1649/** 1650 * String.prototype.includes() polyfill 1651 * @author Ricokola 1652 * @license MIT 1653 */ 1654if ( !String.prototype.includes ) { 1655 String.prototype.includes = function( string ) { 1656 1657 return this.indexOf( string ) !== -1; 1658 1659 }; 1660} 1661 1662/** 1663 * String.prototype.replaceAll() polyfill 1664 * https://gomakethings.com/how-to-replace-a-section-of-a-string-with-another-one-with-vanilla-js/ 1665 * @author Chris Ferdinandi 1666 * @license MIT 1667 */ 1668if ( !String.prototype.replaceAll ) { 1669 String.prototype.replaceAll = function( str, newStr ) { 1670 1671 // If a regex pattern 1672 if ( Object.prototype.toString.call( str ).toLowerCase() === "[object regexp]" ) { 1673 return this.replace( str, newStr ); 1674 } 1675 1676 // If a string 1677 return this.replace( new RegExp( str, "g" ), newStr ); 1678 1679 }; 1680} 1681 1682/** 1683 * @title WET-BOEW Vapour loader 1684 * @overview Helper methods for WET 1685 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 1686 * @author WET Community 1687 */ 1688/* 1689 * Vapour Object that will store tombstone data for plugins to leverage 1690 */ 1691( function( $, window, document, undef ) { 1692"use strict"; 1693 1694/** 1695 * @method getUrlParts 1696 * @param {String} url of URL to examine 1697 * @return {Object} of the parts of passed URL 1698 */ 1699var getUrlParts = function( url ) { 1700 var a = document.createElement( "a" ); 1701 a.href = url; 1702 return { 1703 href: a.href, 1704 absolute: a.href, 1705 host: a.host, 1706 hostname: a.hostname, 1707 port: a.port, 1708 pathname: a.pathname.replace( /^([^/])/, "/$1" ), // Prefix pathname with a slash in browsers that don't natively do it (i.e. all versions of IE and possibly early versions of Edge). See pull request #8110. 1709 protocol: a.protocol, 1710 hash: a.hash, 1711 search: a.search, 1712 1713 // A collection of the parameters of the query string part of the URL. 1714 params: ( function() { 1715 var queryString = a.search.replace( /(^\?)/, "" ); 1716 if ( !queryString ) { 1717 return {}; 1718 } 1719 return queryString.split( "&" ).map( function( n ) { 1720 return ( n = n.split( "=" ), this[ n[ 0 ] ] = decodeURIComponent( n[ 1 ] ), this ); 1721 }.bind( {} ) )[ 0 ]; 1722 }() ) 1723 }; 1724 }, 1725 1726 /** 1727 * @variable seed 1728 * @return a unique number for auto-generating ids 1729 */
1730 seed = 0, 1731 1732 /** 1733 * @variable $src 1734 * @return {jQuery Element} of wb script element 1735 */ 1736 $src = $( "script[src*='wet-boew.js'],script[src*='wet-boew.min.js'],script[data-wb-core]" ) 1737 .last(), 1738 1739 /** 1740 * @variable i18n 1741 * @return {string} of WET document language 1742 */ 1743 lang = ( function( ele ) { 1744 let lang = document.documentElement.lang; 1745 const shortLangLength = 2; 1746 1747 // Perform extra checks if the page uses a long language code 1748 if ( lang.length > shortLangLength ) { 1749 let longLangs = [ "pt-BR", "zh-Hans" ]; // Built-in long language codes 1750 1751 // Check if any custom long language codes have been specified 1752 // Specify by adding data-wb-core and data-lang-long="en-CA en-US etc" attributes to WET's script element (e.g. wet-boew.js or wet-boew.min.js) 1753 if ( ele[ 0 ].hasAttribute( "data-wb-core" ) && ele[ 0 ].hasAttribute( "data-lang-long" ) ) { 1754 const longLangsCustom = ele.attr( "data-lang-long" ).split( " " ); 1755 1756 // Add extra language codes to the beginning of the longLangs array to match them more quickly 1757 longLangs = longLangsCustom.concat( longLangs ); 1758 } 1759 1760 // Shorten the language code if it doesn't exist in the longLangs array 1761 if ( longLangs.indexOf( lang ) === -1 ) { 1762 lang = lang.substring( 0, shortLangLength ); 1763 } 1764 } 1765 1766 return lang; 1767 }( $src ) ), 1768 1769 paths = ( function( ele ) { 1770 var paths = {}; 1771 1772 paths.home = ele.prop( "src" ) 1773 .split( "?" )[ 0 ].split( "/" ) 1774 .slice( 0, -1 ) 1775 .join( "/" ); 1776 paths.asset = paths.home + "/../assets"; 1777 paths.template = paths.home + "/assets/templates"; 1778 paths.dep = paths.home + "/deps"; 1779 paths.js = paths.home; 1780 paths.css = paths.home.substring( 0, paths.home.length - 2 ) + "css"; 1781 paths.mode = ele.prop( "src" ).indexOf( ".min" ) < 0 ? "" : ".min"; 1782 1783 if ( ele[ 0 ].hasAttribute( "data-wb-core" ) ) { 1784 $.extend( paths, { 1785 home: ele.attr( "data-home" ), 1786 asset: ele.attr( "data-asset" ), 1787 template: ele.attr( "data-template" ), 1788 dep: ele.attr( "data-dep" ), 1789 js: ele.attr( "data-js" ), 1790 css: ele.attr( "data-css" ), 1791 mode: ele.attr( "data-mode" ) 1792 } ); 1793 } 1794 1795 return paths; 1796 }( $src ) ), 1797 1798 /** 1799 * @variable oldie 1800 * @return {integer} of IE version 1801 */ 1802 oldie = ( function() { 1803 var undef, 1804 v = 3, 1805 div = document.createElement( "div" ), 1806 all = div.getElementsByTagName( "i" ); 1807 1808 while ( ( 1809 div.innerHTML = "<!--[if gt IE " + ( v += 1 ) + "]><i></i><![endif]-->", 1810 all[ 0 ] 1811 ) ) { /* empty */ } 1812 1813 return v > 4 ? v : undef; 1814 }() ), 1815 1816 /** 1817 * @variable currentpage 1818 * @return {Object} of parts of the current page URL 1819 */ 1820 currentpage = getUrlParts( window.location.href ), 1821 1822 /** 1823 * @variable disabled 1824 * @return {boolean} of state of disabled flag 1825 */ 1826 disabled = ( function() { 1827 var disabledSaved = "false", 1828 disabled; 1829 1830 try { 1831 disabledSaved = localStorage.getItem( "wbdisable" ) || disabledSaved; 1832 } catch ( e ) { 1833 1834 /* swallow error */ 1835 } 1836 1837 disabled = currentpage.params.wbdisable || disabledSaved; 1838 return ( typeof disabled === "string" ) ? ( disabled.toLowerCase() === "true" ) : Boolean( disabled ); 1839 }() ), 1840 1841 /*----------------------------- 1842 * Core Library Object 1843 *----------------------------- 1844 */ 1845 wb = { 1846 "/": paths.home, 1847 "/assets": paths.asset, 1848 "/templates": paths.template, 1849 "/deps": paths.dep, 1850 lang: lang, 1851 mode: paths.mode, 1852 doc: $( document ), 1853 win: $( window ), 1854 html: $( "html" ), 1855 pageUrlParts: currentpage, 1856 getUrlParts: getUrlParts, 1857 isDisabled: disabled, 1858 isStarted: false, 1859 isReady: false, 1860 ignoreHashChange: false, 1861 initQueue: 0, 1862 1863 // Checks if the element <details> is supported natively. 1864 // Vérifier si l'élément <details> est supporté nativement. 1865 supportsDetails: function() { 1866 return "open" in document.createElement( "details" ); 1867 }, 1868 1869 getPath: function( property ) { 1870 return Object.prototype.hasOwnProperty.call( this, property ) ? this[ property ] : undef; 1871 }, 1872 1873 getMode: function() { 1874 return this.mode; 1875 }, 1876 1877 getId: function() { 1878 var idPrefix = "wb-auto-", 1879 ids, 1880 numberCandidate, 1881 numbers = []; 1882
1883 // Check for conflicting hardcoded IDs the first time an ID is requested 1884 if ( !seed ) { 1885 ids = document.querySelectorAll( "[id^='" + idPrefix + "']" ); 1886 1887 // Loop through elements whose IDs begin with the prefix 1888 ids.forEach( function( currentElm ) { 1889 numberCandidate = currentElm.id.substring( idPrefix.length ); 1890 1891 // Verify whether the ID ends with a conflicting number and add it to an array 1892 if ( numberCandidate.search( /^\d+$/ ) !== -1 ) { 1893 numbers.push( numberCandidate ); 1894 } 1895 1896 console.error( "wb.getId: ID '" + currentElm.id + "' isn't supposed to be hardcoded in the page. Please remove it or change its prefix to something different than '" + idPrefix + "'." ); 1897 } ); 1898 1899 // Set the seed to the array's highest number (will be incremented later) 1900 seed = numbers.length ? Math.max.apply( null, numbers ) : seed; 1901 } 1902 1903 return idPrefix + ( seed += 1 ); 1904 }, 1905 1906 init: function( event, componentName, selector, noAutoId ) { 1907 var eventTarget = event.target, 1908 isEvent = !!eventTarget, 1909 node = isEvent ? eventTarget : event, 1910 initedClass = componentName + "-inited", 1911 isDocumentNode = node === document; 1912 1913 // Filter out any events triggered by descendants and only initializes 1914 // the element once (if is an event and document node is not the target) 1915 if ( !isEvent || isDocumentNode || ( event.currentTarget === node && 1916 node.className.indexOf( initedClass ) === -1 ) ) { 1917 1918 this.initQueue += 1; 1919 this.remove( selector ); 1920 if ( !isDocumentNode ) { 1921 node.className += " " + initedClass; 1922 1923 if ( !noAutoId && !node.id ) { 1924 node.id = wb.getId(); 1925 } 1926 } 1927 1928 return node; 1929 } 1930 1931 return undef; 1932 }, 1933 1934 ready: function( $elm, componentName, context ) { 1935 1936 if ( $elm ) { 1937 1938 // Trigger any nested elements (excluding nested within nested) 1939 $elm 1940 .find( wb.allSelectors ) 1941 .addClass( "wb-init" ) 1942 .filter( ":not(#" + $elm.attr( "id" ) + " .wb-init .wb-init)" ) 1943 .trigger( "timerpoke.wb" ); 1944 1945 // Identify that the component is ready 1946 $elm.trigger( "wb-ready." + componentName, context ); 1947 1948 this.initQueue -= 1; 1949 } else { 1950 this.doc.trigger( "wb-ready." + componentName, context ); 1951 } 1952 1953 // Identify that global initialization is complete 1954 if ( !this.isReady && this.isStarted && this.initQueue < 1 ) { 1955 1956 // Create DOM event 1957 const DOMevent = new Event( "wet-boew-ready" ); 1958 1959 this.isReady = true; 1960 this.doc.trigger( "wb-ready.wb" ); 1961 this.doc[ 0 ].dispatchEvent( DOMevent ); // Trigger native DOM event 1962 } 1963 }, 1964 1965 // Lets load some variables into wb for IE detection 1966 other: !oldie, 1967 desktop: ( window.orientation === undefined ), 1968 ie: !!oldie, 1969 ie6: ( oldie === 6 ), 1970 ie7: ( oldie === 7 ), 1971 ie8: ( oldie === 8 ), 1972 ie9: ( oldie === 9 ), 1973 ielt7: ( oldie < 7 ), 1974 ielt8: ( oldie < 8 ), 1975 ielt9: ( oldie < 9 ), 1976 ielt10: ( oldie < 10 ), 1977 ie11: ( navigator.userAgent.includes( "Trident/7." ) ), 1978 1979 selectors: [], 1980 1981 resizeEvents: "xxsmallview.wb xsmallview.wb smallview.wb mediumview.wb largeview.wb xlargeview.wb", 1982 1983 // For Charts and Geomap 1984 drawColours: [ 1985 "#8d201c", 1986 "#EE8310", 1987 "#2a7da6", 1988 "#5a306b", 1989 "#285228", 1990 "#154055", 1991 "#555555", 1992 "#f6d200", 1993 "#d73d38", 1994 "#418541", 1995 "#87aec9", 1996 "#23447e", 1997 "#999999" 1998 ], 1999 2000 // Get and generate a unique session id 2001 sessionGUID: function() { 2002 var sessionId = sessionStorage.getItem( "wb-session-GUID" ); 2003 if ( !sessionId ) { 2004 sessionId = wb.guid(); 2005 sessionStorage.setItem( "wb-session-GUID", sessionId ); 2006 } 2007 return sessionId; 2008 }, 2009 2010 // Add a selector to be targeted by timerpoke 2011 add: function( selector ) { 2012 var exists = false, 2013 len = wb.selectors.length, 2014 i; 2015 2016 // Lets ensure we are not running if things are disabled 2017 if ( wb.isDisabled && selector !== "#wb-tphp" ) { 2018 return 0; 2019 } 2020 2021 // Check to see if the selector is already targeted 2022 for ( i = 0; i !== len; i += 1 ) { 2023 if ( wb.selectors[ i ] === selector ) { 2024 exists = true; 2025 break; 2026 } 2027 } 2028 2029 // Add the selector if it isn't already targeted 2030 if ( !exists ) { 2031 wb.selectors.push( selector ); 2032 } 2033 }, 2034 2035 // Remove a selector targeted by timerpoke 2036 remove: function( selector ) { 2037 var len = this.selectors.length, 2038 i; 2039 2040 for ( i = 0; i !== len; i += 1 ) { 2041 if ( this.selectors[ i ] === selector ) { 2042 this.selectors.splice( i, 1 ); 2043 break; 2044 } 2045 } 2046 }, 2047 2048 // Handles triggering of timerpoke events 2049 timerpoke: function( initial ) { 2050 var selectorsLocal = wb.selectors.slice( 0 ), 2051 len = selectorsLocal.length, 2052 selector, $elms, $foundElms, i; 2053 2054 if ( initial ) { 2055 $foundElms = $(); 2056 for ( i = 0; i !== len; i += 1 ) { 2057 selector = selectorsLocal[ i ]; 2058 $elms = $( selector ); 2059 if ( $elms.length !== 0 ) {
2060 $foundElms = $foundElms.add( $elms ); 2061 2062 // If the selector returns no elements, remove the selector 2063 } else { 2064 wb.remove( selector ); 2065 } 2066 } 2067 2068 // Keep only the non-nested plugin/polyfill elements 2069 $elms = $foundElms.filter( ":not(.wb-init .wb-init)" ).addClass( "wb-init" ); 2070 } else { 2071 $elms = $( selectorsLocal.join( ", " ) ); 2072 } 2073 $elms.trigger( "timerpoke.wb" ); 2074 }, 2075 2076 start: function() { 2077 2078 // Save a copy of all the possible selectors 2079 wb.allSelectors = wb.selectors.join( ", " ); 2080 2081 // Initiate timerpoke events right way 2082 wb.timerpoke( true ); 2083 this.isStarted = true; 2084 this.ready(); 2085 2086 // Initiate timerpoke events again every half second 2087 setInterval( wb.timerpoke, 500 ); 2088 }, 2089 2090 i18nDict: {}, 2091 i18n: function( key, state, mixin ) { 2092 var dictionary = wb.i18nDict, 2093 2094 // eg. 000 or 001 ie. 0 or 1 2095 truthiness = ( typeof key === "string" && key !== "" ) | 2096 2097 // eg. 000 or 010 ie. 0 or 2 2098 ( typeof state === "string" && state !== "" ) << 1 | 2099 2100 // eg. 000 or 100 ie. 0 or 4 2101 ( typeof mixin === "string" && mixin !== "" ) << 2; 2102 2103 switch ( truthiness ) { 2104 case 1: 2105 2106 // only key was provided 2107 return dictionary[ key ]; 2108 2109 case 3: 2110 2111 // key and state were provided 2112 return dictionary[ key ][ state ]; 2113 2114 case 7: 2115 2116 // key, state, and mixin were provided 2117 return dictionary[ key ][ state ].replace( "[MIXIN]", mixin ); 2118 default: 2119 return ""; 2120 } 2121 }, 2122 2123 hashString: function( str ) { 2124 2125 // Sources: 2126 // https://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ 2127 // https://jsperf.com/hashing-strings 2128 var hash = 0, 2129 chr, i; 2130 2131 if ( str.length === 0 ) { 2132 return hash; 2133 } 2134 2135 for ( i = 0; i < str.length; i++ ) { 2136 chr = str.charCodeAt( i ); 2137 hash = ( ( hash << 5 ) - hash ) + chr; 2138 2139 // Convert to 32bit integer 2140 hash = hash & hash; 2141 } 2142 2143 return hash; 2144 }, 2145 2146 stripWhitespace: function( str ) { 2147 return str.replace( /\s+/g, "" ); 2148 }, 2149 2150 // Core function to deal with the dependency racing issue 2151 whenLibReady: function( testCallback, readyCallback ) { 2152 if ( testCallback() ) { 2153 readyCallback(); 2154 } else { 2155 setTimeout( function() { 2156 wb.whenLibReady( testCallback, readyCallback ); 2157 }, 50 ); 2158 } 2159 2160 } 2161 }; 2162 2163window.wb = wb; 2164 2165/*----------------------------- 2166 * Yepnope Prefixes 2167 *-----------------------------*/ 2168/* 2169 * Establish the base path to be more flexible in terms of WCMS where JS can 2170 * reside in theme folders and not in the root of sites 2171 * @TODO: For modularity the prefixes where written independently as we are 2172 * flushing out some use cases on better grouping and optimization of polyfills. 2173 * Once this more hashed out, we could optimize the prefixes down to one or two 2174 * prefixes "site" and "disabled" to thin out the codeblock a bit more 2175 * increase performance due to redundant chaining of the prefixes. 2176 */ 2177 2178/** 2179 * @prefix: site! - adds the root js directory of yepnope resources 2180 */ 2181yepnope.addPrefix( "site", function( resourceObj ) { 2182 resourceObj.url = paths.js + "/" + resourceObj.url; 2183 return resourceObj; 2184} ); 2185 2186/** 2187 * @prefix: plyfll! - builds the path for the polyfill resource 2188 */ 2189yepnope.addPrefix( "plyfll", function( resourceObj ) { 2190 var path, 2191 url = resourceObj.url; 2192 2193 if ( disabled && url.indexOf( "svg" ) === -1 ) { 2194 resourceObj.bypass = true; 2195 } else if ( !paths.mode ) { 2196 url = url.replace( ".min", "" ); 2197 } 2198 2199 if ( url.indexOf( ".css" ) !== -1 ) { 2200 resourceObj.forceCSS = true; 2201 path = paths.css; 2202 } else { 2203 path = paths.js; 2204 } 2205 resourceObj.url = path + "/polyfills/" + url; 2206 2207 return resourceObj; 2208} ); 2209 2210/** 2211 * @prefix: i18n! - adds the correct document language for our i18n library 2212 */ 2213yepnope.addPrefix( "i18n", function( resourceObj ) { 2214 resourceObj.url = paths.js + "/" + resourceObj.url + lang + paths.mode + ".js"; 2215 return resourceObj; 2216} ); 2217 2218/** 2219 * @prefix: mthjx! - adds the root directory of MathJax resources 2220 */ 2221yepnope.addPrefix( "mthjx", function( resourceObj ) { 2222 resourceObj.url = paths.js + "/MathJax/" + resourceObj.url; 2223 return resourceObj; 2224} ); 2225 2226/*----------------------------- 2227 * Deps loading, call "complete" callback when the deps is ready if a testReady is defined 2228 *-----------------------------*/
2229wb.modernizrLoad = Modernizr.load; 2230Modernizr.load = function( options ) { 2231 var i, i_len, i_cache, 2232 testReady, complete; 2233 if ( !Array.isArray( options ) ) { 2234 options = [ options ]; 2235 } 2236 i_len = options.length; 2237 for ( i = 0; i !== i_len; i += 1 ) { 2238 i_cache = options[ i ]; 2239 testReady = i_cache.testReady; 2240 complete = i_cache.complete; 2241 if ( testReady && complete ) { 2242 i_cache.complete = wb.whenLibReady( testReady, complete ); 2243 } 2244 } 2245 wb.modernizrLoad( options ); 2246}; 2247 2248/*----------------------------- 2249 * Modernizr Polyfill Loading 2250 *-----------------------------*/ 2251Modernizr.load( [ 2252 { 2253 test: Modernizr.details, 2254 nope: [ 2255 "plyfll!details.min.js", 2256 "plyfll!details.min.css" 2257 ] 2258 }, { 2259 test: Modernizr.input.list, 2260 nope: [ 2261 "plyfll!datalist.min.js", 2262 "plyfll!datalist.min.css" 2263 ] 2264 }, { 2265 test: Modernizr.inputtypes.date, 2266 nope: [ 2267 "plyfll!datepicker.min.js", 2268 "plyfll!datepicker.min.css" 2269 ] 2270 }, { 2271 test: Modernizr.inputtypes.range, 2272 nope: [ 2273 "plyfll!slider.min.js", 2274 "plyfll!slider_wrapper.min.js", 2275 "plyfll!slider.min.css" 2276 ], 2277 callback: function( url ) { 2278 if ( url === "slider.min.js" ) { 2279 window.fdSlider.onDomReady(); 2280 } 2281 } 2282 }, { 2283 test: Modernizr.progressbar, 2284 nope: [ 2285 "plyfll!progress.min.js", 2286 "plyfll!progress.min.css" 2287 ] 2288 }, { 2289 test: Modernizr.mathml, 2290 2291 // Cleanup Modernizr test and add selector to global timer 2292 complete: function() { 2293 var componentName = "wb-math", 2294 selector = "math", 2295 math = document.getElementsByTagName( selector ), 2296 $document = wb.doc; 2297 2298 // Cleanup elements that Modernizr.mathml test leaves behind. 2299 if ( math.length ) { 2300 document.body.removeChild( math[ math.length - 1 ].parentNode ); 2301 } 2302 2303 // Defer loading the polyfill till an element is detected due to the size 2304 if ( !Modernizr.mathml ) { 2305 let isTrident = new Boolean( window.navigator.msSaveOrOpenBlob ); 2306 2307 // Bind the init event of the plugin 2308 $document.one( "timerpoke.wb wb-init." + componentName, selector, function() { 2309 2310 // Start initialization 2311 wb.init( document, componentName, selector ); 2312 2313 // Disable MathJax's context menu to more closely mimic native MathML implementations 2314 window.MathJax = { 2315 options: { 2316 enableMenu: false 2317 } 2318 }; 2319 2320 // Extra tasks for IE11 2321 if ( isTrident ) { 2322 2323 // Load an ES6 polyfill 2324 Modernizr.load( "timeout=500!https://cdnjs.cloudflare.com/polyfill/v3/polyfill.min.js?features=es6" ); 2325 2326 // Specify the CDN's font URL 2327 // Note: IE11 is unable to resolve this on its own 2328 window.MathJax.chtml = { 2329 fontURL: "https://cdn.jsdelivr.net/npm/mathjax@3/es5/output/chtml/fonts/woff-v2" 2330 }; 2331 } 2332 2333 // Load the MathML dependency. Since the polyfill is only loaded 2334 // when !Modernizr.mathml, we can skip the test here. 2335 Modernizr.load( [ { 2336 2337 // Load latest version of MathJax 3 from a CDN 2338 // Also load a CSS workaround for a MathJax 3.2.0 bug (refer to CSS file for details) 2339 load: [ 2340 "timeout=500!https://cdn.jsdelivr.net/npm/mathjax@3/es5/mml-chtml.js", 2341 "plyfll!mathml.min.css" 2342 ], 2343 complete: function() { 2344 2345 // Wait a moment to reduce the risk of a race condition 2346 setTimeout( function() { 2347 2348 // Specify a font URL for a local copy of MathJax 3 for IE11 2349 // Note: Useful if IE11 has internet access but fails to reach the CDN 2350 if ( isTrident && !window.MathJax.startup ) { 2351 window.MathJax.chtml.fontURL = paths.js + "/MathJax/output/chtml/fonts/woff-v2"; 2352 } 2353 2354 // Fall back on a local copy of MathJax 3 if the CDN is unreachable 2355 // Note: Won't work with IE11 in isolated networks (ES6 polyfill has no local fallback) 2356 Modernizr.load( [ { 2357 test: window.MathJax.startup, 2358 nope: "mthjx!mml-chtml.js", 2359 complete: function() { 2360 2361 // Try loading a local copy of MathJax 2 as a last ditch effort 2362 Modernizr.load( [ { 2363 test: window.MathJax.startup, 2364 nope: "mthjx!MathJax.js?config=Accessible", 2365 complete: function() { 2366 2367 // Identify that initialization has completed 2368 wb.ready( $document, componentName ); 2369 } 2370 } ] ); 2371 } 2372 } ] ); 2373 }, 100 ); 2374 } 2375 } ] ); 2376 } ); 2377 2378 wb.add( selector ); 2379 } 2380 } 2381 }, { 2382 test: Modernizr.meter, 2383 nope: [ 2384 "plyfll!meter.min.js", 2385 "plyfll!meter.min.css" 2386 ] 2387 }, { 2388 test: Modernizr.touch, 2389 yep: "plyfll!mobile.min.js" 2390 }, { 2391 test: Modernizr.svg, 2392 nope: "plyfll!svg.min.js" 2393 }, { 2394 load: "i18n!i18n/", 2395 testReady: function() { 2396 return wb.i18nDict.tphp; 2397 }, 2398 complete: function() { 2399 wb.start(); 2400 } 2401 } 2402] ); 2403 2404} )( jQuery, window, document ); 2405 2406/** 2407 * @title WET-BOEW JQuery Helper Methods 2408 * @overview Helper methods for WET 2409 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 2410 * @author WET Community 2411 * Credits: https://web.archive.org/web/20130826230640/http://kaibun.net/blog/2013/04/19/a-fully-fledged-coffeescript-boilerplate-for-jquery-plugins/ 2412 */ 2413( function( $, wb ) { 2414 2415wb.getData = function( element, dataName ) { 2416 var elm = !element.jquery ? element : element[ 0 ],
2417 dataAttr = elm.getAttribute( "data-" + dataName ), 2418 dataObj; 2419 2420 if ( dataAttr ) { 2421 try { 2422 dataObj = JSON.parse( dataAttr ); 2423 $.data( elm, dataName, dataObj ); 2424 } catch ( error ) { 2425 console.info( elm ); 2426 $.error( "Bad JSON array in data-" + dataName + " attribute" ); 2427 } 2428 } 2429 2430 return dataObj; 2431}; 2432 2433/* 2434 * Initiate an in-browser download from a blob 2435 * @param blob: a reference to a blob object 2436 * @param filename: a suggested file name to save as under 2437 * @param title: (Optional) a title added to the link. Its use case is for web analytics tracking. 2438*/ 2439wb.download = function( blob, filename, title ) { 2440 2441 var objectURL = URL.createObjectURL( blob ), 2442 anchor = document.createElement( "a" ); 2443 2444 filename = filename || "unnamed"; // Ensure a filename is defined 2445 2446 anchor.textContent = title || ""; 2447 anchor.download = filename; 2448 2449 anchor.hidden = true; 2450 document.body.appendChild( anchor ); // Added to the body for the web analytic tracking use case. 2451 2452 if ( window.navigator.msSaveOrOpenBlob ) { 2453 2454 // This is for IE11 support 2455 anchor.addEventListener( "click", function( ) { 2456 window.navigator.msSaveOrOpenBlob( blob, filename ); 2457 } ); 2458 anchor.setAttribute( "target", "_blank" ); 2459 } else { 2460 anchor.href = objectURL; 2461 } 2462 2463 anchor.click(); 2464 2465 // Clean the DOM, remove the accessory anchor at the next tick 2466 setTimeout( function() { 2467 document.body.removeChild( anchor ); 2468 }, 1 ); 2469 2470 // Revoke the object, A setTimeout is used because Blob API don't have a download complete event. 2471 setTimeout( function() { 2472 if ( typeof objectURL === "string" ) { 2473 URL.revokeObjectURL( objectURL ); 2474 } else { 2475 objectURL.remove(); 2476 } 2477 }, 40000 ); // The revoking time is arbitrary 2478 2479}; 2480 2481/* --------------------------------- 2482@extension: shuffleDOM 2483@returns: [list] shuffles a list of items randomly 2484-------------------------------- */ 2485wb.shuffleDOM = function( $elm ) { 2486 var allElems = $elm.get(), 2487 shuffled = $.map( allElems, function() { 2488 var random = Math.floor( Math.random() * allElems.length ), 2489 randEl = $( allElems[ random ] ).clone( true )[ 0 ]; 2490 allElems.splice( random, 1 ); 2491 return randEl; 2492 } ), 2493 elm_len = $elm.length, 2494 i; 2495 2496 for ( i = 0; i < elm_len; i++ ) { 2497 $( $elm[ i ] ).replaceWith( $( shuffled[ i ] ) ); 2498 } 2499 2500 return $( shuffled ); 2501}; 2502 2503/* --------------------------------- 2504@extension: pickElements 2505@returns: [collection] of random elements 2506-------------------------------- */ 2507wb.pickElements = function( $elm, numOfElm ) { 2508 var nbElm = $elm.size(), 2509 elmCopies, 2510 i, swap; 2511 2512 numOfElm = numOfElm || 1; 2513 2514 // Special cases 2515 if ( numOfElm > nbElm ) { 2516 return $elm.pushStack( $elm ); 2517 } else if ( numOfElm === 1 ) { 2518 return $elm.filter( ":eq(" + Math.floor( Math.random() * nbElm ) + ")" ); 2519 } 2520 2521 // Create a randomized copy of the set of elements, 2522 // using Fisher-Yates sorting 2523 elmCopies = $elm.get(); 2524 2525 for ( i = 0; i < nbElm - 1; i++ ) { 2526 swap = Math.floor( Math.random() * ( nbElm - i ) ) + i; 2527 elmCopies[ swap ] = elmCopies.splice( i, 1, elmCopies[ swap ] )[ 0 ]; 2528 } 2529 elmCopies = elmCopies.slice( 0, numOfElm ); 2530 2531 // Finally, filter jQuery stack 2532 return $elm.filter( function( idx ) { 2533 return $.inArray( $elm.get( idx ), elmCopies ) > -1; 2534 } ); 2535}; 2536 2537/* --------------------------------- 2538Adds a link to the Skip links navigation 2539@param text: Text to display in the anchor or button 2540@param attr: JSO with { attribute: value, ... } to add attributes to the anchor or button. Minimum is { href: "#your-anchor" } for the anchor tag 2541@param isBtn: (Optional) Bool if true element is a button, otherwise it is an anchor by default 2542@param isLast: (Optional) Bool if true element will be inserted last in the list 2543-------------------------------- */ 2544wb.addSkipLink = function( text, attr, isBtn, isLast ) { 2545 var list = document.getElementById( "wb-tphp" ), 2546 li = document.createElement( "li" ), 2547 elm = document.createElement( ( isBtn ? "button" : "a" ) ), 2548 key; 2549 2550 // Add skip link's proprietary classes to new element 2551 li.className = "wb-slc"; 2552 elm.className = "wb-sl"; 2553 2554 // Add given attributes to element 2555 for ( key in attr ) { 2556 elm.setAttribute( key, attr[ key ] ); 2557 } 2558 2559 // Append text and new element to the skip link list (after main content) 2560 elm.appendChild( document.createTextNode( text ) ); 2561 li.appendChild( elm ); 2562 2563 if ( isLast ) { 2564 list.appendChild( li ); 2565 } else { 2566 list.insertBefore( li, list.childNodes[ 2 ] ); 2567 } 2568 2569 return true; 2570}; 2571 2572} )( jQuery, wb ); 2573 2574( function( wb, window ) { 2575 2576"use strict"; 2577
2578// Escapes the characters in a string for use in a jQuery selector 2579// Based on https://totaldev.com/content/escaping-characters-get-valid-jquery-id 2580wb.jqEscape = function( selector ) { 2581 // eslint-disable-next-line no-useless-escape 2582 return selector.replace( /([;&,\.\+\*\~':"\\\!\^\/#$%@\[\]\(\)=>\|])/g, "\\$1" ); 2583}; 2584 2585// RegEx used by formattedNumCompare 2586wb.formattedNumCompareRegEx = /(<[^>]*>|[^\d.])/g; 2587 2588// Compares two formatted numbers (e.g., 1.2.12 or 1,000,345) 2589wb.formattedNumCompare = function( a, b ) { 2590 var regEx = wb.formattedNumCompareRegEx, 2591 aMultiple = a.indexOf( "-" ) === -1 ? 1 : -1, 2592 aNumbers = ( ( a === "-" || a === "" ) ? "0" : a.replace( regEx, "" ) ).split( "." ), 2593 bMultiple = b.indexOf( "-" ) === -1 ? 1 : -1, 2594 bNumbers = ( ( b === "-" || b === "" ) ? "0" : b.replace( regEx, "" ) ).split( "." ), 2595 len = aNumbers.length, 2596 i, result; 2597 2598 for ( i = 0; i !== len; i += 1 ) { 2599 result = parseInt( aNumbers[ i ], 10 ) * aMultiple - parseInt( bNumbers[ i ], 10 ) * bMultiple; 2600 if ( result !== 0 ) { 2601 break; 2602 } 2603 } 2604 return result; 2605}; 2606 2607// Compare two strings with special characters (e.g., Cyrillic or Chinese characters) 2608wb.i18nTextCompare = function( a, b ) { 2609 return wb.normalizeDiacritics( a ).localeCompare( wb.normalizeDiacritics( b ) ); 2610}; 2611 2612// Based upon https://gist.github.com/instanceofme/1731620 2613// Licensed under WTFPL v2 http://sam.zoy.org/wtfpl/COPYING 2614wb.normalizeDiacritics = function( str ) { 2615 var diacritics = { 2616 "\u24B6": "A", 2617 "\uFF21": "A", 2618 "\u00C0": "A", 2619 "\u00C1": "A", 2620 "\u00C2": "A", 2621 "\u1EA6": "A", 2622 "\u1EA4": "A", 2623 "\u1EAA": "A", 2624 "\u1EA8": "A", 2625 "\u00C3": "A", 2626 "\u0100": "A", 2627 "\u0102": "A", 2628 "\u1EB0": "A", 2629 "\u1EAE": "A", 2630 "\u1EB4": "A", 2631 "\u1EB2": "A", 2632 "\u0226": "A", 2633 "\u01E0": "A", 2634 "\u00C4": "A", 2635 "\u01DE": "A", 2636 "\u1EA2": "A", 2637 "\u00C5": "A", 2638 "\u01FA": "A", 2639 "\u01CD": "A", 2640 "\u0200": "A", 2641 "\u0202": "A", 2642 "\u1EA0": "A", 2643 "\u1EAC": "A", 2644 "\u1EB6": "A", 2645 "\u1E00": "A", 2646 "\u0104": "A", 2647 "\u023A": "A", 2648 "\u2C6F": "A", 2649 "\uA732": "AA", 2650 "\u00C6": "AE", 2651 "\u01FC": "AE", 2652 "\u01E2": "AE", 2653 "\uA734": "AO", 2654 "\uA736": "AU", 2655 "\uA738": "AV", 2656 "\uA73A": "AV", 2657 "\uA73C": "AY", 2658 "\u24B7": "B", 2659 "\uFF22": "B", 2660 "\u1E02": "B", 2661 "\u1E04": "B", 2662 "\u1E06": "B", 2663 "\u0243": "B", 2664 "\u0182": "B", 2665 "\u0181": "B", 2666 "\u24B8": "C", 2667 "\uFF23": "C", 2668 "\u0106": "C", 2669 "\u0108": "C", 2670 "\u010A": "C", 2671 "\u010C": "C", 2672 "\u00C7": "C", 2673 "\u1E08": "C", 2674 "\u0187": "C", 2675 "\u023B": "C", 2676 "\uA73E": "C", 2677 "\u24B9": "D", 2678 "\uFF24": "D", 2679 "\u1E0A": "D", 2680 "\u010E": "D", 2681 "\u1E0C": "D", 2682 "\u1E10": "D", 2683 "\u1E12": "D", 2684 "\u1E0E": "D", 2685 "\u0110": "D", 2686 "\u018B": "D", 2687 "\u018A": "D", 2688 "\u0189": "D", 2689 "\uA779": "D", 2690 "\u01F1": "DZ", 2691 "\u01C4": "DZ", 2692 "\u01F2": "Dz", 2693 "\u01C5": "Dz", 2694 "\u24BA": "E", 2695 "\uFF25": "E", 2696 "\u00C8": "E", 2697 "\u00C9": "E", 2698 "\u00CA": "E", 2699 "\u1EC0": "E", 2700 "\u1EBE": "E", 2701 "\u1EC4": "E", 2702 "\u1EC2": "E", 2703 "\u1EBC": "E", 2704 "\u0112": "E", 2705 "\u1E14": "E", 2706 "\u1E16": "E", 2707 "\u0114": "E", 2708 "\u0116": "E", 2709 "\u00CB": "E", 2710 "\u1EBA": "E", 2711 "\u011A": "E", 2712 "\u0204": "E", 2713 "\u0206": "E", 2714 "\u1EB8": "E", 2715 "\u1EC6": "E", 2716 "\u0228": "E", 2717 "\u1E1C": "E", 2718 "\u0118": "E", 2719 "\u1E18": "E", 2720 "\u1E1A": "E", 2721 "\u0190": "E", 2722 "\u018E": "E", 2723 "\u24BB": "F", 2724 "\uFF26": "F", 2725 "\u1E1E": "F", 2726 "\u0191": "F", 2727 "\uA77B": "F", 2728 "\u24BC": "G", 2729 "\uFF27": "G", 2730 "\u01F4": "G", 2731 "\u011C": "G", 2732 "\u1E20": "G", 2733 "\u011E": "G", 2734 "\u0120": "G", 2735 "\u01E6": "G", 2736 "\u0122": "G", 2737 "\u01E4": "G", 2738 "\u0193": "G", 2739 "\uA7A0": "G", 2740 "\uA77D": "G", 2741 "\uA77E": "G", 2742 "\u24BD": "H", 2743 "\uFF28": "H", 2744 "\u0124": "H", 2745 "\u1E22": "H", 2746 "\u1E26": "H", 2747 "\u021E": "H", 2748 "\u1E24": "H", 2749 "\u1E28": "H", 2750 "\u1E2A": "H", 2751 "\u0126": "H", 2752 "\u2C67": "H", 2753 "\u2C75": "H", 2754 "\uA78D": "H", 2755 "\u24BE": "I", 2756 "\uFF29": "I", 2757 "\u00CC": "I", 2758 "\u00CD": "I", 2759 "\u00CE": "I", 2760 "\u0128": "I", 2761 "\u012A": "I", 2762 "\u012C": "I", 2763 "\u0130": "I", 2764 "\u00CF": "I", 2765 "\u1E2E": "I", 2766 "\u1EC8": "I", 2767 "\u01CF": "I", 2768 "\u0208": "I", 2769 "\u020A": "I", 2770 "\u1ECA": "I", 2771 "\u012E": "I", 2772 "\u1E2C": "I", 2773 "\u0197": "I", 2774 "\u24BF": "J", 2775 "\uFF2A": "J", 2776 "\u0134": "J", 2777 "\u0248": "J", 2778 "\u24C0": "K", 2779 "\uFF2B": "K", 2780 "\u1E30": "K", 2781 "\u01E8": "K", 2782 "\u1E32": "K", 2783 "\u0136": "K", 2784 "\u1E34": "K", 2785 "\u0198": "K", 2786 "\u2C69": "K", 2787 "\uA740": "K", 2788 "\uA742": "K", 2789 "\uA744": "K", 2790 "\uA7A2": "K", 2791 "\u24C1": "L", 2792 "\uFF2C": "L", 2793 "\u013F": "L", 2794 "\u0139": "L", 2795 "\u013D": "L", 2796 "\u1E36": "L", 2797 "\u1E38": "L", 2798 "\u013B": "L", 2799 "\u1E3C": "L", 2800 "\u1E3A": "L", 2801 "\u0141": "L", 2802 "\u023D": "L", 2803 "\u2C62": "L", 2804 "\u2C60": "L", 2805 "\uA748": "L", 2806 "\uA746": "L", 2807 "\uA780": "L", 2808 "\u01C7": "LJ", 2809 "\u01C8": "Lj", 2810 "\u24C2": "M", 2811 "\uFF2D": "M", 2812 "\u1E3E": "M", 2813 "\u1E40": "M", 2814 "\u1E42": "M", 2815 "\u2C6E": "M", 2816 "\u019C": "M", 2817 "\u24C3": "N", 2818 "\uFF2E": "N", 2819 "\u01F8": "N", 2820 "\u0143": "N", 2821 "\u00D1": "N", 2822 "\u1E44": "N", 2823 "\u0147": "N", 2824 "\u1E46": "N", 2825 "\u0145": "N", 2826 "\u1E4A": "N", 2827 "\u1E48": "N", 2828 "\u0220": "N", 2829 "\u019D": "N", 2830 "\uA790": "N", 2831 "\uA7A4": "N", 2832 "\u01CA": "NJ", 2833 "\u01CB": "Nj", 2834 "\u24C4": "O", 2835 "\uFF2F": "O", 2836 "\u00D2": "O", 2837 "\u00D3": "O", 2838 "\u00D4": "O", 2839 "\u1ED2": "O", 2840 "\u1ED0": "O", 2841 "\u1ED6": "O", 2842 "\u1ED4": "O", 2843 "\u00D5": "O", 2844 "\u1E4C": "O", 2845 "\u022C": "O", 2846 "\u1E4E": "O", 2847 "\u014C": "O", 2848 "\u1E50": "O", 2849 "\u1E52": "O", 2850 "\u014E": "O", 2851 "\u022E": "O", 2852 "\u0230": "O", 2853 "\u00D6": "O", 2854 "\u022A": "O", 2855 "\u1ECE": "O", 2856 "\u0150": "O", 2857 "\u01D1": "O", 2858 "\u020C": "O", 2859 "\u020E": "O", 2860 "\u01A0": "O", 2861 "\u1EDC": "O", 2862 "\u1EDA": "O", 2863 "\u1EE0": "O", 2864 "\u1EDE": "O", 2865 "\u1EE2": "O", 2866 "\u1ECC": "O", 2867 "\u1ED8": "O", 2868 "\u01EA": "O", 2869 "\u01EC": "O", 2870 "\u00D8": "O", 2871 "\u01FE": "O", 2872 "\u0186": "O", 2873 "\u019F": "O", 2874 "\uA74A": "O", 2875 "\uA74C": "O", 2876 "\u0152": "OE", 2877 "\u01A2": "OI", 2878 "\uA74E": "OO", 2879 "\u0222": "OU", 2880 "\u24C5": "P", 2881 "\uFF30": "P", 2882 "\u1E54": "P", 2883 "\u1E56": "P", 2884 "\u01A4": "P", 2885 "\u2C63": "P", 2886 "\uA750": "P", 2887 "\uA752": "P", 2888 "\uA754": "P", 2889 "\u24C6": "Q", 2890 "\uFF31": "Q", 2891 "\uA756": "Q", 2892 "\uA758": "Q", 2893 "\u024A": "Q", 2894 "\u24C7": "R", 2895 "\uFF32": "R", 2896 "\u0154": "R", 2897 "\u1E58": "R", 2898 "\u0158": "R", 2899 "\u0210": "R", 2900 "\u0212": "R", 2901 "\u1E5A": "R", 2902 "\u1E5C": "R", 2903 "\u0156": "R", 2904 "\u1E5E": "R", 2905 "\u024C": "R", 2906 "\u2C64": "R", 2907 "\uA75A": "R", 2908 "\uA7A6": "R", 2909 "\uA782": "R", 2910 "\u24C8": "S", 2911 "\uFF33": "S", 2912 "\u015A": "S", 2913 "\u1E64": "S", 2914 "\u015C": "S", 2915 "\u1E60": "S", 2916 "\u0160": "S", 2917 "\u1E66": "S", 2918 "\u1E62": "S", 2919 "\u1E68": "S", 2920 "\u0218": "S", 2921 "\u015E": "S", 2922 "\u2C7E": "S", 2923 "\uA7A8": "S", 2924 "\uA784": "S", 2925 "\u1E9E": "SS", 2926 "\u24C9": "T", 2927 "\uFF34": "T", 2928 "\u1E6A": "T", 2929 "\u0164": "T", 2930 "\u1E6C": "T", 2931 "\u021A": "T", 2932 "\u0162": "T", 2933 "\u1E70": "T", 2934 "\u1E6E": "T", 2935 "\u0166": "T", 2936 "\u01AC": "T", 2937 "\u01AE": "T", 2938 "\u023E": "T", 2939 "\uA786": "T", 2940 "\uA728": "TZ", 2941 "\u24CA": "U", 2942 "\uFF35": "U", 2943 "\u00D9": "U", 2944 "\u00DA": "U", 2945 "\u00DB": "U", 2946 "\u0168": "U", 2947 "\u1E78": "U", 2948 "\u016A": "U", 2949 "\u1E7A": "U", 2950 "\u016C": "U", 2951 "\u00DC": "U", 2952 "\u01DB": "U", 2953 "\u01D7": "U", 2954 "\u01D5": "U", 2955 "\u01D9": "U", 2956 "\u1EE6": "U", 2957 "\u016E": "U", 2958 "\u0170": "U", 2959 "\u01D3": "U", 2960 "\u0214": "U", 2961 "\u0216": "U", 2962 "\u01AF": "U", 2963 "\u1EEA": "U", 2964 "\u1EE8": "U", 2965 "\u1EEE": "U", 2966 "\u1EEC": "U", 2967 "\u1EF0": "U", 2968 "\u1EE4": "U", 2969 "\u1E72": "U", 2970 "\u0172": "U", 2971 "\u1E76": "U", 2972 "\u1E74": "U", 2973 "\u0244": "U", 2974 "\u24CB": "V", 2975 "\uFF36": "V", 2976 "\u1E7C": "V", 2977 "\u1E7E": "V", 2978 "\u01B2": "V", 2979 "\uA75E": "V", 2980 "\u0245": "V", 2981 "\uA760": "VY", 2982 "\u24CC": "W", 2983 "\uFF37": "W", 2984 "\u1E80": "W", 2985 "\u1E82": "W", 2986 "\u0174": "W", 2987 "\u1E86": "W", 2988 "\u1E84": "W", 2989 "\u1E88": "W", 2990 "\u2C72": "W", 2991 "\u24CD": "X", 2992 "\uFF38": "X", 2993 "\u1E8A": "X", 2994 "\u1E8C": "X", 2995 "\u24CE": "Y", 2996 "\uFF39": "Y", 2997 "\u1EF2": "Y", 2998 "\u00DD": "Y", 2999 "\u0176": "Y", 3000 "\u1EF8": "Y", 3001 "\u0232": "Y", 3002 "\u1E8E": "Y", 3003 "\u0178": "Y", 3004 "\u1EF6": "Y", 3005 "\u1EF4": "Y", 3006 "\u01B3": "Y", 3007 "\u024E": "Y", 3008 "\u1EFE": "Y", 3009 "\u24CF": "Z", 3010 "\uFF3A": "Z", 3011 "\u0179": "Z", 3012 "\u1E90": "Z", 3013 "\u017B": "Z", 3014 "\u017D": "Z", 3015 "\u1E92": "Z", 3016 "\u1E94": "Z", 3017 "\u01B5": "Z", 3018 "\u0224": "Z", 3019 "\u2C7F": "Z", 3020 "\u2C6B": "Z", 3021 "\uA762": "Z", 3022 "\u24D0": "a", 3023 "\uFF41": "a", 3024 "\u1E9A": "a", 3025 "\u00E0": "a", 3026 "\u00E1": "a", 3027 "\u00E2": "a", 3028 "\u1EA7": "a", 3029 "\u1EA5": "a", 3030 "\u1EAB": "a", 3031 "\u1EA9": "a",
3032 "\u00E3": "a", 3033 "\u0101": "a", 3034 "\u0103": "a", 3035 "\u1EB1": "a", 3036 "\u1EAF": "a", 3037 "\u1EB5": "a", 3038 "\u1EB3": "a", 3039 "\u0227": "a", 3040 "\u01E1": "a", 3041 "\u00E4": "a", 3042 "\u01DF": "a", 3043 "\u1EA3": "a", 3044 "\u00E5": "a", 3045 "\u01FB": "a", 3046 "\u01CE": "a", 3047 "\u0201": "a", 3048 "\u0203": "a", 3049 "\u1EA1": "a", 3050 "\u1EAD": "a", 3051 "\u1EB7": "a", 3052 "\u1E01": "a", 3053 "\u0105": "a", 3054 "\u2C65": "a", 3055 "\u0250": "a", 3056 "\uA733": "aa", 3057 "\u00E6": "ae", 3058 "\u01FD": "ae", 3059 "\u01E3": "ae", 3060 "\uA735": "ao", 3061 "\uA737": "au", 3062 "\uA739": "av", 3063 "\uA73B": "av", 3064 "\uA73D": "ay", 3065 "\u24D1": "b", 3066 "\uFF42": "b", 3067 "\u1E03": "b", 3068 "\u1E05": "b", 3069 "\u1E07": "b", 3070 "\u0180": "b", 3071 "\u0183": "b", 3072 "\u0253": "b", 3073 "\u24D2": "c", 3074 "\uFF43": "c", 3075 "\u0107": "c", 3076 "\u0109": "c", 3077 "\u010B": "c", 3078 "\u010D": "c", 3079 "\u00E7": "c", 3080 "\u1E09": "c", 3081 "\u0188": "c", 3082 "\u023C": "c", 3083 "\uA73F": "c", 3084 "\u2184": "c", 3085 "\u24D3": "d", 3086 "\uFF44": "d", 3087 "\u1E0B": "d", 3088 "\u010F": "d", 3089 "\u1E0D": "d", 3090 "\u1E11": "d", 3091 "\u1E13": "d", 3092 "\u1E0F": "d", 3093 "\u0111": "d", 3094 "\u018C": "d", 3095 "\u0256": "d", 3096 "\u0257": "d", 3097 "\uA77A": "d", 3098 "\u01F3": "dz", 3099 "\u01C6": "dz", 3100 "\u24D4": "e", 3101 "\uFF45": "e", 3102 "\u00E8": "e", 3103 "\u00E9": "e", 3104 "\u00EA": "e", 3105 "\u1EC1": "e", 3106 "\u1EBF": "e", 3107 "\u1EC5": "e", 3108 "\u1EC3": "e", 3109 "\u1EBD": "e", 3110 "\u0113": "e", 3111 "\u1E15": "e", 3112 "\u1E17": "e", 3113 "\u0115": "e", 3114 "\u0117": "e", 3115 "\u00EB": "e", 3116 "\u1EBB": "e", 3117 "\u011B": "e", 3118 "\u0205": "e", 3119 "\u0207": "e", 3120 "\u1EB9": "e", 3121 "\u1EC7": "e", 3122 "\u0229": "e", 3123 "\u1E1D": "e", 3124 "\u0119": "e", 3125 "\u1E19": "e", 3126 "\u1E1B": "e", 3127 "\u0247": "e", 3128 "\u025B": "e", 3129 "\u01DD": "e", 3130 "\u24D5": "f", 3131 "\uFF46": "f", 3132 "\u1E1F": "f", 3133 "\u0192": "f", 3134 "\uA77C": "f", 3135 "\u24D6": "g", 3136 "\uFF47": "g", 3137 "\u01F5": "g", 3138 "\u011D": "g", 3139 "\u1E21": "g", 3140 "\u011F": "g", 3141 "\u0121": "g", 3142 "\u01E7": "g", 3143 "\u0123": "g", 3144 "\u01E5": "g", 3145 "\u0260": "g", 3146 "\uA7A1": "g", 3147 "\u1D79": "g", 3148 "\uA77F": "g", 3149 "\u24D7": "h", 3150 "\uFF48": "h", 3151 "\u0125": "h", 3152 "\u1E23": "h", 3153 "\u1E27": "h", 3154 "\u021F": "h", 3155 "\u1E25": "h", 3156 "\u1E29": "h", 3157 "\u1E2B": "h", 3158 "\u1E96": "h", 3159 "\u0127": "h", 3160 "\u2C68": "h", 3161 "\u2C76": "h", 3162 "\u0265": "h", 3163 "\u0195": "hv", 3164 "\u24D8": "i", 3165 "\uFF49": "i", 3166 "\u00EC": "i", 3167 "\u00ED": "i", 3168 "\u00EE": "i", 3169 "\u0129": "i", 3170 "\u012B": "i", 3171 "\u012D": "i", 3172 "\u00EF": "i", 3173 "\u1E2F": "i", 3174 "\u1EC9": "i", 3175 "\u01D0": "i", 3176 "\u0209": "i", 3177 "\u020B": "i", 3178 "\u1ECB": "i", 3179 "\u012F": "i", 3180 "\u1E2D": "i", 3181 "\u0268": "i", 3182 "\u0131": "i", 3183 "\u24D9": "j", 3184 "\uFF4A": "j", 3185 "\u0135": "j", 3186 "\u01F0": "j", 3187 "\u0249": "j", 3188 "\u24DA": "k", 3189 "\uFF4B": "k", 3190 "\u1E31": "k", 3191 "\u01E9": "k", 3192 "\u1E33": "k", 3193 "\u0137": "k", 3194 "\u1E35": "k", 3195 "\u0199": "k", 3196 "\u2C6A": "k", 3197 "\uA741": "k", 3198 "\uA743": "k", 3199 "\uA745": "k", 3200 "\uA7A3": "k", 3201 "\u24DB": "l", 3202 "\uFF4C": "l", 3203 "\u0140": "l", 3204 "\u013A": "l", 3205 "\u013E": "l", 3206 "\u1E37": "l", 3207 "\u1E39": "l", 3208 "\u013C": "l", 3209 "\u1E3D": "l", 3210 "\u1E3B": "l", 3211 "\u0142": "l", 3212 "\u019A": "l", 3213 "\u026B": "l", 3214 "\u2C61": "l", 3215 "\uA749": "l", 3216 "\uA781": "l", 3217 "\uA747": "l", 3218 "\u01C9": "lj", 3219 "\u24DC": "m", 3220 "\uFF4D": "m", 3221 "\u1E3F": "m", 3222 "\u1E41": "m", 3223 "\u1E43": "m", 3224 "\u0271": "m", 3225 "\u026F": "m", 3226 "\u24DD": "n", 3227 "\uFF4E": "n", 3228 "\u01F9": "n", 3229 "\u0144": "n", 3230 "\u00F1": "n", 3231 "\u1E45": "n", 3232 "\u0148": "n", 3233 "\u1E47": "n", 3234 "\u0146": "n", 3235 "\u1E4B": "n", 3236 "\u1E49": "n", 3237 "\u019E": "n", 3238 "\u0272": "n", 3239 "\u0149": "n", 3240 "\uA791": "n", 3241 "\uA7A5": "n", 3242 "\u01CC": "nj", 3243 "\u24DE": "o", 3244 "\uFF4F": "o", 3245 "\u00F2": "o", 3246 "\u00F3": "o", 3247 "\u00F4": "o", 3248 "\u1ED3": "o", 3249 "\u1ED1": "o", 3250 "\u1ED7": "o", 3251 "\u1ED5": "o", 3252 "\u00F5": "o", 3253 "\u1E4D": "o", 3254 "\u022D": "o", 3255 "\u1E4F": "o", 3256 "\u014D": "o", 3257 "\u1E51": "o", 3258 "\u1E53": "o", 3259 "\u014F": "o", 3260 "\u022F": "o", 3261 "\u0231": "o", 3262 "\u00F6": "o", 3263 "\u022B": "o", 3264 "\u1ECF": "o", 3265 "\u0151": "o", 3266 "\u01D2": "o", 3267 "\u020D": "o", 3268 "\u020F": "o", 3269 "\u01A1": "o", 3270 "\u1EDD": "o", 3271 "\u1EDB": "o", 3272 "\u1EE1": "o", 3273 "\u1EDF": "o", 3274 "\u1EE3": "o", 3275 "\u1ECD": "o", 3276 "\u1ED9": "o", 3277 "\u01EB": "o", 3278 "\u01ED": "o", 3279 "\u00F8": "o", 3280 "\u01FF": "o", 3281 "\u0254": "o", 3282 "\uA74B": "o", 3283 "\uA74D": "o", 3284 "\u0275": "o", 3285 "\u0153": "oe", 3286 "\u0276": "oe", 3287 "\u01A3": "oi", 3288 "\u0223": "ou", 3289 "\uA74F": "oo", 3290 "\u24DF": "p", 3291 "\uFF50": "p", 3292 "\u1E55": "p", 3293 "\u1E57": "p", 3294 "\u01A5": "p", 3295 "\u1D7D": "p", 3296 "\uA751": "p", 3297 "\uA753": "p", 3298 "\uA755": "p", 3299 "\u24E0": "q", 3300 "\uFF51": "q", 3301 "\u024B": "q", 3302 "\uA757": "q", 3303 "\uA759": "q", 3304 "\u24E1": "r", 3305 "\uFF52": "r", 3306 "\u0155": "r", 3307 "\u1E59": "r", 3308 "\u0159": "r", 3309 "\u0211": "r", 3310 "\u0213": "r", 3311 "\u1E5B": "r", 3312 "\u1E5D": "r", 3313 "\u0157": "r", 3314 "\u1E5F": "r", 3315 "\u024D": "r", 3316 "\u027D": "r", 3317 "\uA75B": "r", 3318 "\uA7A7": "r", 3319 "\uA783": "r", 3320 "\u24E2": "s", 3321 "\uFF53": "s", 3322 "\u015B": "s", 3323 "\u1E65": "s", 3324 "\u015D": "s", 3325 "\u1E61": "s", 3326 "\u0161": "s", 3327 "\u1E67": "s", 3328 "\u1E63": "s", 3329 "\u1E69": "s", 3330 "\u0219": "s", 3331 "\u015F": "s", 3332 "\u023F": "s", 3333 "\uA7A9": "s", 3334 "\uA785": "s", 3335 "\u017F": "s", 3336 "\u1E9B": "s", 3337 "\u00DF": "ss", 3338 "\u24E3": "t", 3339 "\uFF54": "t", 3340 "\u1E6B": "t", 3341 "\u1E97": "t", 3342 "\u0165": "t", 3343 "\u1E6D": "t", 3344 "\u021B": "t", 3345 "\u0163": "t", 3346 "\u1E71": "t", 3347 "\u1E6F": "t", 3348 "\u0167": "t", 3349 "\u01AD": "t", 3350 "\u0288": "t", 3351 "\u2C66": "t", 3352 "\uA787": "t", 3353 "\uA729": "tz", 3354 "\u24E4": "u", 3355 "\uFF55": "u", 3356 "\u00F9": "u", 3357 "\u00FA": "u", 3358 "\u00FB": "u", 3359 "\u0169": "u", 3360 "\u1E79": "u", 3361 "\u016B": "u", 3362 "\u1E7B": "u", 3363 "\u016D": "u", 3364 "\u00FC": "u", 3365 "\u01DC": "u", 3366 "\u01D8": "u", 3367 "\u01D6": "u", 3368 "\u01DA": "u", 3369 "\u1EE7": "u", 3370 "\u016F": "u", 3371 "\u0171": "u", 3372 "\u01D4": "u", 3373 "\u0215": "u", 3374 "\u0217": "u", 3375 "\u01B0": "u", 3376 "\u1EEB": "u", 3377 "\u1EE9": "u", 3378 "\u1EEF": "u", 3379 "\u1EED": "u", 3380 "\u1EF1": "u", 3381 "\u1EE5": "u", 3382 "\u1E73": "u", 3383 "\u0173": "u", 3384 "\u1E77": "u", 3385 "\u1E75": "u", 3386 "\u0289": "u", 3387 "\u24E5": "v", 3388 "\uFF56": "v", 3389 "\u1E7D": "v", 3390 "\u1E7F": "v", 3391 "\u028B": "v", 3392 "\uA75F": "v", 3393 "\u028C": "v", 3394 "\uA761": "vy", 3395 "\u24E6": "w", 3396 "\uFF57": "w",
3397 "\u1E81": "w", 3398 "\u1E83": "w", 3399 "\u0175": "w", 3400 "\u1E87": "w", 3401 "\u1E85": "w", 3402 "\u1E98": "w", 3403 "\u1E89": "w", 3404 "\u2C73": "w", 3405 "\u24E7": "x", 3406 "\uFF58": "x", 3407 "\u1E8B": "x", 3408 "\u1E8D": "x", 3409 "\u24E8": "y", 3410 "\uFF59": "y", 3411 "\u1EF3": "y", 3412 "\u00FD": "y", 3413 "\u0177": "y", 3414 "\u1EF9": "y", 3415 "\u0233": "y", 3416 "\u1E8F": "y", 3417 "\u00FF": "y", 3418 "\u1EF7": "y", 3419 "\u1E99": "y", 3420 "\u1EF5": "y", 3421 "\u01B4": "y", 3422 "\u024F": "y", 3423 "\u1EFF": "y", 3424 "\u24E9": "z", 3425 "\uFF5A": "z", 3426 "\u017A": "z", 3427 "\u1E91": "z", 3428 "\u017C": "z", 3429 "\u017E": "z", 3430 "\u1E93": "z", 3431 "\u1E95": "z", 3432 "\u01B6": "z", 3433 "\u0225": "z", 3434 "\u0240": "z", 3435 "\u2C6C": "z", 3436 "\uA763": "z", 3437 "\uFF10": "0", 3438 "\u2080": "0", 3439 "\u24EA": "0", 3440 "\u2070": "0", 3441 "\u00B9": "1", 3442 "\u2474": "1", 3443 "\u2081": "1", 3444 "\u2776": "1", 3445 "\u24F5": "1", 3446 "\u2488": "1", 3447 "\u2460": "1", 3448 "\uFF11": "1", 3449 "\u00B2": "2", 3450 "\u2777": "2", 3451 "\u2475": "2", 3452 "\uFF12": "2", 3453 "\u2082": "2", 3454 "\u24F6": "2", 3455 "\u2461": "2", 3456 "\u2489": "2", 3457 "\u00B3": "3", 3458 "\uFF13": "3", 3459 "\u248A": "3", 3460 "\u2476": "3", 3461 "\u2083": "3", 3462 "\u2778": "3", 3463 "\u24F7": "3", 3464 "\u2462": "3", 3465 "\u24F8": "4", 3466 "\u2463": "4", 3467 "\u248B": "4", 3468 "\uFF14": "4", 3469 "\u2074": "4", 3470 "\u2084": "4", 3471 "\u2779": "4", 3472 "\u2477": "4", 3473 "\u248C": "5", 3474 "\u2085": "5", 3475 "\u24F9": "5", 3476 "\u2478": "5", 3477 "\u277A": "5", 3478 "\u2464": "5", 3479 "\uFF15": "5", 3480 "\u2075": "5", 3481 "\u2479": "6", 3482 "\u2076": "6", 3483 "\uFF16": "6", 3484 "\u277B": "6", 3485 "\u2086": "6", 3486 "\u2465": "6", 3487 "\u24FA": "6", 3488 "\u248D": "6", 3489 "\uFF17": "7", 3490 "\u2077": "7", 3491 "\u277C": "7", 3492 "\u24FB": "7", 3493 "\u248E": "7", 3494 "\u2087": "7", 3495 "\u247A": "7", 3496 "\u2466": "7", 3497 "\u2467": "8", 3498 "\u248F": "8", 3499 "\u24FC": "8", 3500 "\u247B": "8", 3501 "\u2078": "8", 3502 "\uFF18": "8", 3503 "\u277D": "8", 3504 "\u2088": "8", 3505 "\u24FD": "9", 3506 "\uFF19": "9", 3507 "\u2490": "9", 3508 "\u277E": "9", 3509 "\u247C": "9", 3510 "\u2089": "9", 3511 "\u2468": "9", 3512 "\u2079": "9" 3513 }, 3514 chars = str.split( "" ), 3515 len = chars.length, 3516 normalized = false, 3517 i, character; 3518 for ( i = 0; i !== len; i += 1 ) { 3519 character = chars[ i ]; 3520 if ( Object.prototype.hasOwnProperty.call( diacritics, character ) ) { 3521 chars[ i ] = diacritics[ character ]; 3522 normalized = true; 3523 } 3524 } 3525 return ( normalized ? chars.join( "" ) : str ); 3526}; 3527 3528/** 3529 * @namespace wb.string 3530 */ 3531wb.string = { 3532 3533 /* 3534 * Left-pads a number with zeros. 3535 * @memberof wb.string 3536 * @param {number} number The original number to pad. 3537 * @param {number} length The width of the resulting padded number, not the number of zeros to add to the front of the string. 3538 * @return {string} The padded string 3539 */ 3540 pad: function( number, length ) { 3541 var str = number + "", 3542 diff = length - str.length, 3543 i; 3544 for ( i = 0; i !== diff; i += 1 ) { 3545 str = "0" + str; 3546 } 3547 return str; 3548 }, 3549 3550 /* 3551 * Convert a base64 string into an ArrayBuffer (Note: this function are not fully UTF-8 supported and may create interoperability issue) 3552 * ref. https://www.isummation.com/blog/convert-arraybuffer-to-base64-string-and-vice-versa/ 3553 * @memberof wb.string 3554 * @param {string} Base64 browser encoded 3555 * @return {ArrayBuffer} string converted into ArrayBuffer 3556 */ 3557 base64ToArrayBuffer: function( base64 ) { 3558 var binary_string = window.atob( base64 ), 3559 len = binary_string.length, 3560 bytes = new Uint8Array( len ), 3561 i; 3562 for ( i = 0; i < len; i++ ) { 3563 bytes[ i ] = binary_string.charCodeAt( i ); 3564 } 3565 return bytes.buffer; 3566 }, 3567 3568 /* 3569 * Convert an ArrayBuffer into base64 string (Note: this function are not fully UTF-8 supported and may create interoperability issue) 3570 * ref. https://www.isummation.com/blog/convert-arraybuffer-to-base64-string-and-vice-versa/ 3571 * @memberof wb.string 3572 * @param {ArrayBuffer} 3573 * @return {string} ArrayBuffer converted into base64 string 3574 */ 3575 arrayBufferToBase64: function( buffer ) { 3576 var binary = "", 3577 bytes = new Uint8Array( buffer ), 3578 len = bytes.byteLength, 3579 i; 3580 for ( i = 0; i < len; i++ ) { 3581 binary += String.fromCharCode( bytes[ i ] ); 3582 } 3583 return window.btoa( binary ); 3584 }, 3585 3586 /* 3587 * Convert an hexadecimal string into an ArrayBuffer 3588 * ref. https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript/50868276#50868276 3589 * @memberof wb.string 3590 * @param {string} Encoded string in hexadecimal 3591 * @return {Uint8Array} Binary array buffer 3592 */ 3593 fromHexString: function( hexString ) { 3594 return hexString === null ? null : Uint8Array.from( hexString.match( /.{1,2}/g ).map( function( byte ) { 3595 return parseInt( byte, 16 ); 3596 } ) ); 3597 }, 3598 3599 /* 3600 * Convert an ArrayBuffer into an hexadecimal string 3601 * ref. https://stackoverflow.com/questions/38987784/how-to-convert-a-hexadecimal-string-to-uint8array-and-back-in-javascript/50868276#50868276 3602 * @memberof wb.string 3603 * @param {Uint8Array} Binary array buffer 3604 * @return {string} Encoded string in hexadecimal 3605 */ 3606 toHexString: function( bytes ) { 3607 return bytes.reduce( function( str, byte ) { 3608 return str + byte.toString( 16 ).padStart( 2, "0" ); 3609 }, "" ); 3610 } 3611 3612}; 3613 3614/* 3615 * A suite of date related functions for easier parsing of dates 3616 * @namespace wb.date 3617 */ 3618wb.date = { 3619 3620 /* 3621 * Converts the date to a date-object. The input can be: 3622 * <ul> 3623 * <li>a Date object: returned without modification.</li> 3624 * <li>an array: Interpreted as [year,month,day]. NOTE: month is 0-11.</li> 3625 * <li>a number: Interpreted as number of milliseconds since 1 Jan 1970 (a timestamp).</li> 3626 * <li>a string: Any format supported by the javascript engine, like 'YYYY/MM/DD', 'MM/DD/YYYY', 'Jan 31 2009' etc.</li> 3627 * <li>an object: Interpreted as an object with year, month and date attributes. **NOTE** month is 0-11.</li> 3628 * </ul> 3629 * @memberof wb.date 3630 * @param {Date | number[] | number | string | object} dateValue 3631 * @return {Date | NaN} 3632 */ 3633 convert: function( dateValue ) { 3634 var dateConstructor = dateValue.constructor; 3635 3636 switch ( dateConstructor ) { 3637 case Date: 3638 return dateConstructor; 3639 case Array: 3640 return new Date( dateValue[ 0 ], dateValue[ 1 ], dateValue[ 2 ] ); 3641 case Number: 3642 case String: 3643 return new Date( dateValue ); 3644 default: 3645 return typeof dateValue === "object" ? new Date( dateValue.year, dateValue.month, dateValue.date ) : NaN; 3646 } 3647 }, 3648 3649 /* 3650 * Compares two dates (input can be any type supported by the convert function). 3651 * @memberof wb.date 3652 * @param {Date | number[] | number | string | object} dateValue1 3653 * @param {Date | number[] | number | string | object} dateValue2 3654 * @return {number | NaN} 3655 * @example returns 3656 * -1 if dateValue1 < dateValue2 3657 * 0 if dateValue1 = dateValue2 3658 * 1 if dateValue1 > dateValue2 3659 * NaN if dateValue1 or dateValue2 is an illegal date 3660 */ 3661 compare: function( dateValue1, dateValue2 ) { 3662 var convert = wb.date.convert; 3663 3664 if ( isFinite( dateValue1 = convert( dateValue1 ).valueOf() ) && isFinite( dateValue2 = convert( dateValue2 ).valueOf() ) ) { 3665 return ( dateValue1 > dateValue2 ) - ( dateValue1 < dateValue2 ); 3666 } 3667 return NaN; 3668 }, 3669 3670 /* 3671 * Cross-browser safe way of translating a date to ISO format 3672 * @memberof wb.date 3673 * @param {Date | number[] | number | string | object} dateValue 3674 * @param {boolean} withTime Optional. Whether to include the time in the result, or just the date. False if blank. 3675 * @return {string} 3676 * @example 3677 * toDateISO( new Date() ) 3678 * returns "2012-04-27" 3679 * toDateISO( new Date(), true ) 3680 * returns "2012-04-27 13:46" 3681 */ 3682 toDateISO: function( dateValue, withTime ) { 3683 var date = wb.date.convert( dateValue ), 3684 pad = wb.string.pad; 3685 3686 return date.getFullYear() + "-" + pad( date.getMonth() + 1, 2, "0" ) + "-" + pad( date.getDate(), 2, "0" ) + 3687 ( withTime ? " " + pad( date.getHours(), 2, "0" ) + ":" + pad( date.getMinutes(), 2, "0" ) : "" ); 3688 }, 3689 3690 /* 3691 * Cross-browser safe way of creating a date object from a date string in ISO format 3692 * @memberof wb.date 3693 * @param {string} dateISO Date string in ISO format 3694 * @return {Date} 3695 */ 3696 fromDateISO: function( dateISO ) { 3697 var date = null; 3698 3699 if ( dateISO && /\d{4}-\d{2}-\d{2}/.test( dateISO ) ) { 3700 date = new Date( dateISO.substr( 0, 4 ), dateISO.substr( 5, 2 ) - 1, dateISO.substr( 8, 2 ), 0, 0, 0, 0 ); 3701 } 3702 return date; 3703 } 3704}; 3705 3706/* 3707 * Returns a RFC4122 compliant Global Unique ID (GUID). 3708 * Originally from https://stackoverflow.com/a/2117523/455535 3709 */ 3710wb.guid = function() { 3711 return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace( /[xy]/g, function( replacementChar ) { 3712 var rand = Math.random() * 16 | 0, 3713 newChar = replacementChar === "x" ? rand : ( rand & 0x3 | 0x8 ); 3714 return newChar.toString( 16 ); 3715 } ); 3716}; 3717 3718wb.escapeAttribute = function( str ) { 3719 return str.replace( /'/g, "'" ).replace( /"/g, """ ); 3720}; 3721 3722/* 3723 * Returns an escaped HTML string 3724 */ 3725wb.escapeHTML = function( str ) { 3726 return wb.escapeAttribute( str 3727 .replace( /&/g, "&" ) 3728 .replace( /</g, "<" ) 3729 .replace( />/g, ">" ) ); 3730}; 3731 3732/* 3733 * Returns a UTF-8 output from Base64 3734 * Reference: https://developer.mozilla.org/fr/docs/Glossary/Base64 (To be reviewed later because escape function is deprecated) 3735 */
3736wb.decodeUTF8Base64 = function( str ) { 3737 return decodeURIComponent( escape( atob( str ) ) ); 3738}; 3739 3740/* 3741* Find most common Personal Identifiable Information (PII) in a string and return either the cleaned string either true/false 3742* @param {string} str (required) - the content that needs to be verified 3743* 3744* @param {boolean} scope - if true will scrub the content 3745* @param {object} (optional) the 2nd param (scope) can also be an object having the following properties (optional): 3746* {string} any key name of the default patterns e.g. email, digits, etc. with the value 1. The function will only scrub the content that match the regex of the default patterns passed in this object 3747* {regex} customCase - this param is a regex. It will search and replace the values corresponding that pattern 3748* 3749* @param {object} opts (optional) - the 3rd param of the function that can contain the following properties (optional): 3750* {boolean} isCustomExclusive - if true, it will scrub only the custom regex if the regex is the only property of the "scope" object 3751* {boolean} useFullBlock - if true, it will replace the scrubbed characters with the "â" symbol; 3752* {string} replaceWith - this string will replace the scrubbed content 3753* 3754 3755* @return {string | true | false} 3756* @example 3757* wb.findPotentialPII( "email:[email protected], phone:123 123 1234", true ) 3758* returns "email:, phone:", 3759* 3760* wb.findPotentialPII( "email:[email protected], phone:123 123 1234", false ) 3761* returns true 3762* 3763* wb.findPotentialPII( "email:[email protected], phone:123 123 1234", { email:1 }{ replaceWith: [REDACTED/CAVIARDÃ] } ) 3764* returns "email:[REDACTED/CAVIARDÃ], phone:123 123 1234" 3765* 3766* wb.findPotentialPII( "email:[email protected], phone:123 123 1234, numéro de cas 12345678", { "customCase":/\b(?:case[\s-]?number[\s\-\\.]?(?:\d{5,10}))|(?:numéro[\s-]?de[\s-]?cas[\s\-\\.]?(?:\d{5,10}))/ig }, { useFullBlock:1}) 3767* returns "phone:ââââââââââââ, email:âââââââââââââ, postalCode:ââââââ, ââââââââââââââââââââââ" 3768*/ 3769wb.findPotentialPII = function( str, scope, opts ) { 3770 if ( str && typeof str !== "string" ) { 3771 return false; 3772 } 3773 var oRegEx = { 3774 3775 /* 3776 * Digits: 3777 * 9 digits or more 3778 */ 3779 digits: /\d(?:[\s\-\\.\\/]?\d){8,}(?!\d)/ig, 3780 3781 /* 3782 * Phone: 3783 * Any international phone number format 3784 */ 3785 phone: /\+?(\d{1,3})?[-._\s]?(\(?\d{3}\)?)[-._\s]?(\d{3})[-._\s]?(\d{4})/ig, 3786 3787 /* 3788 * Passport: 3789 * 2 letters followed by either a " ", a "/", a ".", or a "-" any amount of times, followed by 6 digits 3790 */ 3791 passport: /\b[A-Za-z]{2}[\s\\.-]*?\d{6}\b/ig, 3792 3793 /* 3794 * Email: 3795 * valid email format 3796 */ 3797 email: /\b(?:[a-zA-Z0-9_\-\\.]+)(?:@|%40|%2540)(?:[a-zA-Z0-9_\-\\.]+)\.(?:[a-zA-Z]{2,5})\b/ig, 3798 3799 /* 3800 * Loose email: 3801 * email address that has one or more whitespaces before the "@" sign and either a "." or "," after the domain name 3802 */ 3803 looseEmail: /([a-zA-Z0-9_\-.]+)\s*@([\sa-zA-Z0-9_\-.]+)[.,]([a-zA-Z]{1,5})/g, 3804 3805 /* 3806 * Loose email 2: 3807 * matches probable email format that the user tried to hide 3808 * any amount of letters, numbers, ".", "_", "%", "+", or "-", followed by 0 or 1 whitespace, 3809 * followed by "@", followed by 0 or 1 whitespace, followed by "gmail", "outlook", "hotmail", or "yahoo". 3810 */ 3811 looseEmail2: /([a-zA-Z0-9._%+-]+)\s?@\s?(gmail|outlook|icloud|hotmail|yahoo)(\s?\.?\s?(com|ca))?/ig, 3812 3813 /* 3814 * Postal code: 3815 * valid Canadian postal code 3816 */ 3817 postalCode: /\b[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d\b/ig, 3818 3819 /* 3820 * Username: 3821 * "username" or "user", 3822 * followed by a colon or an equals sign, 3823 * followed by any character that is not a " " or a "&" 3824 */ 3825 username: /(?:(username|user)[%20]?([:=]|(%EF%BC%9A))[^\s&]*)/ig, 3826 3827 /* 3828 * Password: 3829 * "password" or "pass", 3830 * followed by a ":" or a "=", 3831 * followed by any character that is not a " " or a "&" 3832 */ 3833 password: /(?:(password|pass)[%20]?([:=]|(%EF%BC%9A))[^\s&]*)/ig 3834 }, 3835 isFound = false, 3836 txtMarker = opts && opts.replaceWith ? opts.replaceWith : "", 3837 toClean = typeof scope === "object" ? true : scope, 3838 arMatchedStr, 3839 settings = opts || {}, 3840 defaultSettings = { 3841 isCustomExclusive: false, 3842 useFullBlock: false, 3843 replaceWith: "" 3844 }, 3845 isFullBlock = settings.useFullBlock || false, 3846 validatedScope = typeof scope === "object" ? {} : oRegEx; 3847 //settings = $.extend( {}, defaultSettings, settings ); 3848 settings = Object.assign( {}, defaultSettings, settings ); 3849 3850 if ( Object.keys( validatedScope ).length === 0 ) { 3851 if ( settings.isCustomExclusive ) { 3852 for ( var key in scope ) { 3853 if ( scope[ key ] instanceof RegExp ) { 3854 validatedScope[ key ] = scope[ key ]; 3855 } 3856 } 3857 } else { 3858 if ( Object.keys( scope ).length === 1 && Object.values( scope )[ 0 ] instanceof RegExp ) { 3859 validatedScope = oRegEx; 3860 validatedScope[ Object.keys( scope )[ 0 ] ] = Object.values( scope )[ 0 ]; 3861 } else { 3862 for ( var keyScope in scope ) { 3863 if ( Object.prototype.hasOwnProperty.call( oRegEx, keyScope ) ) { 3864 validatedScope [ keyScope ] = oRegEx [ keyScope ]; 3865 } else { 3866 if ( scope[ keyScope ] instanceof RegExp ) { 3867 validatedScope [ keyScope ] = scope [ keyScope ]; 3868 } 3869 } 3870 } 3871 } 3872 } 3873 } 3874 3875 for ( var valKey in validatedScope ) { 3876 arMatchedStr = str.match( validatedScope[ valKey ] ); 3877 if ( arMatchedStr ) { 3878 isFound = true; 3879 if ( toClean ) { 3880 txtMarker = isFullBlock ? "â".repeat( arMatchedStr[ 0 ].length ) : txtMarker; 3881 str = str.replaceAll( validatedScope[ valKey ], txtMarker ); 3882 } 3883 } 3884 } 3885 3886 return toClean && isFound ? str : isFound; 3887}; 3888 3889} )( wb, window ); 3890 3891( function( $, undef ) { 3892"use strict"; 3893 3894var methods, 3895 settings = { 3896 "default": "wet-boew" 3897 }; 3898 3899methods = { 3900 3901 init: function( options ) { 3902 return $.extend( settings, options || {} ); 3903 }, 3904 3905 show: function( onlyAria ) { 3906 $( this ).each( function() { 3907 var $elm = $( this ); 3908 $elm.attr( "aria-hidden", "false" ); 3909 if ( onlyAria === undef ) { 3910 $elm.removeClass( "wb-inv" ); 3911 } 3912 } ); 3913 }, 3914 3915 hide: function( onlyAria ) { 3916 $( this ) 3917 .each( function() { 3918 var $elm = $( this ); 3919 $elm.attr( "aria-hidden", "true" ); 3920 if ( onlyAria === undef ) { 3921 return $elm.addClass( "wb-inv" ); 3922 } 3923 } ); 3924 }, 3925 3926 toggle: function( to, from ) { 3927 $( this ) 3928 .addClass( to ) 3929 .removeClass( from ); 3930 } 3931}; 3932 3933$.fn.wb = function( method ) { 3934 3935 if ( methods[ method ] ) { 3936 methods[ method ].apply( this, Array.prototype.slice.call( arguments, 1 ) ); 3937 } else if ( typeof method === "object" || !method ) { 3938 methods.init.apply( this, arguments ); 3939 } else { 3940 $.error( "Method " + method + " does not exist on jquery.wb" ); 3941 } 3942}; 3943 3944} )( jQuery ); 3945 3946/* 3947:focusable and :tabable jQuery helper expressions - https://github.com/jquery/jquery-ui/blob/24756a978a977d7abbef5e5bce403837a01d964f/ui/jquery.ui.core.js 3948*/ 3949( function( $ ) { 3950 3951"use strict"; 3952 3953function focusable( element, isTabIndexNotNaN, visibility ) { 3954 var map, mapName, img, 3955 nodeName = element.nodeName.toLowerCase( ); 3956 if ( nodeName === "area" ) { 3957 map = element.parentNode; 3958 mapName = map.name; 3959 if ( !element.href || !mapName || map.nodeName.toLowerCase( ) !== "map" ) { 3960 return false;
3961 } 3962 img = $( "img[usemap=#" + mapName + "]" )[ 0 ]; 3963 return !!img && visible( img ); 3964 } 3965 if ( visibility ) { 3966 return ( /input|select|textarea|button|object|summary/.test( nodeName ) ? !element.disabled : 3967 nodeName === "a" ? 3968 element.href || isTabIndexNotNaN : 3969 isTabIndexNotNaN ) && 3970 visible( element ); /* the element and all of its ancestors must be visible */ 3971 } else { 3972 return ( /input|select|textarea|button|object|summary/.test( nodeName ) ? !element.disabled : 3973 nodeName === "a" ? 3974 element.href || isTabIndexNotNaN : 3975 isTabIndexNotNaN ); 3976 } 3977} 3978 3979function visible( element ) { 3980 return $.expr.filters.visible( element ) && !$( element ) 3981 .parents( ) 3982 .addBack( ) 3983 .filter( function() { 3984 return $.css( this, "visibility" ) === "hidden"; 3985 } ) 3986 .length; 3987} 3988 3989$.extend( $.expr.pseudos, { 3990 data: function( elem, index, match ) { 3991 return !!$.data( elem, match[ 3 ] ); 3992 }, 3993 focusable: function( element ) { 3994 return focusable( element, !isNaN( $.attr( element, "tabindex" ) ), true ); 3995 }, 3996 discoverable: function( element ) { 3997 return focusable( element, !isNaN( $.attr( element, "tabindex" ) ) ); 3998 }, 3999 tabbable: function( element ) { 4000 var tabIndex = $.attr( element, "tabindex" ), 4001 isTabIndexNaN = isNaN( tabIndex ); 4002 return ( isTabIndexNaN || tabIndex >= 0 ) && focusable( element, !isTabIndexNaN ); 4003 } 4004} ); 4005 4006} )( jQuery ); 4007 4008/** 4009 * @title WET-BOEW Add to calendar 4010 * @overview Create an add to calendar button for an event 4011 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 4012 * @author @ricokola 4013 */ 4014( function( $, wb ) { 4015"use strict"; 4016 4017/* 4018 * Variable and function definitions. 4019 * These are global to the plugin - meaning that they will be initialized once per page, 4020 * not once per instance of plugin on the page. So, this is a good place to define 4021 * variables that are common to all instances of the plugin on a page. 4022 */ 4023var componentName = "wb-addcal", 4024 selector = ".provisional." + componentName, 4025 initEvent = "wb-init." + componentName, 4026 $document = wb.doc, 4027 4028 /** 4029 * @method init 4030 * @param {jQuery Event} event Event that triggered the function call 4031 */ 4032 init = function( event ) { 4033 4034 // Start initialization 4035 // returns DOM object = proceed with init 4036 // returns undefined = do not proceed with init (e.g., already initialized) 4037 var elm = wb.init( event, componentName, selector ), 4038 $elm = $( elm ); 4039 4040 if ( elm ) { 4041 4042 wb.ready( $( elm ), componentName ); 4043 4044 var properties = elm.querySelectorAll( "[property]" ), 4045 event_details = new Object(), 4046 place_details = [], 4047 i, 4048 i_len, 4049 prop_cache, 4050 googleLink, 4051 icsFile, 4052 i18nDict = { 4053 en: { 4054 "addcal-addto": "Add to", 4055 "addcal-calendar": "calendar", 4056 "addcal-other": "Other (Outlook, Apple, etc.)" 4057 }, 4058 fr: { 4059 "addcal-addto": "Ajouter au", 4060 "addcal-calendar": "calendrier", 4061 "addcal-other": "Autre (Outlook, Apple, etc.)" 4062 } 4063 }; 4064 4065 // Initiate dictionary 4066 i18nDict = i18nDict[ $( "html" ).attr( "lang" ) || "en" ]; 4067 i18nDict = { 4068 addto: i18nDict[ "addcal-addto" ], 4069 calendar: i18nDict[ "addcal-calendar" ], 4070 ical: i18nDict[ "addcal-other" ] 4071 }; 4072 4073 // Set date stamp with the date modified 4074 event_details.dtStamp = dtToISOString( $( "time[property='dateModified']" ) ); 4075 4076 i_len = properties.length; 4077 for ( i = 0; i < i_len; i++ ) { 4078 prop_cache = properties[ i ]; 4079 switch ( prop_cache.getAttribute( "property" ) ) { 4080 case "name": 4081 4082 // If the property=name is inside an element with typeof=Place defined 4083 if ( $( prop_cache ).parentsUntil( ( "." + componentName ), "[typeof=Place]" ).length ) { 4084 event_details.placeName = prop_cache.textContent; 4085 } else { 4086 event_details.name = prop_cache.textContent; 4087 } 4088 break; 4089 case "description": 4090 event_details.description = prop_cache.textContent.replace( /(\r\n|\n|\r)/gm, " " ); 4091 break; 4092 case "startDate": 4093 event_details.sDate = dtToISOString( $( "time[property='startDate']", $elm ) ); 4094 break; 4095 case "endDate": 4096 event_details.eDate = dtToISOString( $( "time[property='endDate']", $elm ) ); 4097 break; 4098 case "location": 4099 4100 // If the location doesn't have typeof defined OR has typeof=VirtualLocation without URL inside. 4101 if ( !prop_cache.getAttribute( "typeof" ) || ( prop_cache.getAttribute( "typeof" ) === "VirtualLocation" && !$( prop_cache ).find( "[property=url]" ).length ) ) { 4102 event_details.placeName = prop_cache.textContent; 4103 } 4104 break; 4105 case "streetAddress": 4106 event_details.placeAddress = prop_cache.textContent; 4107 break; 4108 case "addressLocality": 4109 event_details.placeLocality = prop_cache.textContent; 4110 break; 4111 case "addressRegion": 4112 event_details.placeRegion = prop_cache.textContent; 4113 break; 4114 case "postalCode": 4115 event_details.placePostalCode = prop_cache.textContent; 4116 break; 4117 case "url": 4118 4119 // If the property=url is inside a property=location 4120 if ( $( prop_cache ).parentsUntil( ( "." + componentName ), "[property=location]" ).length ) { 4121 event_details.placeName = prop_cache.textContent; 4122 } 4123 break; 4124 } 4125 } 4126 4127 place_details.push( ( event_details.placeName || "" ), ( event_details.placeAddress || "" ), ( event_details.placeLocality || "" ), ( event_details.placeRegion || "" ), ( event_details.placePostalCode || "" ) ); 4128 4129 // Error handling 4130 if ( !event_details.name ) { 4131 throw componentName + ": Event title is missing."; 4132 } else if ( !event_details.sDate ) { 4133 throw componentName + ": Start date is missing."; 4134 } else if ( !event_details.eDate ) { 4135 throw componentName + ": End date is missing."; 4136 } 4137 4138 // Set Unique Identifier (UID) and Date Stamp (DSTAMP) 4139 event_details.uid = window.location.href.replace( /\.|-|\/|:|[G-Zg-z]/g, "" ).toUpperCase().substr( -10 ) + "-" + event_details.sDate + "-" + event_details.dtStamp; 4140 4141 // Set google calendar link 4142 googleLink = encodeURI( "https://www.google.com/calendar/render?action=TEMPLATE" + "&text=" + event_details.name + "&details=" + 4143 event_details.description + "&dates=" + event_details.sDate + "/" + event_details.eDate + "&location=" + place_details.join( " " ) ); 4144 4145 // Set ICS file for Outlook, Apple and other calendars 4146 icsFile = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//WET-BOEW//Add to Calendar v4.0//\n
4146BEGIN:VEVENT\nDTSTAMP:" + event_details.dtStamp + "\nSUMMARY:" + event_details.name + "\nDESCRIPTION:" + event_details.description + "\nUID:" + event_details.uid + "\nDTSTART:" + event_details.sDate + "\nDTEND:" + event_details.eDate + "\nLOCATION:" + place_details.join( " " ) + "\nEND:VEVENT\nEND:VCALENDAR"; 4147 4148 elm.dataset.icsFile = icsFile; 4149 4150 // Create and add details summary to the wb-addcal event and initiate the unordered list 4151 $elm.append( "<details class='max-content " + componentName + "-buttons'><summary>" + i18nDict.addto + " " + i18nDict.calendar + 4152 "</summary><ul class='list-unstyled mrgn-bttm-0'><li><a class='btn btn-link' href='" + googleLink.replace( /'/g, "%27" ) + "' target='_blank' rel='noreferrer noopener'>Google<span class='wb-inv'>" + i18nDict.calendar + "</span></a></li><li><button class='btn btn-link download-ics'>" + i18nDict.ical + 4153 "<span class='wb-inv'>Calendar</span></button></li></ul></details>" ); 4154 } 4155 4156 wb.ready( $( elm ), componentName ); 4157 4158 }; 4159 4160// Convert date to ISO string and formatting for ICS file 4161var dtToISOString = function( date ) { 4162 if ( date.is( "[datetime]" ) ) { 4163 date = date.attr( "datetime" ); 4164 } else { 4165 date = date.text(); 4166 } 4167 4168 return new Date( date ).toISOString().replace( /\..*[0-9]/g, "" ).replace( /-|:|\./g, "" ); 4169}; 4170 4171$document.on( "click", ".download-ics", function( event ) { 4172 var icsFile = $( event.currentTarget ).parentsUntil( "." + componentName ).parent()[ 0 ]; 4173 icsFile = $( icsFile ).attr( "data-ics-file" ); 4174 wb.download( new Blob( [ icsFile ], { type: "text/calendar;charset=utf-8" } ), "evenement-gc-event.ics" ); 4175} ); 4176 4177// Bind the init event of the plugin 4178$document.on( "timerpoke.wb " + initEvent, selector, init ); 4179 4180// Add the timer poke to initialize the plugin 4181wb.add( selector ); 4182 4183} )( jQuery, wb ); 4184 4185/** 4186 * @title WET-BOEW Ajax Fetch [ ajax-fetch ] 4187 * @overview A basic AjaxLoader wrapper for WET-BOEW 4188 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 4189 * @author WET Community 4190 */ 4191( function( $, wb, DOMPurify ) { 4192"use strict"; 4193 4194/* 4195 * Variable and function definitions. 4196 * These are global to the plugin - meaning that they will be initialized once per page, 4197 * not once per instance of plugin on the page. So, this is a good place to define 4198 * variables that are common to all instances of the plugin on a page. 4199 */ 4200var $document = wb.doc; 4201 4202// Event binding 4203$document.on( "ajax-fetch.wb", function( event ) { 4204 4205 // TODO: Remove event.element in future versions 4206 var caller = event.element || event.target, 4207 fetchOpts = event.fetch, 4208 urlParts = fetchOpts.url.split( " " ), 4209 url = urlParts[ 0 ], 4210 urlSubParts = url.split( "#" ), 4211 urlHash = urlSubParts[ 1 ], 4212 selector = urlParts[ 1 ] || ( urlHash ? "#" + urlHash : false ), 4213 fetchData = {}, 4214 callerId, fetchNoCacheURL, urlSub, 4215 fetchNoCache = fetchOpts.nocache, 4216 fetchNoCacheKey = fetchOpts.nocachekey || wb.cacheBustKey || "wbCacheBust"; 4217 4218 // Separate the URL from the filtering criteria 4219 if ( selector ) { 4220 fetchOpts.url = urlParts[ 0 ]; 4221 4222 if ( urlParts[ 1 ] ) { 4223 selector = urlParts.slice( 1 ).join( " " ); 4224 } 4225 } 4226 4227 if ( fetchNoCache ) { 4228 if ( fetchNoCache === "nocache" ) { 4229 fetchNoCacheURL = wb.guid(); 4230 } else { 4231 fetchNoCacheURL = wb.sessionGUID(); 4232 } 4233 fetchNoCacheURL = fetchNoCacheKey + "=" + fetchNoCacheURL; 4234 4235 urlSub = urlSubParts[ 0 ]; 4236 if ( urlSub.indexOf( "?" ) !== -1 ) { 4237 url = urlSub + "&" + fetchNoCacheURL + ( urlHash ? "#" + urlHash : "" ); 4238 } else { 4239 url = urlSub + "?" + fetchNoCacheURL + ( urlHash ? "#" + urlHash : "" ); 4240 } 4241 4242 fetchOpts.url = url; 4243 } 4244 4245 // Filter out any events triggered by descendants 4246 if ( caller === event.target || event.currentTarget === event.target ) { 4247 4248 if ( !caller.id ) { 4249 caller.id = wb.getId(); 4250 } 4251 callerId = caller.id; 4252 4253 // Ensure we don't allow jsonp load 4254 if ( fetchOpts.dataType && fetchOpts.dataType === "jsonp" ) { 4255 fetchOpts.dataType = "json"; 4256 } 4257 if ( fetchOpts.jsonp ) { 4258 fetchOpts.jsonp = false; 4259 } 4260 4261 $.ajax( fetchOpts ) 4262 .done( function( response, status, xhr ) { 4263 var responseType = typeof response; 4264 4265 if ( selector ) { 4266 response = $( "<div>" + response + "</div>" ).find( selector ); 4267 } 4268 4269 fetchData.pointer = $( "<div id='" + wb.getId() + "' data-type='" + responseType + "'></div>" ) 4270 .append( responseType === "string" ? response : "" ); 4271 4272 if ( !xhr.responseJSON ) { 4273 try { 4274 response = $( response ); 4275 } catch ( e ) { 4276 response = DOMPurify.sanitize( xhr.responseText ); 4277 } 4278 } else { 4279 response = xhr.responseText; 4280 } 4281 4282 fetchData.response = response; 4283 fetchData.hasSelector = !!selector; 4284 fetchData.status = status; 4285 fetchData.xhr = xhr; 4286 4287 $( "#" + callerId ).trigger( { 4288 type: "ajax-fetched.wb", 4289 fetch: fetchData 4290 }, this ); 4291 } ) 4292 .fail( function( xhr, status, error ) { 4293 $( "#" + callerId ).trigger( { 4294 type: "ajax-failed.wb", 4295 fetch: { 4296 xhr: xhr, 4297 status: status, 4298 error: error 4299 } 4300 }, this ); 4301 }, this ); 4302 } 4303} ); 4304 4305} )( jQuery, wb, DOMPurify ); 4306 4307/** 4308 * @title WET-BOEW Set background image 4309 * @overview Apply a background image or detects the change in screen width and replace the background image accordingly 4310 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 4311 * @author @namjohn920, @duboisp 4312 */ 4313( function( $, wb ) { 4314"use strict"; 4315 4316/* 4317 * Variable and function definitions. 4318 * These are global to the plugin - meaning that they will be initialized once per page, 4319 * not once per instance of plugin on the page. So, this is a good place to define 4320 * variables that are common to all instances of the plugin on a page. 4321 */ 4322var $document = wb.doc, 4323 $window = wb.win, 4324 componentName = "wb-bgimg", 4325 selector = "[data-bgimg-srcset], [data-bgimg]", 4326 bgViews = {}, 4327 ids = [], 4328 4329 init = function( event ) { 4330 4331 var elm, elmId, 4332 bgImg, bgimgSrcset, bgRawViews, 4333 i, i_len, i_views, 4334 imgSrc, imgSize; 4335 4336 // Start initialization 4337 // returns DOM object = proceed with init 4338 // returns undefined = do not proceed with init (e.g., already initialized) 4339 elm = wb.init( event, componentName, selector ); 4340 4341 if ( elm ) { 4342 4343 // Ensure the feature have an ID. 4344 if ( !elm.id ) { 4345 elm.id = wb.getId(); 4346 } 4347 elmId = elm.id; 4348 4349 // Apply default background image 4350 bgImg = elm.dataset.bgimg; 4351 if ( bgImg ) { 4352 elm.style.backgroundImage = "url(" + bgImg + ")"; 4353 } 4354 4355 // Apply background image set if defined 4356 bgimgSrcset = elm.dataset.bgimgSrcset; 4357 if ( bgimgSrcset ) { 4358 ids.push( elm.id ); 4359 bgRawViews = elm.dataset.bgimgSrcset.split( "," ); 4360 i_len = bgRawViews.length; 4361 bgViews[ elmId ] = []; 4362 4363 for ( i = 0; i < i_len; i++ ) { 4364 i_views = bgRawViews[ i ].trim().split( " " ); 4365 4366 imgSrc = i_views[ 0 ]; 4367 imgSize = i_views[ i_views.length - 1 ]; 4368 4369 imgSize = parseInt( imgSize.substring( 0, imgSize.length - 1 ) ); 4370 bgViews[ elmId ].push( [ imgSrc, imgSize ] ); 4371 } 4372 4373 bgViews[ elmId ].sort( 4374 function( a, b ) { 4375 return a[ 1 ] > b[ 1 ] ? 1 : -1; 4376 } 4377 ); 4378 4379 selectImage(); 4380 4381 // Add the resize listener 4382 $window.on( "resize", selectImage ); 4383 } 4384 4385 // Identify that initialization has completed 4386 wb.ready( $( elm ), componentName ); 4387 } 4388 }, 4389 4390 selectImage = function() { 4391 var screenWidth = window.innerWidth, 4392 optimizedLink = {}, 4393 i, i_len = ids.length, j, 4394 optimizedSize, currentId, currentId_len, 4395 currentInput, 4396 link, elm; 4397 4398 for ( i = 0; i < i_len; i++ ) { 4399 optimizedSize = Infinity; 4400 currentId = bgViews[ ids[ i ] ]; 4401 currentId_len = currentId.length; 4402 4403 for ( j = 0; j < currentId_len; j++ ) { 4404 currentInput = currentId[ j ]; 4405 if ( currentInput[ 1 ] >= screenWidth ) { 4406 if ( optimizedSize > currentInput[ 1 ] ) { 4407 optimizedSize = currentInput[ 1 ]; 4408 optimizedLink[ ids[ i ] ] = currentInput[ 0 ]; 4409 } 4410 } 4411 } 4412 if ( optimizedSize === Infinity ) { 4413 optimizedLink[ ids[ i ] ] = currentId[ currentId_len - 1 ][ 0 ]; 4414 } 4415 } 4416 4417 for ( link in optimizedLink ) { 4418 elm = document.getElementById( link ); 4419 4420 if ( optimizedLink[ link ] === "https://wet-boew.github.io/vocab/wb/utilities#no-image" ) { 4421 elm.style.backgroundImage = "none"; 4422 } else { 4423 elm.style.backgroundImage = "url(" + optimizedLink[ link ] + ")"; 4424 } 4425 } 4426 }; 4427 4428// Bind the init event of the plugin 4429$document.on( "timerpoke.wb wb-init." + componentName, selector, init ); 4430 4431// Add the timer poke to initialize the plugin
4432wb.add( selector ); 4433 4434} )( jQuery, wb ); 4435 4436/** 4437 * @title WET-BOEW Events Calendar 4438 * @overview Dynamically generates a calendar interface for navigating a list of events. 4439 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 4440 * @author WET Community 4441 */ 4442( function( $, window, wb ) { 4443"use strict"; 4444 4445/* 4446 * Variable and function definitions. 4447 * These are global to the plugin - meaning that they will be initialized once per page, 4448 * not once per instance of plugin on the page. So, this is a good place to define 4449 * variables that are common to all instances of the plugin on a page. 4450 */ 4451var componentName = "wb-calevt", 4452 selector = "." + componentName, 4453 componentEventName = componentName + "-cal", 4454 selectorEvent = "." + componentEventName, 4455 initEvent = "wb-init" + selector, 4456 evDetails = "ev-details", 4457 setFocusEvent = "focus", 4458 dataAttr = componentName, 4459 $document = wb.doc, 4460 hiddenClass = "hidden", 4461 4462 /** 4463 * @method init 4464 * @param {jQuery Event} event Event that triggered this handler 4465 */ 4466 init = function( event ) { 4467 4468 // Start initialization 4469 // returns DOM object = proceed with init 4470 // returns undefined = do not proceed with init (e.g., already initialized) 4471 var elm = wb.init( event, componentName, selector ), 4472 $elm; 4473 4474 if ( elm ) { 4475 $elm = $( elm ); 4476 4477 // Load ajax content 4478 $.when.apply( $, $.map( $elm.find( "[data-calevt]" ), getAjax ) ) 4479 .always( function() { 4480 processEvents( $elm ); 4481 4482 // Identify that initialization has completed 4483 wb.ready( $elm, componentName ); 4484 } ); 4485 } 4486 }, 4487 4488 getAjax = function( ajaxContainer ) { 4489 var $ajaxContainer = $( ajaxContainer ), 4490 urls = $ajaxContainer.data( "calevt" ).split( /\s+/ ), 4491 dfd = $.Deferred(), 4492 len = urls.length, 4493 promises = [], 4494 i, appendData; 4495 4496 appendData = function( data ) { 4497 $ajaxContainer.append( $.trim( data ) ); 4498 }; 4499 4500 for ( i = 0; i < len; i += 1 ) { 4501 promises.push( $.get( urls[ i ], appendData, "html" ) ); 4502 } 4503 4504 $.when.apply( $, promises ).always( function() { 4505 dfd.resolve(); 4506 } ); 4507 4508 return dfd.promise(); 4509 }, 4510 4511 processEvents = function( $elm ) { 4512 var settings = $.extend( {}, window[ componentName ], $elm.data( dataAttr ) ), 4513 year, month, events, containerId, $container, 4514 minDate, maxDate, minDateTime, maxDateTime, 4515 currDate = new Date(), 4516 currDateTime = currDate.getTime(); 4517 4518 events = getEvents( $elm ); 4519 containerId = $elm.data( "calevtSrc" ); 4520 $container = $( "#" + containerId ).addClass( componentEventName ); 4521 4522 year = settings.year; 4523 month = settings.month; 4524 4525 if ( $elm.data( "calevtMinDate" ) ) { 4526 minDate = getLocaleDate( $elm.data( "calevtMinDate" ) ); 4527 } 4528 if ( $elm.data( "calevtMaxDate" ) ) { 4529 maxDate = getLocaleDate( $elm.data( "calevtMaxDate" ) ); 4530 } 4531 4532 if ( !minDate || ( events.minDate < minDate ) ) { 4533 minDate = events.minDate; 4534 } 4535 if ( !maxDate || ( events.maxDate > maxDate ) ) { 4536 maxDate = events.maxDate; 4537 } 4538 4539 minDateTime = minDate.getTime(); 4540 maxDateTime = maxDate.getTime(); 4541 4542 if ( !year && minDateTime < currDateTime && currDateTime < maxDateTime ) { 4543 year = currDate.getFullYear(); 4544 } else if ( !year && currDateTime < minDateTime ) { 4545 year = minDate.getFullYear(); 4546 } else if ( !year && maxDateTime < currDateTime ) { 4547 year = maxDate.getFullYear(); 4548 } 4549 4550 if ( !month && minDateTime < currDateTime && currDate.getTime() < maxDateTime ) { 4551 month = currDate.getMonth(); 4552 } else if ( !month && currDateTime < minDateTime ) { 4553 month = minDate.getMonth(); 4554 } else if ( !month && maxDateTime < currDateTime ) { 4555 month = maxDate.getMonth(); 4556 } 4557 4558 wb.calendar.create( $container, { 4559 year: year, 4560 month: month, 4561 minDate: minDate, 4562 maxDate: maxDate, 4563 daysCallback: addEvents, 4564 events: events.list, 4565 $events: $elm 4566 } ); 4567 }, 4568 4569 daysBetween = function( dateLow, dateHigh ) { 4570 4571 // Simplified conversion to date object 4572 var date1 = wb.date.convert( dateLow ), 4573 date2 = wb.date.convert( dateHigh ), 4574 dstAdjust = 0, 4575 oneMinute = 1000 * 60, 4576 oneDay = oneMinute * 60 * 24, 4577 diff; 4578 4579 // Equalize times in case date objects have them 4580 date1.setHours( 0 ); 4581 date1.setMinutes( 0 ); 4582 date1.setSeconds( 0 ); 4583 date2.setHours( 0 ); 4584 date2.setMinutes( 0 ); 4585 date2.setSeconds( 0 ); 4586 4587 // Take care of spans across Daylight Saving Time changes 4588 if ( date2 > date1 ) { 4589 dstAdjust = ( date2.getTimezoneOffset() - date1.getTimezoneOffset() ) * oneMinute; 4590 } else { 4591 dstAdjust = ( date1.getTimezoneOffset() - date2.getTimezoneOffset() ) * oneMinute; 4592 } 4593 diff = Math.abs( date2.getTime() - date1.getTime() ) - dstAdjust; 4594 return Math.ceil( diff / oneDay ); 4595 }, 4596 4597 ///TODO: Review this code 4598 getEvents = function( obj ) { 4599 var directLinking = !( $( obj ).hasClass( "evt-anchor" ) ), 4600 events = { 4601 minDate: null, 4602 maxDate: null, 4603 iCount: 0, 4604 list: [ 4605 { 4606 a: 1 4607 } 4608 ] 4609 }, 4610 objEventsList = obj.find( "ul, ol" ).first(), 4611 objEventsListItems = objEventsList.find( "> li:not(.wb-fltr-out)" ), 4612 iLen = objEventsListItems.length, 4613 dateTimeRegExp = /datetime\s+\{date:\s*(\d+-\d+-\d+)\}/, 4614 i, $event, event, $objTitle, title, link, href, target, 4615 linkId, date, tCollection, tCollectionTemp, strDate1, 4616 strDate2, z, zLen, className, dateClass; 4617 4618 for ( i = 0; i !== iLen; i += 1 ) { 4619 $event = objEventsListItems.eq( i ); 4620 event = $event[ 0 ]; 4621 $objTitle = $event.find( "*:header:first" ); 4622 className = $objTitle.attr( "class" ); 4623 title = $objTitle.text(); 4624 link = $event.find( "a" )[ 0 ]; 4625 href = link.getAttribute( "href" ); 4626 target = link.getAttribute( "target" ); 4627 zLen = 1; 4628 4629 /* 4630 * Modification direct-linking or page-linking 4631 * - added the ability to have class set the behaviour of the links 4632 * - default is to use the link of the item as the event link in the calendar 4633 * - 'evt-anchor' class dynamically generates page anchors on the links it maps to the event 4634 */ 4635 if ( !directLinking ) {
4636 linkId = event.id || wb.getId(); 4637 event.id = linkId; 4638 href = "#" + linkId; 4639 } 4640 4641 date = new Date(); 4642 date.setHours( 0, 0, 0, 0 ); 4643 tCollection = event.getElementsByTagName( "time" ); 4644 4645 /* 4646 * Date spanning capability 4647 * - since there may be some dates that are capable of spanning over months we need to identify them 4648 * the process is see how many time nodes are in the event. 2 nodes will trigger a span 4649 */ 4650 if ( tCollection.length !== 0 ) { 4651 tCollectionTemp = tCollection[ 0 ]; 4652 strDate1 = tCollectionTemp.nodeName.toLowerCase() === "time" ? 4653 tCollectionTemp.getAttribute( "datetime" ).substr( 0, 10 ).split( "-" ) : 4654 tCollectionTemp.className.match( dateTimeRegExp )[ 1 ].substr( 0, 10 ).split( "-" ); 4655 4656 // Convert to zero-based month 4657 strDate1[ 1 ] = strDate1[ 1 ] - 1; 4658 4659 date.setFullYear( strDate1[ 0 ], strDate1[ 1 ], strDate1[ 2 ] ); 4660 4661 if ( tCollection.length !== 1 ) { 4662 4663 // This is a spanning event 4664 tCollectionTemp = tCollection[ 1 ]; 4665 strDate2 = tCollectionTemp.nodeName.toLowerCase() === "time" ? 4666 tCollectionTemp.getAttribute( "datetime" ).substr( 0, 10 ).split( "-" ) : 4667 tCollectionTemp.className.match( dateTimeRegExp )[ 1 ].substr( 0, 10 ).split( "-" ); 4668 4669 // Convert to zero-based month 4670 strDate2[ 1 ] = strDate2[ 1 ] - 1; 4671 4672 zLen += daysBetween( strDate1, strDate2 ); 4673 } 4674 4675 // Now loop in events to load up all the days that it would be on tomorrow.setDate(tomorrow.getDate() + 1); 4676 for ( z = 0; z !== zLen; z += 1 ) { 4677 if ( z !== 0 ) { 4678 date = new Date( date.setDate( date.getDate() + 1 ) ); 4679 } 4680 4681 if ( events.minDate === null || date < events.minDate ) { 4682 events.minDate = date; 4683 } 4684 4685 if ( events.maxDate === null || date > events.maxDate ) { 4686 events.maxDate = date; 4687 } 4688 4689 events.list[ events.iCount ] = { 4690 title: title, 4691 date: new Date( date.getTime() ), 4692 href: href, 4693 target: target 4694 }; 4695 4696 // Add a viewfilter 4697 dateClass = "filter-" + ( date.getFullYear() ) + "-" + 4698 wb.string.pad( date.getMonth() + 1, 2 ); 4699 if ( !className ) { 4700 className = dateClass; 4701 } else if ( className.indexOf( dateClass ) === -1 ) { 4702 className += " " + dateClass; 4703 } 4704 events.iCount += 1; 4705 } 4706 $objTitle.attr( "class", className ); 4707 } 4708 4709 // End of loop through objects/events 4710 } 4711 4712 //Sort events 4713 events.list.sort( function( firstEvent, secondEvent ) { 4714 return firstEvent.date - secondEvent.date; 4715 } ); 4716 4717 return events; 4718 }, 4719 4720 addEvents = function( year, month, $days ) { 4721 var eventsList = this.events, 4722 i, eLen, date, dayIndex, $day, $dayEvents, event, eventMonth; 4723 4724 /* 4725 * Determines for each event, if it occurs in the display month 4726 */ 4727 for ( i = 0, eLen = eventsList.length; i !== eLen; i += 1 ) { 4728 event = eventsList[ i ]; 4729 date = event.date; 4730 4731 if ( date.getFullYear() === year ) { 4732 eventMonth = date.getMonth(); 4733 if ( eventMonth > month ) { 4734 4735 //End the loop if the next event is in a future month because events are sorted chronologically 4736 break; 4737 } else if ( date.getMonth() === month ) { 4738 dayIndex = date.getDate() - 1; 4739 $day = $( $days[ dayIndex ] ); 4740 4741 //Get the appropriate day events if a day link exists 4742 if ( $day.parent().get( 0 ).nodeName !== "A" ) { 4743 $dayEvents = $day.next(); 4744 } else { 4745 $dayEvents = $day.parent().next(); 4746 } 4747 4748 //Create the event list container if it doesn't exist 4749 if ( $dayEvents.length !== 1 ) { 4750 $dayEvents = $( "<ul></ul>" ).insertAfter( $day ); 4751 4752 //Determine the focus based on the day before 4753 if ( dayIndex && $days[ dayIndex - 1 ].parentNode.nodeName === "A" ) { 4754 $day.wrap( "<a class='cal-evt' tabindex='-1'></a>" ); 4755 } else { 4756 $day.wrap( "<a class='cal-evt'></a>" ); 4757 } 4758 $day.parent().attr( "href", "javascript:;" ); 4759 } 4760 4761 //Add the event to the list 4762 $dayEvents.append( "<li><a tabindex='-1' class='cal-evt-lnk' href='" + event.href + "'>" + event.title + "</a></li>" ); 4763 } 4764 } 4765 } 4766 }, 4767 4768 filterEvents = function( year, month ) { 4769 this.find( "li.cal-disp-onshow" ) 4770 .addClass( hiddenClass ) 4771 .has( ":header[class*=filter-" + year + "-" + 4772 wb.string.pad( parseInt( month, 10 ) + 1, 2 ) + "]" ) 4773 .removeClass( hiddenClass ); 4774 }, 4775 4776 showEvents = function() { 4777 $( this ) 4778 .next() 4779 .addClass( evDetails ); 4780 }, 4781 4782 hideEvents = function() { 4783 var $link = $( this ), 4784 $cell = $link.closest( "td" ); 4785 4786 setTimeout( function() { 4787 if ( $cell.find( "a:focus" ).length === 0 ) { 4788 $cell.find( "ul" ) 4789 .removeClass( evDetails ) 4790 .find( "a" ) 4791 .attr( "tabindex", "-1" ); 4792 } 4793 }, 5 ); 4794 }, 4795 4796 getLocaleDate = function( dateString ) { 4797 var date = new Date(), 4798 dateComponents = dateString.split( "-" ); 4799 4800 dateComponents[ 1 ] = dateComponents[ 1 ] - 1; // Convert to zero-based month 4801 date.setFullYear( dateComponents[ 0 ], dateComponents[ 1 ], dateComponents[ 2 ] ); 4802 4803 return date; 4804 }; 4805 4806// Bind the init event of the plugin 4807$document.on( "timerpoke.wb " + initEvent + " wb-redraw" + selector, selector, function( event ) { 4808 4809 var eventType = event.type, 4810 $elm = $( "#" + event.target.id ), 4811 calendarId = event.currentTarget.dataset.calevtSrc; 4812 4813 switch ( eventType ) { 4814 case "timerpoke": 4815 case "wb-init": 4816 init( event ); 4817 break; 4818 4819 case "wb-redraw": 4820 $( "#" + calendarId + " .wb-clndr" ).remove(); 4821 processEvents( $elm ); 4822 $elm.trigger( "wb-updated" + selector ); 4823 break; 4824 } 4825} ); 4826 4827$document.on( "wb-navigate.wb-clndr", selectorEvent, function( event ) { 4828 var lib = event.target.lib, 4829 $calEvent; 4830 4831 if ( lib ) { 4832 $calEvent = lib.$events; 4833 4834 if ( $calEvent ) { 4835 filterEvents.call( $calEvent, event.year, event.month ); 4836 4837 //TODO: Added for backwards compatibility. Remove in previous versions 4838 $calEvent.trigger( "wb-updated" + selector ); 4839 } 4840 } 4841} ); 4842 4843$document.on( "focusin focusout keydown", selectorEvent + " .cal-evt", function( event ) { 4844 var eventType = event.type, 4845 $link; 4846 4847 switch ( eventType ) { 4848 case "focusin": 4849 showEvents.call( event.target ); 4850 break; 4851 case "focusout": 4852 hideEvents.call( event.target ); 4853 break; 4854 case "keydown": 4855 $link = $( event.target ); 4856 if ( ( event.which === 13 || event.which === 32 ) && $link.hasClass( "cal-evt" ) ) { 4857 $( event.target ).next().find( "a:first" ).trigger( setFocusEvent ); 4858 } 4859 break; 4860 } 4861} ); 4862 4863$document.on( "keydown", selectorEvent + " td > ul li", function( event ) { 4864 var $item = $( event.currentTarget ), 4865 $toFocus, $itemParent; 4866 4867 switch ( event.which ) { 4868 case 38: 4869 $toFocus = $item.prev().find( "a" ); 4870 if ( $toFocus.length === 0 ) { 4871 $toFocus = $item.siblings( ":last" ).find( "a" ); 4872 } 4873 $toFocus.trigger( setFocusEvent ); 4874 break; 4875 case 40: 4876 $toFocus = $item.next().find( "a" ); 4877 if ( $toFocus.length === 0 ) { 4878 $toFocus = $item.siblings( ":first" ).find( "a" ); 4879 } 4880 $toFocus.trigger( setFocusEvent ); 4881 break; 4882 case 27: 4883 $itemParent = $item.closest( "td" ).children( "a" ); 4884 $itemParent.trigger( setFocusEvent ); 4885 break; 4886 } 4887} ); 4888 4889$document.on( "focusout", selectorEvent + " td > ul", function( event ) { 4890 hideEvents.call( event.target ); 4891} ); 4892 4893// Add the timer poke to initialize the plugin
4894wb.add( selector ); 4895 4896} )( jQuery, window, wb ); 4897 4898( function( $, DOMPurify, window, document, wb, undef ) { 4899 4900var i18nText, 4901 $document = wb.doc, 4902 selector = ".wb-clndr", 4903 navigateEvent = "wb-navigate" + selector, 4904 inited = false, 4905 currDate = new Date(), 4906 currYear = currDate.getFullYear(), 4907 currMonth = currDate.getMonth(), 4908 defaults = { 4909 year: currYear, 4910 month: currMonth, 4911 minDate: new Date( currYear, 0, 1 ), 4912 maxDate: new Date( currYear, 11, 31 ) 4913 }, 4914 $calBase, 4915 4916 init = function() { 4917 var i18n = wb.i18n, 4918 textWeekDayNames, textMonthNames; 4919 4920 i18nText = { 4921 monthNames: i18n( "mnths" ), 4922 prevMonth: i18n( "prvMnth" ), 4923 nextMonth: i18n( "nxtMnth" ), 4924 goToYear: i18n( "cal-goToYr" ), 4925 goToMonth: i18n( "cal-goToMnth" ), 4926 dayNames: i18n( "days" ), 4927 currDay: i18n( "currDay" ), 4928 format: i18n( "cal-format" ), 4929 calendar: i18n( "cal" ) 4930 }; 4931 4932 textWeekDayNames = i18nText.dayNames; 4933 textMonthNames = i18nText.monthNames; 4934 4935 $calBase = $( "<div class='wb-clndr' role='application' aria-label='" + i18nText.calendar + "'>" + 4936 "<div class='cal-nav'>" + 4937 "<span class='wb-inv current-month' aria-live='polite'></span>" + // Added for screen-readers 4938 "<button type='button' class='btn pull-left cal-month-prev'>" + 4939 "<span class='glyphicon glyphicon-arrow-left' aria-hidden='true'></span>" + 4940 "<span class='wb-inv'>" + i18nText.prevMonth + "<span></span></span>" + 4941 "</button>" + 4942 "<button type='button' class='btn pull-right cal-month-next'>" + 4943 "<span class='glyphicon glyphicon-arrow-right' aria-hidden='true'></span>" + 4944 "<span class='wb-inv'>" + i18nText.nextMonth + "<span></span></span>" + 4945 "</button>" + 4946 "<div class='form-group'>" + 4947 "<label><span class='wb-inv'>" + i18nText.goToYear + "</span><select class='cal-year'></select></label>\n" + 4948 "<label><span class='wb-inv'>" + i18nText.goToMonth + "</span><select class='cal-month'>" + 4949 ( function() { 4950 var months = "", 4951 m; 4952 4953 for ( m = 0; m < 12; m += 1 ) { 4954 months += "<option value='" + m + "'>" + textMonthNames[ m ] + "</option>"; 4955 } 4956 4957 return months; 4958 } )() + 4959 "</select></label>" + 4960 "</div>" + 4961 "</div>" + 4962 "<table>" + 4963 "<thead>" + 4964 "<tr>" + 4965 ( function() { 4966 var days = "", 4967 d; 4968 4969 for ( d = 0; d < 7; d += 1 ) { 4970 days += "<th><abbr title='" + textWeekDayNames[ d ] + "'>" + textWeekDayNames[ d ].substr( 0, 1 ) + "</abbr></th>"; 4971 } 4972 4973 return days; 4974 } )() + 4975 "</tr>" + 4976 "</thead>" + 4977 "<tbody class='cal-days'></tbody>" + 4978 "</table>" + 4979 "</div>" ); 4980 4981 inited = true; 4982 }, 4983 4984 createCalendar = function( $container, settings ) { 4985 var calendar = { 4986 reInit: initCalendar 4987 }, $calendarObj, calendarObj; 4988 4989 if ( !inited ) { 4990 init(); 4991 } 4992 4993 $calendarObj = $calBase.clone(); 4994 calendarObj = $calendarObj.get( 0 ); 4995 4996 calendar.$o = $calendarObj; 4997 calendar.o = calendarObj; 4998 calendarObj.lib = calendar; 4999 5000 $calendarObj.appendTo( $container ); 5001 5002 calendar.reInit( settings ); 5003 5004 return calendar; 5005 }, 5006 5007 initCalendar = function( settings ) { 5008 var years = "", 5009 $calendarObj = this.$o, 5010 $yearField, lastYear, y, defaultsPartial; 5011 5012 settings = settings || {}; 5013 5014 if ( settings.year !== undef && settings.month !== undef ) { 5015 defaultsPartial = { 5016 minDate: new Date( settings.year, 0, 1 ), 5017 maxDate: new Date( settings.year, 11, 31 ) 5018 }; 5019 $.extend( this, defaultsPartial, settings ); 5020 } else { 5021 $.extend( this, defaults ); 5022 } 5023 5024 //Generates the list of years 5025 lastYear = this.maxDate.getFullYear(); 5026 $yearField = $calendarObj.find( ".cal-year" ).empty(); 5027 for ( y = this.minDate.getFullYear(); y <= lastYear; y += 1 ) { 5028 years += "<option value='" + y + "'>" + y + "</option>"; 5029 } 5030 $yearField.append( years ); 5031 5032 $calendarObj 5033 .trigger( { 5034 type: navigateEvent, 5035 year: this.year, 5036 month: this.month, 5037 initEvent: true 5038 } ); 5039 }, 5040 5041 createDays = function( calendar, year, month ) { 5042 var $container = $( calendar ).find( ".cal-days" ), 5043 daysContainer = $container.get( 0 ), 5044 dayCount = 1, 5045 textCurrentDay = i18nText.currDay, 5046 lib = calendar.lib, 5047 minDate = lib.minDate, 5048 maxDate = lib.maxDate, 5049 callback = lib.daysCallback, 5050 row, cell, 5051 date, firstDay, lastDay, currYear, currMonth, currDay, week, day, className, isCurrentDate, isoDate, printDate, breakAt
5051End, days, inRange; 5052 5053 date = new Date( year, month, 1 ); 5054 5055 firstDay = date.getDay(); 5056 date.setMonth( month + 1, 0 ); 5057 lastDay = date.getDate(); 5058 5059 // Get the current date 5060 date = new Date(); 5061 currYear = date.getFullYear(); 5062 currMonth = date.getMonth(); 5063 currDay = date.getDate(); 5064 5065 // Clean all existing rows 5066 $container.empty(); 5067 5068 for ( week = 1; week < 7; week += 1 ) { 5069 row = daysContainer.insertRow(); 5070 5071 for ( day = 0; day < 7; day += 1 ) { 5072 5073 if ( ( week === 1 && day < firstDay ) || ( dayCount > lastDay ) ) { 5074 5075 // Creates empty cells | Cree les cellules vides 5076 cell = row.insertCell(); 5077 cell.classList.add( "cal-empty" ); 5078 cell.textContent = " "; 5079 5080 } else { 5081 5082 // Creates date cells | Cree les cellules de date 5083 isCurrentDate = ( dayCount === currDay && month === currMonth && year === currYear ); 5084 className = "cal-index-" + dayCount + ( isCurrentDate ? " cal-curr-day " : "" ); 5085 5086 date.setFullYear( year, month, dayCount ); 5087 isoDate = date.toLocalISOString().substr( 0, 10 ); 5088 printDate = displayDate( date ) + ( isCurrentDate ? "<span class='wb-inv'>" + textCurrentDay + "</span>" : "" ); 5089 5090 cell = row.insertCell(); 5091 cell.setAttribute( "class", className ); 5092 cell.innerHTML = DOMPurify.sanitize( "<time datetime='" + isoDate + "'>" + printDate + "</time>" ); 5093 5094 if ( dayCount >= lastDay ) { 5095 breakAtEnd = true; 5096 } 5097 5098 dayCount += 1; 5099 } 5100 } 5101 if ( breakAtEnd ) { 5102 break; 5103 } 5104 } 5105 5106 if ( callback ) { 5107 days = $container.find( "time" ); 5108 inRange = {}; 5109 5110 if ( year === minDate.getFullYear() && month === minDate.getMonth() ) { 5111 inRange.min = minDate.getDate() - 1; 5112 } 5113 5114 if ( year === maxDate.getFullYear() && month === maxDate.getMonth() ) { 5115 inRange.max = maxDate.getDate() - 1; 5116 } 5117 5118 callback.call( calendar.lib, year, month, days, inRange ); 5119 } 5120 }, 5121 5122 displayDate = function( date ) { 5123 var textWeekDayNames = i18nText.dayNames, 5124 textMonthNames = i18nText.monthNames; 5125 5126 return i18nText.format.replace( /\{ddd\}|\{d\}|\{M\}|\{Y\}/g, function( match ) { 5127 switch ( match ) { 5128 case "{ddd}": 5129 return textWeekDayNames[ parseInt( date.getDay(), 10 ) ]; 5130 case "{d}": 5131 return parseInt( date.getDate(), 10 ); 5132 case "{M}": 5133 return textMonthNames[ parseInt( date.getMonth(), 10 ) ]; 5134 case "{Y}": 5135 return date.getFullYear(); 5136 } 5137 } ); 5138 }; 5139 5140wb.calendar = { 5141 create: createCalendar 5142}; 5143 5144$document.on( navigateEvent, selector, function( event ) { 5145 var calendarObj = event.currentTarget, 5146 $calendar = $( calendarObj ), 5147 year = event.year, 5148 month = event.month, 5149 lib = calendarObj.lib, 5150 maxYear = lib.maxDate.getFullYear(), 5151 maxMonth = lib.maxDate.getMonth(), 5152 minYear = lib.minDate.getFullYear(), 5153 minMonth = lib.minDate.getMonth(), 5154 $prevArrow = $calendar.find( ".cal-month-prev" ), 5155 $nextArrow = $calendar.find( ".cal-month-next" ), 5156 $monthField = $calendar.find( ".cal-month" ), 5157 disabled = "disabled"; 5158 5159 if ( year !== undef ) { 5160 lib.year = year; 5161 } 5162 5163 if ( month !== undef ) { 5164 lib.month = month; 5165 } 5166 5167 //Update UI 5168 $calendar.find( ".cal-year" ).val( year ); 5169 5170 $monthField.val( month ); 5171 5172 $monthField.children( ":" + disabled ).removeAttr( disabled ); 5173 5174 if ( year < minYear || ( year === minYear && month <= minMonth ) ) { 5175 $prevArrow.attr( disabled, disabled ); 5176 } else { 5177 $prevArrow.removeAttr( disabled ); 5178 } 5179 5180 if ( year > maxYear || ( year === maxYear && month >= maxMonth ) ) { 5181 $nextArrow.attr( disabled, disabled ); 5182 } else { 5183 $nextArrow.removeAttr( disabled ); 5184 } 5185 5186 if ( year === minYear ) { 5187 $monthField.children( ":lt(" + minMonth + ")" ).attr( disabled, disabled ); 5188 } 5189 5190 if ( year === maxYear ) { 5191 $monthField.children( ":gt(" + maxMonth + ")" ).attr( disabled, disabled ); 5192 } 5193 5194 createDays( event.currentTarget, event.year, event.month ); 5195 5196 // Added declaration of current month in aria-live="polite" for screen-readers 5197 if ( !event.initEvent ) { 5198 $calendar.find( ".current-month" ).text( i18nText.monthNames[ month ] + " " + year ); 5199 } 5200} ); 5201 5202$document.on( "change", selector, function( event ) { 5203 var target = event.target, 5204 calendar = event.currentTarget, 5205 year, month; 5206 5207 switch ( target.className ) { 5208 case "cal-year": 5209 year = parseInt( target.value, 10 );
5210 month = calendar.lib.month; 5211 break; 5212 case "cal-month": 5213 year = calendar.lib.year; 5214 month = parseInt( target.value, 10 ); 5215 break; 5216 } 5217 5218 $( calendar ).trigger( { 5219 type: navigateEvent, 5220 year: year, 5221 month: month 5222 } ); 5223} ); 5224 5225$document.on( "click", ".cal-month-prev, .cal-month-next", function( event ) { 5226 var $calendar = $( event.currentTarget ).closest( selector ), 5227 calendar = $calendar.get( 0 ), 5228 className = event.currentTarget.className, 5229 modifier = className.indexOf( "cal-month-prev" ) !== -1 ? -1 : 1, 5230 date = new Date( calendar.lib.year, calendar.lib.month + modifier, 1 ); 5231 5232 $calendar.trigger( { 5233 type: navigateEvent, 5234 year: date.getFullYear(), 5235 month: date.getMonth() 5236 } ); 5237 if ( wb.ie11 ) { 5238 $calendar.trigger( "focusin" ); 5239 } 5240} ); 5241 5242$document.on( "keydown", selector, function( event ) { 5243 var calendar = event.currentTarget, 5244 $days = $( event.currentTarget ).find( ".cal-days" ), 5245 target = event.target, 5246 which = event.which, 5247 lib = calendar.lib, 5248 date = new Date( lib.year, lib.month, 1 ), 5249 minDate = lib.minDate, 5250 maxDate = lib.maxDate, 5251 currentDate = new Date( date ), 5252 navigate = true, 5253 classMatch, isDayLink, lastDay, modifier, day; 5254 5255 if ( !event.altKey && !event.metaKey && which > 32 && which < 41 ) { 5256 5257 classMatch = target.parentNode.className.match( /cal-index-(\d{1,2})/ ); 5258 isDayLink = classMatch !== null; 5259 5260 //Key binding for the entire calendar 5261 switch ( which ) { 5262 5263 //page up 5264 case 33: 5265 date.setDate( minDate.getDate() ); 5266 5267 //page down 5268 /* falls through */ 5269 case 34: 5270 modifier = ( which === 33 ? -1 : 1 ); 5271 5272 if ( event.ctrlKey || event.shiftKey || event.altKey ) { 5273 date.setYear( date.getFullYear() + modifier ); 5274 } else { 5275 date.setMonth( date.getMonth() + modifier ); 5276 } 5277 break; 5278 } 5279 5280 //Key binding for navigating calendar days 5281 if ( isDayLink ) { 5282 day = parseInt( classMatch[ 1 ], 10 ); 5283 date.setMonth( date.getMonth() + 1, 0 ); 5284 lastDay = date.getDate(); 5285 date.setDate( day > lastDay ? lastDay : day ); 5286 5287 switch ( which ) { 5288 5289 // end / home 5290 case 35: 5291 date.setDate( lastDay ); 5292 break; 5293 case 36: 5294 date.setDate( 1 ); 5295 break; 5296 5297 // left / up / right / down arrows 5298 case 37: 5299 date.setDate( day - 1 ); 5300 break; 5301 case 38: 5302 date.setDate( day - 7 ); 5303 break; 5304 case 39: 5305 date.setDate( day + 1 ); 5306 break; 5307 case 40: 5308 date.setDate( day + 7 ); 5309 break; 5310 } 5311 } 5312 5313 if ( date < minDate || date > maxDate ) { 5314 if ( which === 35 ) { 5315 date.setDate( maxDate.getDate() ); 5316 } else if ( which === 36 ) { 5317 date = minDate; 5318 } else { 5319 navigate = false; 5320 } 5321 } 5322 5323 if ( navigate && ( date.getMonth() !== currentDate.getMonth() || date.getFullYear() !== currentDate.getFullYear() ) ) { 5324 $( calendar ).trigger( { 5325 type: navigateEvent, 5326 year: date.getFullYear(), 5327 month: date.getMonth() 5328 } ); 5329 } 5330 5331 if ( isDayLink ) { 5332 $days.find( ".cal-index-" + date.getDate() + " a:first" ).focus(); 5333 } 5334 5335 event.preventDefault(); 5336 return false; 5337 } 5338 5339} ); 5340 5341( function() { 5342 5343 function pad( number ) { 5344 if ( number < 10 ) { 5345 return "0" + number; 5346 } 5347 return number; 5348 } 5349 5350 Date.prototype.toLocalISOString = function() { 5351 var tz = this.getTimezoneOffset(); 5352 if ( tz === 0 ) { 5353 return this.toISOString(); 5354 } 5355 return this.getFullYear() + 5356 "-" + pad( this.getMonth() + 1 ) + 5357 "-" + pad( this.getDate() ) + 5358 "T" + pad( this.getHours() ) + 5359 ":" + pad( this.getMinutes() ) + 5360 ":" + pad( this.getSeconds() ) + 5361 "." + ( this.getMilliseconds() / 1000 ).toFixed( 3 ).slice( 2, 5 ) + 5362 ( tz < 0 ? "+" : "-" ) + 5363 pad( Math.floor( Math.abs( tz / 60 ) ) ) + 5364 ":" + pad( tz % 60 ); 5365 }; 5366 5367 if ( !Date.prototype.toISOString ) { 5368 Date.prototype.toISOString = function() { 5369 return this.getUTCFullYear() + 5370 "-" + pad( this.getUTCMonth() + 1 ) + 5371 "-" + pad( this.getUTCDate() ) + 5372 "T" + pad( this.getUTCHours() ) + 5373 ":" + pad( this.getUTCMinutes() ) + 5374 ":" + pad( this.getUTCSeconds() ) + 5375 "." + ( this.getUTCMilliseconds() / 1000 ).toFixed( 3 ).slice( 2, 5 ) + 5376 "Z"; 5377 }; 5378 } 5379}() ); 5380 5381} )( jQuery, DOMPurify, window, document, wb ); 5382 5383/** 5384 * Web Experience Toolkit (WET) / Boîte à outils de l'expérience Web (BOEW) 5385 * @title Charts and Graph 5386 * @overview Draw charts from an html simple and complex data table 5387 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 5388 * @author @duboisp 5389 * 5390 */ 5391( function( $, window, document, wb ) { 5392"use strict"; 5393 5394/** 5395 * Variable and function definitions. 5396 * These are global to the plugin - meaning that they will be initialized once per page, 5397 * not once per instance of plugin on the page. So, this is a good place to define 5398 * variables that are common to all instances of the plugin on a page. 5399 */
5400var componentName = "wb-charts", 5401 selector = "." + componentName, 5402 initEvent = "wb-init" + selector, 5403 tableParsingEvent = "passiveparse.wb-tableparser", 5404 tableParsingCompleteEvent = "parsecomplete.wb-tableparser", 5405 $document = wb.doc, 5406 i18n, i18nText, 5407 5408 /** 5409 * Main Entry function to create the charts 5410 * @method createCharts 5411 * @param {jQuery DOM element} $elm table element use to create the chart 5412 */ 5413 createCharts = function( $elm ) { 5414 var allSeries = [], 5415 chartslabels = [], 5416 dataSeries = [], 5417 nbBarChart = 0, 5418 $caption = $( "caption", $elm ), 5419 captionHtml = $caption.html() || "", 5420 captionText = $caption.text() || "", 5421 valuePoint = 0, 5422 dataCellUnitRegExp = /[^+\-., 0-9]+[^\-+0-9]*/, 5423 lowestFlotDelta, $imgContainer, $placeHolder, 5424 $wetChartContainer, htmlPlaceHolder, figurehtml, 5425 cellValue, datacolgroupfound, dataGroup, header, 5426 i, iLength, j, jLength, parsedData, rIndex, currVectorOptions, 5427 currentRowGroup, reverseTblParsing, dataGroupVector, 5428 currentDataGroupVector, dataCell, previousDataCell, currDataVector, 5429 pieQuaterFlotSeries, optionFlot, optionsCharts, globalOptions, 5430 defaultsOptions = { 5431 5432 // Flot Global Options 5433 flot: { 5434 prefix: "wb-charts-", 5435 defaults: { 5436 colors: wb.drawColours, 5437 canvas: true, 5438 xaxis: { 5439 ticks: { } 5440 } 5441 }, 5442 line: { }, 5443 area: { 5444 lines: { 5445 show: true, 5446 fill: true 5447 } 5448 }, 5449 bar: { 5450 bars: { 5451 show: true, 5452 barWidth: 1, 5453 align: "center" 5454 } 5455 }, 5456 pie: { 5457 series: { 5458 pie: { 5459 show: true 5460 } 5461 }, 5462 fn: { 5463 "/series/pie/label/formatter": function( label, series ) { 5464 var textlabel; 5465 if ( !optionsCharts.decimal ) { 5466 textlabel = Math.round( series.percent ); 5467 } else { 5468 textlabel = Math.round( series.percent * Math.pow( 10, optionsCharts.decimal ) ); 5469 textlabel = textlabel / Math.pow( 10, optionsCharts.decimal ); 5470 } 5471 5472 if ( optionsCharts.nolegend ) { 5473 5474 // Add the series label 5475 textlabel = label + "<br />" + textlabel; 5476 } 5477 return textlabel + "%"; 5478 } 5479 } 5480 }, 5481 donut: { 5482 base: "pie", 5483 series: { 5484 pie: { 5485 radius: 1, 5486 label: { 5487 show: true, 5488 radius: 1, 5489 threshold: 0.08 5490 }, 5491 tilt: 0.5, 5492 innerRadius: 0.45, 5493 startAngle: 1 5494 } 5495 }, 5496 grid: { 5497 hoverable: true 5498 } 5499 }, 5500 slicelegend: { 5501 base: "pie", 5502 series: { 5503 pie: { 5504 radius: 1, 5505 label: { 5506 radius: 1, 5507 show: true, 5508 threshold: 0.05 5509 }, 5510 combine: { 5511 threshold: 0.05, 5512 color: "#555", 5513 label: i18nText.slicelegend 5514 } 5515 } 5516 }, 5517 fn: { 5518 "/series/pie/label/formatter": function( label ) { 5519 return label; 5520 } 5521 } 5522 } 5523 }, 5524 5525 // Flot Series Options 5526 series: { 5527 prefix: "wb-charts-", 5528 defaults: { }, 5529 line: { }, 5530 area: { 5531 lines: { 5532 show: true, 5533 fill: true 5534 } 5535 }, 5536 bar: { 5537 bars: { 5538 show: true, 5539 barWidth: 1, 5540 align: "center" 5541 } 5542 }, 5543 stacked: { 5544 base: "bar" 5545 } 5546 }, 5547 5548 // Wet-boew Charts Options 5549 charts: { 5550 prefix: "wb-charts-", 5551 defaults: { 5552
5553 // [string] Class name added at the figure element container 5554 graphclass: "wb-graph", 5555 5556 // [boolean] Wrap or not the table in a details/summary elements 5557 noencapsulation: false, 5558 5559 // [number] false means the deepest vector will be used for labelling 5560 labelposition: false, 5561 5562 // [number] false means the deepest vector will be used for calculate the reference 5563 referencevalue: false, 5564 5565 // [boolean] false means to move the legend from inside the charts to next to it 5566 legendinline: false, 5567 5568 // [boolean] true means that the legend will be destroyed and the label for pie chart will include the legend 5569 nolegend: false, 5570 5571 // [number] Literal number of displayed decimal for a pie charts 5572 decimal: 0, 5573 5574 // [number] Provide a default width for the charts that will be rendered 5575 width: $elm.width(), 5576 5577 // [number] Provide a default height for the charts that will be rendered 5578 height: $elm.height(), 5579 5580 // [boolean] Flag for defining if the data table should be read in reverse compared to HTML spec 5581 reversettblparsing: false, 5582 fn: { 5583 "/getcellvalue": function( elem ) { 5584 5585 // Get the number from the data cell, #3267 5586 var cellValue = $.trim( elem.dataset.wbChartsValue || $( elem ).text() ); 5587 return [ 5588 parseFloat( cellValue.replace( /(\d{1,3}(?:(?: |,)\d{3})*)(?:(?:.|,)(\d{1,2}))?$/, function( a, b, c ) { 5589 return b.replace( / |,/g, "" ) + "." + c || "0"; 5590 } ), 10 ), 5591 cellValue.match( dataCellUnitRegExp ) 5592 ]; 5593 } 5594 } 5595 }, 5596 donut: { 5597 decimal: 1 5598 } 5599 } 5600 }; 5601 5602 /** 5603 * A little function to overwrite and add preset into the default options 5604 * 5605 * @method overwriteDefaultsOptions 5606 * @param {string} scopekey - Key that represent the subject of the setting, [flot, charts, series,...] 5607 * @param {json object} target - DefaultOptions that will be overwritten 5608 * @param {json object} object - User defined object for overwriting options 5609 * @return {json object} - Return the new object 5610 */ 5611 function overwriteDefaultsOptions( scopekey, target, object ) { 5612 var cachedObj, key; 5613 5614 cachedObj = object[ scopekey ]; 5615 if ( !cachedObj ) { 5616 return target; 5617 } 5618 for ( key in cachedObj ) { 5619 if ( !Object.prototype.hasOwnProperty.call( cachedObj, key ) ) { 5620 continue; 5621 } 5622 target[ scopekey ][ key ] = cachedObj[ key ]; 5623 } 5624 return target; 5625 } 5626 5627 // User defined options 5628 if ( !window.chartsGraphOpts ) { 5629 globalOptions = window[ componentName ]; 5630 5631 // Global setting 5632 if ( globalOptions ) { 5633 overwriteDefaultsOptions( "flot", defaultsOptions, globalOptions ); 5634 overwriteDefaultsOptions( "series", defaultsOptions, globalOptions ); 5635 overwriteDefaultsOptions( "charts", defaultsOptions, globalOptions ); 5636 } 5637 5638 // Save the setting here in a case of a second graphic on the same page 5639 window.chartsGraphOpts = defaultsOptions; 5640 } 5641 defaultsOptions = window.chartsGraphOpts; 5642 5643 /** 5644 * A little function to ease the web editor life 5645 * 5646 * Apply preset defined by a set of space-separated tokens from a baseline json object and at the same time extend the result by using the HTML5 data attribute 5647 * 5648 * @method applyPreset 5649 * @param {json object} baseline - Base line json object that includes predefined and user defined preset 5650 * @param {jQuery} $elem - Element on which the class attribute will be taken for a set of space-separated tokens 5651 * @param {string} attribute - Name of the HTML5 data attribute for extending the object at the end 5652 * @return {json object} - Return a new object build from the ```baseline``` or ```baseline.default``` object with the preset applied. 5653 */ 5654 function applyPreset( baseline, $elem, attribute ) { 5655 5656 var config = $.extend( true, {}, baseline.defaults || baseline ), 5657 fn = $.extend( true, {}, baseline.defaults && baseline.defaults.fn || { } ), 5658 tokens = $elem.attr( "class" ) || "", 5659 tblTokens, i, iLength, token, tokenLength, 5660 5661 // Prefix used in front of the token 5662 prefix, prefixLength,
5663 preset, key, tblFn, localKey, currObj; 5664 5665 if ( tokens.length ) { 5666 5667 prefix = ( baseline.prefix || "" ); 5668 prefixLength = prefix.length; 5669 5670 // split the set of space-separated tokens 5671 tblTokens = tokens.split( " " ); 5672 5673 for ( i = 0, iLength = tblTokens.length; i !== iLength; i += 1 ) { 5674 5675 // Get the current token 5676 token = tblTokens[ i ]; 5677 tokenLength = token.length; 5678 5679 // Remove the token is used 5680 if ( tokenLength <= prefixLength || token.slice( 0, prefixLength ) !== prefix ) { 5681 continue; 5682 } 5683 token = token.slice( prefixLength, tokenLength ); 5684 5685 preset = baseline[ token ]; 5686 5687 // Apply the preset 5688 if ( preset ) { 5689 if ( preset.base ) { 5690 5691 // Like setting inherited from a parent config 5692 config = $.extend( true, config, baseline[ preset.base ] ); 5693 fn = $.extend( true, fn, baseline[ preset.base ].fn || { } ); 5694 } 5695 config = $.extend( true, config, preset ); 5696 fn = $.extend( true, fn, preset.fn || { } ); 5697 } 5698 } 5699 } 5700 5701 // Extend the config from the element @data attribute 5702 config = $.extend( true, config, wb.getData( $elem, attribute ) ); 5703 5704 // Merge and override the function. 5705 for ( key in fn ) { 5706 if ( !Object.prototype.hasOwnProperty.call( fn, key ) ) { 5707 continue; 5708 } 5709 tblFn = key.split( "/" ); 5710 currObj = config; 5711 for ( i = 0, iLength = tblFn.length - 1; i !== iLength; i += 1 ) { 5712 localKey = tblFn.shift(); 5713 if ( localKey === "" ) { 5714 continue; 5715 } 5716 if ( !currObj[ localKey ] ) { 5717 currObj[ localKey ] = { }; 5718 } 5719 currObj = currObj[ localKey ]; 5720 } 5721 localKey = tblFn.shift(); 5722 currObj[ localKey ] = fn[ key ]; 5723 } 5724 return config; 5725 } 5726 5727 // Apply any preset 5728 optionFlot = applyPreset( defaultsOptions.flot, $elm, "flot" ); 5729 5730 // Apply any preset 5731 optionsCharts = applyPreset( defaultsOptions.charts, $elm, componentName ); 5732 5733 // Fix default width and height in case the table is hidden or too small. 5734 optionsCharts.width = ( optionsCharts.width && optionsCharts.width > 250 ? optionsCharts.width : 250 ); 5735 optionsCharts.height = ( optionsCharts.height && optionsCharts.height > 250 ? optionsCharts.height : 250 ); 5736 5737 /** 5738 * @method getColumnGroupHeaderCalculateSteps 5739 * @param {object} colGroupHead - Column Group Header Object from the table parser 5740 * @param {number} referenceValuePosition - Vector position use as reference for defining the steps, zero based position 5741 */ 5742 function getColumnGroupHeaderCalculateSteps( colGroupHead, referenceValuePosition ) { 5743 5744 // Get the appropriate ticks 5745 var headerCell, i, iLen, 5746 calcStep = 1, 5747 colRefValue, colCurent; 5748 5749 if ( !colGroupHead ) { 5750 5751 // There is an error. Possibly the series are missing a header. 5752 return; 5753 } 5754 5755 colRefValue = colGroupHead.col[ referenceValuePosition ]; 5756 colCurent = colGroupHead.col[ 0 ]; 5757 5758 for ( i = 0, iLen = colRefValue.cell.length; i !== iLen; i += 1 ) { 5759 5760 headerCell = colRefValue.cell[ i ]; 5761 5762 if ( i === 0 || ( i > 0 && colCurent.cell[ i - 1 ].uid !== headerCell.uid ) ) { 5763 5764 if ( headerCell.rowgroup && headerCell.rowgroup.type === 3 ) { 5765 5766 // We only process the first column data group 5767 break; 5768 } 5769 5770 if ( headerCell.type === 1 || headerCell.type === 7 ) { 5771 if ( headerCell.child.length !== 0 ) { 5772 calcStep = calcStep * groupHeaderCalculateStepsRecursive( headerCell, 1 ); 5773 } 5774 } 5775 } 5776 } 5777 5778 return calcStep; 5779 } 5780 5781 /** 5782 * @method getRowGroupHeaderCalculateSteps 5783 * @param {object} rowGroupHead - Row Group Header Object from the table parser 5784 * @param {number} referenceValuePosition - Vector position use as reference for defining the steps, zero based position 5785 * @param {number} dataColgroupStart - Column position where the column data group start 5786 */ 5787 function getRowGroupHeaderCalculateSteps( rowGroupHead, referenceValuePosition, dataColgroupStart ) { 5788 5789 // Find the range of the first data colgroup 5790 var headerCell, i, iLen, 5791 calcStep = 1, 5792 rowRefValueCells = rowGroupHead[ referenceValuePosition ].elem.cells; 5793 5794 for ( i = 0, iLen = rowRefValueCells.length; i !== iLen; i += 1 ) { 5795 5796 headerCell = $( rowRefValueCells[ i ] ).data().tblparser; 5797 5798 if ( headerCell.colgroup && headerCell.colgroup.type === 3 ) { 5799 5800 // We only process the first column data group 5801 break; 5802 } 5803 5804 if ( headerCell.colpos >= dataColgroupStart && ( headerCell.type === 1 || headerCell.type === 7 ) ) { 5805 if ( headerCell.child.length !== 0 ) { 5806 calcStep = calcStep * headerCell.child.length * groupHeaderCalculateStepsRecursive( headerCell, 1 ); 5807 } 5808 } 5809 } 5810 5811 return calcStep; 5812 } 5813 5814 /** 5815 * @method groupHeaderCalculateStepsRecursive 5816 * @param {object} headerCell - Header cell object from the table parser 5817 * @param {number} refValue - Reference Value (Dénominateur) of headerCell 5818 */ 5819 function groupHeaderCalculateStepsRecursive( headerCell, refValue ) { 5820 var childLength = headerCell.child.length, 5821 calcStep = 1, 5822 kIndex, subRefValue, headerCellChild; 5823 5824 if ( childLength === 0 ) { 5825 return calcStep; 5826 } 5827 5828 subRefValue = childLength * refValue; 5829 5830 calcStep = calcStep * subRefValue; 5831 5832 for ( kIndex = 0; kIndex !== childLength; kIndex += 1 ) { 5833 headerCellChild = headerCell.child[ kIndex ]; 5834 if ( headerCellChild.child.length !== 0 ) { 5835 calcStep = calcStep * groupHeaderCalculateStepsRecursive( headerCellChild, subRefValue ); 5836 } 5837 } 5838 return calcStep; 5839 } 5840 5841 /** 5842 * Set the inner step value (divisor) of an header cell and for his child 5843 * 5844 * @method setInnerStepValues 5845 * @param {object} vectorHead - Group Header Object from the table parser 5846 * @param {number} headerLevel - Hierarchical Level of heading 5847 * @param {number} stepsValue - Step Value for the reference value vector 5848 * @param {number} referenceValue - Reference Value Vector ID 5849 * @param {number} dataColgroupStart - Column position where the column data group start 5850 * 5851 */ 5852 function setInnerStepValues( vectorHead, headerLevel, stepsValue, referenceValue, dataColgroupStart ) { 5853 var i, iLength,
5854 headerCell, 5855 cumulativeValue = 0; 5856 5857 for ( i = 0, iLength = vectorHead.cell.length; i !== iLength; i += 1 ) { 5858 headerCell = vectorHead.cell[ i ]; 5859 if ( i !== 0 && headerCell.uid === vectorHead.cell[ i - 1 ].uid || ( dataColgroupStart && headerCell.colpos < dataColgroupStart ) ) { 5860 continue; 5861 } 5862 5863 // Only process the first data group 5864 if ( !reverseTblParsing ) { 5865 if ( headerCell.colgroup && headerCell.colgroup.type === 3 ) { 5866 break; 5867 } 5868 } else { 5869 if ( headerCell.rowgroup && headerCell.rowgroup.type === 3 ) { 5870 break; 5871 } 5872 } 5873 if ( headerCell.child > 0 && headerLevel < referenceValue ) { 5874 headerCell.flotDelta = stepsValue * headerCell.child.length; 5875 } else { 5876 headerCell.flotDelta = stepsValue; 5877 } 5878 if ( headerCell.type === 1 || headerCell.type === 7 ) { 5879 5880 if ( !lowestFlotDelta || headerCell.flotDelta < lowestFlotDelta ) { 5881 lowestFlotDelta = headerCell.flotDelta; 5882 } 5883 headerCell.flotValue = cumulativeValue; 5884 5885 cumulativeValue = cumulativeValue + stepsValue; 5886 5887 if ( headerCell.child.length > 0 ) { 5888 setInnerStepValuesChildRecursive( headerCell, headerLevel, stepsValue, referenceValue ); 5889 } 5890 } 5891 } 5892 } 5893 5894 /** 5895 * Recursize - Set the inner step value (divisor) of an sub header cell 5896 * 5897 * @method setInnerStepValuesChildRecursive 5898 * @param {object} headerCell - Header cell object from the table parser 5899 * @param {number} headerLevel - Hierarchical Level of heading 5900 * @param {number} stepsValue - Specific Step Value applied for current headerCell 5901 * @param {number} referenceValue - Reference Value Vector ID 5902 */ 5903 function setInnerStepValuesChildRecursive( headerCell, headerLevel, stepsValue, referenceValue ) { 5904 var cumulativeValue = 0, 5905 5906 // Step Values for childs header in headerCell 5907 flotDelta, 5908 i, iLength, currentHeaderCellChild; 5909 5910 headerLevel += 1; 5911 cumulativeValue = headerCell.flotValue; 5912 flotDelta = stepsValue / headerCell.child.length; 5913 5914 // Use to calculate the largest width for a bar in a bar chart 5915 if ( !lowestFlotDelta || flotDelta < lowestFlotDelta ) { 5916 lowestFlotDelta = flotDelta; 5917 } 5918 5919 for ( i = 0, iLength = headerCell.child.length; i !== iLength; i += 1 ) { 5920 currentHeaderCellChild = headerCell.child[ i ]; 5921 if ( headerLevel < referenceValue ) { 5922 currentHeaderCellChild.flotDelta = flotDelta * currentHeaderCellChild.child.length; 5923 } else { 5924 currentHeaderCellChild.flotDelta = flotDelta; 5925 } 5926 currentHeaderCellChild.flotValue = cumulativeValue; 5927 if ( currentHeaderCellChild.child.length > 0 ) { 5928 setInnerStepValuesChildRecursive( currentHeaderCellChild, headerLevel, flotDelta, referenceValue ); 5929 } 5930 cumulativeValue = cumulativeValue + flotDelta; 5931 } 5932 } 5933 5934 /** 5935 * Set the header cell step value (flotDelta) for vector that regroup more than one reference 5936 * 5937 * @method setUpperStepValues 5938 * @param {object} vectorHead - Group Header Object from the table parser 5939 * @param {number} referenceValue - Reference Value Vector ID 5940 */ 5941 function setUpperStepValues( vectorHead, referenceValue ) { 5942 var i, k, m, kLen, mLen, 5943 cumulativeValue, 5944 currentCell, 5945 currentCellChild, 5946 currentVectorHead; 5947 5948 // Calculate upper-step for cells that are 5949 // less precise than the reference value vector 5950 for ( i = referenceValue - 1; i !== -1; i -= 1 ) { 5951 currentVectorHead = vectorHead[ i ]; 5952 5953 for ( k = 0, kLen = currentVectorHead.cell.length; k !== kLen; k += 1 ) { 5954 currentCell = currentVectorHead.cell[ k ]; 5955 5956 if ( currentCell.flotDelta || k > 0 && 5957 currentCell.uid === currentVectorHead.cell[ k - 1 ].uid ) { 5958 5959 continue; 5960 } 5961 5962 if ( !( currentCell.type === 1 || currentCell.type === 7 ) ) { 5963 continue; 5964 } 5965 5966 cumulativeValue = 0; 5967 for ( m = 0, mLen = currentCell.child.length; m !== mLen; m += 1 ) { 5968 currentCellChild = currentCell.child[ m ]; 5969 5970 cumulativeValue = currentCellChild.flotDelta; 5971 if ( !currentCell.flotValue ) { 5972 currentCell.flotValue = currentCellChild.flotValue; 5973 } 5974 } 5975 currentCell.flotDelta = cumulativeValue; 5976 } 5977 } 5978 } 5979 5980 /** 5981 * Get labels for a specific vector 5982 * 5983 * @method getLabels 5984 * @param {object} labelVector - Vector Header Object from the table parser 5985 * @param {number} dataColgroupStart - Column position where the column data group start 5986 */ 5987 function getLabels( labelVector, dataColgroupStart ) { 5988 var labels = [], 5989 i, iLen, currentCell; 5990 5991 for ( i = 0, iLen = labelVector.cell.length; i !== iLen; i += 1 ) { 5992 currentCell = labelVector.cell[ i ]; 5993 5994 if ( ( i !== 0 && currentCell.uid === labelVector.cell[ i - 1 ].uid ) || 5995 ( !( currentCell.type === 1 || currentCell.type === 7 ) ) || 5996 ( dataColgroupStart && currentCell.colpos < dataColgroupStart ) ) { 5997 continue; 5998 } 5999 6000 labels.push( [ currentCell.flotValue, $( currentCell.elem ).text() ] ); 6001 } 6002 return labels; 6003 } 6004 6005 /** 6006 * Get the vector that would be used for labelling x-axis 6007 * 6008 * @method getlabelsVectorPosition 6009 * @param {object[]} arrVectorHeaders - Collection of vector headers 6010 */ 6011 function getlabelsVectorPosition( arrVectorHeaders ) { 6012 var labelPosition = optionsCharts.labelposition; 6013 return ( !labelPosition || ( labelPosition && labelPosition > arrVectorHeaders.length ) ? 6014 parsedData.theadRowStack.length : labelPosition ) - 1; 6015 } 6016 6017 /** 6018 * Get the vertical label and set the appropriate header cell x-axis Value 6019 * 6020 * @method verticalLabels 6021 * @param {object} parsedData - Generic object generated by the table parser 6022 */ 6023 function verticalLabels( parsedData ) { 6024 6025 // Get the appropriate ticks 6026 var headerlevel = 0, 6027 labelsVectorPosition, stepsValue, columnReferenceValue; 6028 6029 if ( !reverseTblParsing || ( reverseTblParsing && optionsCharts.referencevalue === false ) ) { 6030 columnReferenceValue = parsedData.colgrouphead.col.length; 6031 } else { 6032 columnReferenceValue = optionsCharts.referencevalue; 6033 } 6034 6035 columnReferenceValue = columnReferenceValue - 1; 6036 6037 stepsValue = getColumnGroupHeaderCalculateSteps( parsedData.colgrouphead, columnReferenceValue ); 6038 6039 if ( !reverseTblParsing ) { 6040 labelsVectorPosition = parsedData.colgrouphead.col.length - 1; 6041 } else { 6042 labelsVectorPosition = getlabelsVectorPosition( parsedData.colgrouphead.col ); 6043 } 6044 6045 headerlevel = columnReferenceValue; 6046 6047 // Calculate inner-step for cells that are more precise than the reference value vector 6048 setInnerStepValues( parsedData.colgrouphead.col[ columnReferenceValue ], headerlevel, stepsValue, columnReferenceValue ); 6049 6050 // Calculate upper-step for cells that are less precise than the reference value vector 6051 setUpperStepValues( parsedData.colgrouphead.col, columnReferenceValue ); 6052 6053 // Get the labelling 6054 return getLabels( parsedData.colgrouphead.col[ labelsVectorPosition ] ); 6055 } 6056 6057 /** 6058 * Get the horizontal label and set the appropriate header cell x-axis Value 6059 * 6060 * @method horizontalLabels 6061 * @param {object} parsedData - Generic object generated by the table parser 6062 */ 6063 function horizontalLabels( parsedData ) { 6064 6065 // Find the range of the first data colgroup 6066 var dataColgroupStart = -1, 6067 headerlevel = 0, 6068 theadRowStack = parsedData.theadRowStack, 6069 i, iLength, labelsVectorPosition, 6070 stepsValue, rowReferenceValue; 6071 6072 if ( !theadRowStack ) { 6073 return; 6074 } 6075 6076 for ( i = 0, iLength = parsedData.colgroup.length; i !== iLength; i += 1 ) { 6077 if ( parsedData.colgroup[ i ].type === 2 ) { 6078 dataColgroupStart = parsedData.colgroup[ i ].start; 6079 break; 6080 } 6081 } 6082 6083 if ( ( !reverseTblParsing && optionsCharts.referencevalue === false ) || reverseTblParsing ) { 6084 rowReferenceValue = theadRowStack.length; 6085 } else { 6086 rowReferenceValue = optionsCharts.referencevalue; 6087 } 6088 6089 rowReferenceValue = rowReferenceValue - 1; 6090 6091 stepsValue = getRowGroupHeaderCalculateSteps( theadRowStack, rowReferenceValue, dataColgroupStart ); 6092 6093 if ( !reverseTblParsing ) { 6094 labelsVectorPosition = getlabelsVectorPosition( theadRowStack ); 6095 } else { 6096 labelsVectorPosition = theadRowStack.length - 1; 6097 } 6098 6099 headerlevel = rowReferenceValue; 6100 6101 // Calculate inner-step for cells that are more precise than the reference value vector 6102 setInnerStepValues( theadRowStack[ rowReferenceValue ], headerlevel, stepsValue, rowReferenceValue, dataColgroupStart ); 6103 6104 // Calculate upper-step for cells that are less precise than the reference value vector 6105 setUpperStepValues( theadRowStack, rowReferenceValue ); 6106 6107 // Get the labelling 6108 return getLabels( theadRowStack[ labelsVectorPosition ], dataColgroupStart ); 6109 6110 } 6111 6112 /** 6113 * Wrap the table into a smart details/summary element 6114 *
6115 * @method wrapTableIntoDetails 6116 */ 6117 function wrapTableIntoDetails() { 6118 var $summary; 6119 6120 if ( !captionHtml.length ) { 6121 return; 6122 } 6123 6124 $summary = $( "<summary>" + captionHtml + i18nText.tableMention + "</summary>" ); 6125 $elm 6126 .wrap( "<details></details>" ) 6127 .before( $summary ); 6128 6129 $summary.trigger( "wb-init.wb-details" ); 6130 } 6131 6132 function createContainer( withDimension ) { 6133 $elm 6134 .wrap( "<figure class='" + optionsCharts.graphclass + "'/>" ) 6135 .before( 6136 6137 // Copy to the inner table caption 6138 ( captionHtml.length ? "<figcaption>" + captionHtml + "</figcaption>" : "" ) + 6139 6140 // Image Container 6141 "<div role='img' aria-label='" + captionText + i18nText.tableFollowing + "'" + 6142 6143 // Add Dimension 6144 ( withDimension ? "style='height:" + optionsCharts.height + 6145 "px; width:" + optionsCharts.width + "px'" : "" ) + "></div>" 6146 ); 6147 6148 return $( "div:eq(0)", $elm.parent() ); 6149 } 6150 6151 // Retrieve the parsed data 6152 parsedData = $elm.data().tblparser; 6153 6154 // Reverse table parsing 6155 reverseTblParsing = optionsCharts.reversettblparsing; 6156 6157 // first data row group 6158 currentRowGroup = parsedData.lstrowgroup[ 0 ]; 6159 6160 if ( optionFlot.series && optionFlot.series.pie ) { 6161 6162 // WET Charts placeholder 6163 $wetChartContainer = createContainer( false ); 6164 6165 // Flot pie chart placeholder 6166 htmlPlaceHolder = "<div style='height:" + optionsCharts.height + 6167 "px; width:" + optionsCharts.width + "px'></div>"; 6168 6169 if ( !reverseTblParsing ) { 6170 6171 // If normal parsing 6172 dataGroup = parsedData.colgroup[ 0 ].type === 1 ? 6173 parsedData.colgroup[ 1 ] : 6174 parsedData.colgroup[ 0 ]; 6175 6176 rIndex = currentRowGroup.row.length - 1; 6177 } else { 6178 6179 // If reverse parsing 6180 dataGroup = currentRowGroup; 6181 rIndex = ( parsedData.colgroup[ 0 ].type === 1 ? 6182 parsedData.colgroup[ 1 ].col.length : 6183 parsedData.colgroup[ 0 ].col.length ) - 1; 6184 } 6185 6186 for ( rIndex; rIndex >= 0; rIndex -= 1 ) { 6187 6188 dataGroupVector = !reverseTblParsing ? dataGroup.col : dataGroup.row; 6189 6190 // For each row or column 6191 for ( i = 0, iLength = dataGroupVector.length; i !== iLength; i += 1 ) { 6192 dataSeries = []; 6193 valuePoint = 0; 6194 currentDataGroupVector = dataGroupVector[ i ]; 6195 6196 // For each cells 6197 for ( j = 0, jLength = currentDataGroupVector.cell.length; j !== jLength; j += 1 ) { 6198 6199 dataCell = currentDataGroupVector.cell[ j ]; 6200 6201 // Skip the column if 6202 if ( reverseTblParsing && dataCell.col.type === 1 ) { 6203 continue; 6204 } 6205 6206 previousDataCell = undefined; 6207 if ( j !== 0 ) { 6208 previousDataCell = currentDataGroupVector.cell[ j - 1 ]; 6209 } 6210 6211 // Verify if the selected cell still in the scope of a data group in his another axes (eg. row/col) 6212 // Verify if we are still in the same datagroup as the previous data cell 6213 if ( ( !reverseTblParsing && ( dataCell.row.type !== 2 || ( previousDataCell && 6214 previousDataCell.rowgroup.uid !== dataCell.rowgroup.uid ) ) ) || 6215 ( reverseTblParsing && ( dataCell.col.type !== 2 ) || ( previousDataCell && 6216 previousDataCell.col.type !== 1 && 6217 previousDataCell.col.groupstruct.uid !== dataCell.col.groupstruct.uid ) ) ) { 6218 break; 6219 } 6220 6221 // Gets the value 6222 header = !reverseTblParsing ? dataCell.row.header : dataCell.col.header; 6223 6224 cellValue = optionsCharts.getcellvalue( !reverseTblParsing ? 6225 currentDataGroupVector.cell[ rIndex ].elem : 6226 currentDataGroupVector.datacell[ rIndex ].elem ); 6227 6228 dataSeries.push( 6229 [ 6230 valuePoint, 6231 typeof cellValue === "object" ? 6232 cellValue[ 0 ] : 6233 cellValue 6234 ] 6235 ); 6236 6237 valuePoint += header[ header.length - 1 ].flotDelta; 6238 6239 break; 6240 } 6241 6242 pieQuaterFlotSeries = { }; 6243 6244 // Get the setting from the associative cell header 6245 dataCell = !reverseTblParsing ? 6246 currentDataGroupVector.cell[ rIndex ] : 6247 currentDataGroupVector.datacell[ rIndex ]; 6248 header = !reverseTblParsing ? 6249 dataCell.col.header : 6250 dataCell.row.header; 6251 header = header[ header.length - 1 ]; 6252 6253 // Apply any preset 6254 pieQuaterFlotSeries = applyPreset( defaultsOptions.series, $( header.elem ), "flot" ); 6255 6256 // Set the data issue from the table 6257 pieQuaterFlotSeries.data = dataSeries; 6258 pieQuaterFlotSeries.label = ( !reverseTblParsing ? 6259 $( currentDataGroupVector.dataheader[ currentDataGroupVector.dataheader.length - 1 ].elem ).text() : 6260 $( currentDataGroupVector.header[ currentDataGroupVector.header.length - 1 ].elem ).text() ); 6261 6262 // Add the series 6263 allSeries.push( pieQuaterFlotSeries ); 6264 } 6265 6266 // Create a sub Figure or use the main one 6267 if ( currentRowGroup.row.length === 1 && 6268 ( currentRowGroup.row[ 0 ].header[ 0 ].elem.innerHTML === captionHtml || 6269 currentRowGroup.row[ 0 ].header.length === 0 ) ) { 6270 6271 $placeHolder = $wetChartContainer; 6272 $placeHolder.css( { 6273 height: optionsCharts.height, 6274 width: optionsCharts.width 6275 } ); 6276 6277 } else { 6278 6279 header = !reverseTblParsing ? dataCell.row.header : dataCell.col.header; 6280 6281 figurehtml = "<figure><figcaption>" + 6282 header[ header.length - 1 ].elem.innerHTML + 6283 "</figcaption>" + htmlPlaceHolder + "</figure>"; 6284 6285 $wetChartContainer.append( $( figurehtml ) ); 6286 6287 $placeHolder = $( "div:last()", $wetChartContainer ); 6288 } 6289 6290 // Create the graphic 6291 $.plot( $placeHolder, allSeries, optionFlot ); 6292 6293 if ( !optionsCharts.legendinline ) { 6294 6295 // Move the legend under the graphic 6296 $( ".legend", $placeHolder ).appendTo( $wetChartContainer ); 6297 } 6298 6299 allSeries = []; 6300 } 6301 6302 if ( optionsCharts.nolegend ) { 6303 6304 // Remove the legend 6305 $( ".legend", $wetChartContainer ).remove(); 6306 } 6307 if ( !optionsCharts.legendinline ) { 6308 6309 // Fix the legend that appear under the graphic 6310 $( ".legend > div", $wetChartContainer ).remove(); 6311 $( ".legend > table", $wetChartContainer ).removeAttr( "style" ).addClass( "font-small" ); 6312 $( ".legend", $placeHolder ).appendTo( $imgContainer ); 6313 } 6314 6315 // Remove any "pieLabel" ids set by the flotPie.js plugin at line #457 6316 $( ".pieLabel" ).removeAttr( "id" ); 6317 6318 if ( !optionsCharts.noencapsulation ) {
6319 wrapTableIntoDetails(); 6320 } 6321 6322 return; 6323 } 6324 6325 if ( !reverseTblParsing ) { 6326 6327 // If normal parsing 6328 dataGroup = currentRowGroup; 6329 rIndex = ( parsedData.colgroup[ 0 ].type === 1 ? 6330 parsedData.colgroup[ 1 ].col.length : 6331 parsedData.colgroup[ 0 ].col.length ) - 1; 6332 chartslabels = horizontalLabels( parsedData ); 6333 } else { 6334 6335 // If reverse parsing 6336 dataGroup = parsedData.colgroup[ 0 ].type === 1 ? 6337 parsedData.colgroup[ 1 ] : 6338 parsedData.colgroup[ 0 ]; 6339 rIndex = currentRowGroup.row.length - 1; 6340 chartslabels = verticalLabels( parsedData ); 6341 } 6342 6343 // Add the labels at the Flot options 6344 optionFlot.xaxis.ticks = chartslabels; 6345 6346 dataGroupVector = !reverseTblParsing ? dataGroup.row : dataGroup.col; 6347 6348 // Count the number of bar charts, 6349 for ( i = 0, iLength = dataGroupVector.length; i !== iLength; i += 1 ) { 6350 currentDataGroupVector = dataGroupVector[ i ]; 6351 currDataVector = currentDataGroupVector.header[ currentDataGroupVector.header.length - 1 ]; 6352 6353 // Apply any preset 6354 currVectorOptions = applyPreset( defaultsOptions.series, $( currDataVector.elem ), "flot" ); 6355 6356 if ( currVectorOptions.bars || ( optionFlot.bars && !currVectorOptions.lines ) ) { 6357 6358 // Count number of bars, this number is use to calculate the bar width. 6359 nbBarChart += 1; 6360 6361 // Set a default setting specially for bar charts 6362 if ( !currVectorOptions.bars ) { 6363 currVectorOptions.bars = { show: true, barWidth: 0.9 }; 6364 } 6365 6366 // Set a default order for orderBars flot plugin 6367 if ( !currVectorOptions.bars.order ) { 6368 currVectorOptions.bars.order = nbBarChart; 6369 } 6370 } 6371 6372 // cache the compiled setting 6373 currDataVector.chartOption = currVectorOptions; 6374 } 6375 6376 // First rowgroup assume is a data row group. 6377 // For all row.... 6378 for ( i = 0, iLength = dataGroupVector.length; i !== iLength; i += 1 ) { 6379 dataSeries = []; 6380 datacolgroupfound = 0; 6381 valuePoint = 0; 6382 currDataVector = dataGroupVector[ i ]; 6383 6384 currVectorOptions = currDataVector.header[ currDataVector.header.length - 1 ].chartOption; 6385 6386 // For each cells 6387 for ( j = 0, jLength = currDataVector.cell.length; j !== jLength; j += 1 ) { 6388 6389 dataCell = currDataVector.cell[ j ]; 6390 6391 if ( datacolgroupfound > 1 && dataCell.col.groupstruct.type !== 2 ) { 6392 break; 6393 } 6394 6395 if ( ( !reverseTblParsing && dataCell.col.groupstruct.type === 2 ) || 6396 ( reverseTblParsing && dataCell.row.rowgroup.type === 2 ) ) { 6397 6398 // Gets the value 6399 header = !reverseTblParsing ? dataCell.col.header : dataCell.row.header; 6400 6401 cellValue = optionsCharts.getcellvalue( dataCell.elem ); 6402 6403 // Add the data point 6404 dataSeries.push( 6405 [ 6406 valuePoint, 6407 typeof cellValue === "object" ? 6408 cellValue[ 0 ] : 6409 cellValue 6410 ] 6411 ); 6412 valuePoint += header[ header.length - 1 ].flotDelta; 6413 datacolgroupfound += 1; 6414 } 6415 } 6416 6417 currVectorOptions.data = dataSeries; 6418 currVectorOptions.label = $( currDataVector.header[ currDataVector.header.length - 1 ].elem ).text(); 6419 6420 if ( currVectorOptions.bars ) { 6421 6422 // Adjust the bars width 6423 currVectorOptions.bars.barWidth = currVectorOptions.bars.barWidth * ( 1 / nbBarChart ); 6424 } 6425 6426 allSeries.push( currVectorOptions ); 6427 6428 } 6429 6430 if ( optionFlot.bars ) { 6431 6432 // Adjust the bars width 6433 optionFlot.bars.barWidth = optionFlot.bars.barWidth * ( 1 / nbBarChart ); 6434 } 6435 6436 // WET Charts Placeholder 6437 $placeHolder = createContainer( true ); 6438 6439 // Maximum width 6440 $placeHolder.css( "width", "100%" ); 6441 6442 // Create the graphic 6443 $.plot( $placeHolder, allSeries, optionFlot ); 6444 6445 if ( !optionsCharts.legendinline ) { 6446 6447 // Move the legend under the graphic 6448 $( ".legend > div", $placeHolder ).remove(); 6449 $( ".legend > table", $placeHolder ).removeAttr( "style" ).addClass( "font-small" ); 6450 $placeHolder.css( "height", "auto" ); 6451 } 6452 if ( optionsCharts.nolegend ) { 6453 6454 // Remove the legend 6455 $( ".legend", $placeHolder ).remove(); 6456 } 6457 6458 if ( !optionsCharts.noencapsulation ) { 6459 wrapTableIntoDetails(); 6460 } 6461 6462 $( "canvas:eq(1)", $placeHolder ).css( "position", "static" ); 6463 $( "canvas:eq(0)", $placeHolder ).css( "width", "100%" ); 6464 6465 $elm.trigger( "wb-updated" + selector ); 6466 }, 6467 6468 /** 6469 * @method init 6470 * @param {jQuery Event} event Event that triggered this handler 6471 */ 6472 init = function( event ) { 6473 6474 // Start initialization 6475 // returns DOM object = proceed with init 6476 // returns undefined = do not proceed with init (e.g., already initialized) 6477 var elm = wb.init( event, componentName, selector ), 6478 settings = window[ componentName ], 6479 elmId, modeJS, deps; 6480 6481 if ( elm ) { 6482 elmId = elm.id; 6483 modeJS = wb.getMode() + ".js"; 6484 deps = [ 6485 "site!deps/jquery.flot" + modeJS, 6486 "site!deps/jquery.flot.pie" + modeJS, 6487 "site!deps/jquery.flot.canvas" + modeJS, 6488 "site!deps/jquery.flot.orderBars" + modeJS, 6489 "site!deps/tableparser" + modeJS 6490 ]; 6491 6492 //TODO: Revisit this in the new plugin structure 6493 if ( settings && settings.plugins ) { 6494 deps = deps.concat( settings.plugins ); 6495 } 6496 6497 // Only initialize the i18nText once 6498 if ( !i18nText ) { 6499 i18n = wb.i18n; 6500 i18nText = { 6501 tableMention: i18n( "hyphen" ) + i18n( "tbl-txt" ), 6502 tableFollowing: i18n( "hyphen" ) + i18n( "tbl-dtls" ),
6503 slicelegend: i18n( "chrt-cmbslc" ) 6504 }; 6505 } 6506 6507 // Load the required dependencies 6508 Modernizr.load( { 6509 6510 // For loading multiple dependencies 6511 load: deps, 6512 complete: function() { 6513 var $elm = $( "#" + elmId ); 6514 6515 // Let's parse the table 6516 $elm.trigger( tableParsingEvent ); 6517 6518 // Identify that initialization has completed 6519 wb.ready( $elm, componentName ); 6520 } 6521 } ); 6522 } 6523 }; 6524 6525// Bind the init event of the plugin 6526$document.on( "timerpoke.wb " + initEvent + " " + tableParsingCompleteEvent, selector, function( event ) { 6527 var eventType = event.type, 6528 elm = event.target; 6529 6530 switch ( eventType ) { 6531 6532 /* 6533 * Init 6534 */ 6535 case "timerpoke": 6536 case "wb-init": 6537 init( event ); 6538 break; 6539 6540 /* 6541 * Data table parsed 6542 */ 6543 case "parsecomplete": 6544 if ( event.currentTarget === elm ) { 6545 createCharts( $( elm ) ); 6546 } 6547 break; 6548 } 6549 6550 /* 6551 * Since we are working with events we want to ensure that we are being passive about our control, 6552 * so returning true allows for events to always continue 6553 */ 6554 return true; 6555} ); 6556 6557// Add the timer poke to initialize the plugin 6558wb.add( selector ); 6559 6560} )( jQuery, window, document, wb ); 6561 6562/** 6563 * @title WET-BOEW Collapsible alerts plugin 6564 * @overview Collapsible alerts (details/summary) 6565 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 6566 * @author WET community 6567 */ 6568( function( $, window, wb ) { 6569"use strict"; 6570 6571/* 6572 * Variable and function definitions. 6573 * These are global to the event - meaning that they will be initialized once per page, 6574 * not once per instance of event on the page. 6575 */ 6576var componentName = "wb-collapsible", 6577 selector = "details.alert", 6578 initEvent = "wb-init." + componentName, 6579 $document = wb.doc, 6580 key, 6581 6582 /** 6583 * @method init 6584 * @param {jQuery Event} event Event that triggered the function call 6585 */ 6586 init = function( event ) { 6587 6588 // Start initialization 6589 // returns DOM object = proceed with init 6590 // returns undefined = do not proceed with init (e.g., already initialized) 6591 var details = wb.init( event, componentName, selector ), 6592 $details; 6593 6594 if ( details ) { 6595 $details = $( details ); 6596 6597 key = "alert-collapsible-state-" + details.getAttribute( "id" ); 6598 6599 try { 6600 if ( localStorage.getItem( key ) ) { 6601 6602 // Set open/closed state for existing localStorage keys 6603 if ( localStorage.getItem( key ) === "open" ) { 6604 details.setAttribute( "open", "open" ); 6605 details.className += " open"; 6606 } else if ( localStorage.getItem( key ) === "closed" ) { 6607 details.removeAttribute( "open" ); 6608 details.className = details.className.replace( " open", "" ); 6609 } 6610 6611 } else { 6612 6613 // Set new localStorage values 6614 if ( details.hasAttribute( "open" ) ) { 6615 localStorage.setItem( key, "open" ); 6616 } else { 6617 localStorage.setItem( key, "closed" ); 6618 } 6619 6620 } 6621 } catch ( e ) { 6622 /* swallow error */} 6623 6624 // Identify that initialization has completed 6625 wb.ready( $details, componentName ); 6626 } 6627 }; 6628 6629// Bind the init event of the plugin 6630$document.on( "timerpoke.wb " + initEvent, selector, init ); 6631 6632// Do not bind events if details polyfill is active 6633if ( Modernizr.details ) { 6634 6635 // Bind the the event handlers of the plugin 6636 $document.on( "click keydown toggle." + componentName, selector + " summary", function( event ) { 6637 var which = event.which, 6638 currentTarget = event.currentTarget, 6639 isClosed, 6640 details; 6641 6642 // Ignore middle/right mouse buttons and wb-toggle enhanced summary elements (except for toggle) 6643 if ( ( !which || which === 1 ) && 6644 ( currentTarget.className.indexOf( "wb-toggle" ) === -1 || 6645 ( event.type === "toggle" && event.namespace === componentName ) ) ) { 6646 6647 details = currentTarget.parentNode; 6648 isClosed = details.getAttribute( "open" ) === null; 6649 key = "alert-collapsible-state-" + details.getAttribute( "id" ); 6650 6651 if ( isClosed ) { 6652 try { 6653 localStorage.setItem( key, "open" ); 6654 } catch ( e ) { 6655 /* swallow error */} 6656 } else { 6657 try { 6658 localStorage.setItem( key, "closed" ); 6659 } catch ( e ) { 6660 /* swallow error */} 6661 } 6662 } else if ( which === 13 || which === 32 ) { 6663 event.preventDefault(); 6664 $( currentTarget ).trigger( "click" ); 6665 } 6666 6667 /* 6668 * Since we are working with events we want to ensure that we are being passive about our control, 6669 * so returning true allows for events to always continue 6670 */ 6671 return true; 6672 } ); 6673} 6674 6675// Add the timer poke to initialize the plugin
6676wb.add( selector ); 6677 6678} )( jQuery, window, wb ); 6679 6680/** 6681 * @title WET-BOEW Country Content 6682 * @overview A basic AjaxLoader wrapper that inserts AJAXed in content based on a visitors country as resolved by freegeoip.app 6683 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 6684 * @author @nschonni 6685 */ 6686( function( $, window, wb ) { 6687"use strict"; 6688 6689/* 6690 * Variable and function definitions. 6691 * These are global to the plugin - meaning that they will be initialized once 6692 * per page, not once per instance of plugin on the page. So, this is a good 6693 * place to define variables that are common to all instances of the plugin on a 6694 * page. 6695 */ 6696var componentName = "wb-ctrycnt", 6697 selector = "[data-ctrycnt]", 6698 initEvent = "wb-init." + componentName, 6699 $document = wb.doc, 6700 6701 /** 6702 * @method init 6703 * @param {jQuery Event} event Event that triggered this handler 6704 */ 6705 init = function( event ) { 6706 6707 // Start initialization 6708 // returns DOM object = proceed with init 6709 // returns undefined = do not proceed with init (e.g., already initialized) 6710 var elm = wb.init( event, componentName, selector ), 6711 $elm, url; 6712 6713 if ( elm ) { 6714 $elm = $( elm ); 6715 url = $elm.data( "ctrycnt" ); 6716 6717 $.when( getCountry() ).then( function( countryCode ) { 6718 6719 if ( countryCode === "" ) { 6720 6721 // Leave default content since we couldn't find the country 6722 return; 6723 } else { 6724 6725 // @TODO: Handle bad country values or any whitelist of countries. 6726 } 6727 6728 url = url.replace( "{country}", countryCode.toLowerCase() ); 6729 6730 $elm.load( url, function() { 6731 6732 // Identify that initialization has completed 6733 wb.ready( $elm, componentName ); 6734 } ); 6735 } ); 6736 } 6737 }, 6738 getCountry = function() { 6739 var dfd = $.Deferred(), 6740 countryCode = localStorage.getItem( "countryCode" ); 6741 6742 // Couldn't find a value in the session 6743 if ( countryCode === null ) { 6744 6745 // From https://github.com/aFarkas/webshim/blob/master/src/shims/geolocation.js#L89-L127 6746 $.ajax( { 6747 url: "https://api.country.is/", 6748 dataType: "json", 6749 cache: true, 6750 success: function( data ) { 6751 if ( data ) { 6752 countryCode = data.country; 6753 try { 6754 localStorage.setItem( "countryCode", countryCode ); 6755 } catch ( error ) { 6756 6757 /* swallow error */ 6758 } 6759 } 6760 6761 dfd.resolve( countryCode ); 6762 }, 6763 error: function() { 6764 dfd.reject( "" ); 6765 } 6766 } ); 6767 } else { 6768 dfd.resolve( countryCode ); 6769 } 6770 6771 return dfd.promise(); 6772 }; 6773 6774// Bind the init event of the plugin 6775$document.on( "timerpoke.wb " + initEvent, selector, init ); 6776 6777// Add the timer poke to initialize the plugin 6778wb.add( selector ); 6779 6780} )( jQuery, window, wb ); 6781 6782/** 6783 * @title WET-BOEW Data Ajax [data-ajax-after], [data-ajax-append], 6784 * [data-ajax-before], [data-ajax-prepend] and [data-ajax-replace] 6785 * @overview A basic AjaxLoader wrapper that inserts AJAXed-in content 6786 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 6787 * @author WET Community 6788 */ 6789( function( $, window, wb ) { 6790"use strict"; 6791 6792/* 6793 * Variable and function definitions. 6794 * These are global to the plugin - meaning that they will be initialized once 6795 * per page, not once per instance of plugin on the page. So, this is a good 6796 * place to define variables that are common to all instances of the plugin on a 6797 * page. 6798 */ 6799var componentName = "wb-data-ajax", 6800 shortName = "wb-ajax", 6801 selectors = [ 6802 "[data-ajax-after]", 6803 "[data-ajax-append]", 6804 "[data-ajax-before]", 6805 "[data-ajax-prepend]", 6806 "[data-ajax-replace]", 6807 "[data-" + shortName + "]" 6808 ], 6809 ajaxTypes = [ 6810 "before", 6811 "replace", 6812 "after", 6813 "append", 6814 "prepend" 6815 ], 6816 selectorsLength = selectors.length, 6817 selector = selectors.join( "," ), 6818 initEvent = "wb-init." + componentName, 6819 updateEvent = "wb-update." + componentName, 6820 contentUpdatedEvent = "wb-contentupdated", 6821 $document = wb.doc, 6822 s, 6823 6824 /** 6825 * @method init 6826 * @param {jQuery Event} event Event that triggered this handler 6827 * @param {string} ajaxType The type of AJAX operation, either after, append, before or replace 6828 */ 6829 init = function( event ) { 6830 6831 // Start initialization 6832 // returns DOM object = proceed with init 6833 // returns undefined = do not proceed with init (e.g., already initialized) 6834 var ajxInfo = getAjxInfo( event.target ), 6835 ajaxType = ajxInfo.type, 6836 elm = wb.init( event, componentName + "-" + ajaxType, selector ); 6837 6838 if ( elm && ajxInfo.url ) { 6839 6840 ajax.call( this, event, ajxInfo ); 6841 6842 // Identify that initialization has completed 6843 wb.ready( $( elm ), componentName, [ ajaxType ] ); 6844 } 6845 }, 6846 6847 ajax = function( event, ajxInfo ) { 6848 var elm = event.target, 6849 $elm = $( elm ), 6850 settings = window[ componentName ], 6851 url, 6852 fetchObj, 6853 urlParts; 6854 6855 if ( !ajxInfo ) { 6856 ajxInfo = getAjxInfo( elm ); 6857 } 6858 url = ajxInfo.url; 6859 fetchObj = { 6860 url: url, 6861 nocache: ajxInfo.nocache, 6862 nocachekey: ajxInfo.nocachekey 6863 }; 6864 6865 // Detect CORS requests. 6866 if ( settings && ( url.substr( 0, 4 ) === "http" || url.substr( 0, 2 ) === "//" ) ) { 6867 urlParts = wb.getUrlParts( url ); 6868 if ( ( wb.pageUrlParts.protocol !== urlParts.protocol || wb.pageUrlParts.host !== urlParts.host ) && ( !Modernizr.cors || settings.forceCorsFallback ) ) { 6869 if ( typeof settings.corsFallback === "function" ) { 6870 fetchObj.dataType = "jsonp"; 6871 fetchObj.jsonp = "callback"; 6872 fetchObj = settings.corsFallback( fetchObj ); 6873 } 6874 } 6875 } 6876 6877 $elm.trigger( { 6878 type: "ajax-fetch.wb", 6879 fetch: fetchObj 6880 } ); 6881 }, 6882 6883 // Get Info and return { "url": "the/ajax/URL", "atype" } 6884 getAjxInfo = function( elm ) { 6885 var ajaxType, 6886 len = ajaxTypes.length, 6887 i, url, dtAttr, nocache, nocachekey; 6888 6889 for ( i = 0; i !== len; i += 1 ) { 6890 ajaxType = ajaxTypes[ i ]; 6891 url = elm.getAttribute( "data-ajax-" + ajaxType ); 6892 if ( url ) { 6893 break; 6894 } 6895 } 6896 6897 if ( !url ) { 6898 dtAttr = wb.getData( $( elm ), shortName ); 6899
6900 // Abort the init when called on an invalid element (related to #8058) 6901 if ( !dtAttr ) { 6902 return {}; 6903 } 6904 6905 ajaxType = dtAttr.type; 6906 if ( ajaxTypes.indexOf( ajaxType ) === -1 ) { 6907 throw "Invalid ajax type"; 6908 } 6909 6910 url = getURL( dtAttr.url, dtAttr.httpref ); 6911 if ( !url ) { 6912 return { "type": ajaxType }; 6913 } 6914 6915 nocache = dtAttr.nocache; 6916 nocachekey = dtAttr.nocachekey; 6917 } 6918 6919 return { 6920 "url": url, 6921 "type": ajaxType, 6922 "nocache": nocache, 6923 "nocachekey": nocachekey 6924 }; 6925 }, 6926 6927 // Return url for conditional display if regexp match http refer 6928 getURL = function( url, referer ) { 6929 var refers, httpRef, regHttpRef, 6930 i, i_len; 6931 6932 if ( referer ) { 6933 if ( !Array.isArray( referer ) ) { 6934 refers = []; 6935 refers.push( referer ); 6936 } else { 6937 refers = referer; 6938 } 6939 6940 httpRef = window.document.referrer; 6941 i_len = refers.length; 6942 for ( i = 0; i !== i_len; i += 1 ) { 6943 regHttpRef = new RegExp( refers[ i ] ); 6944 if ( regHttpRef.test( httpRef ) ) { 6945 if ( Array.isArray( url ) && url.length === i_len ) { 6946 return url[ i ]; 6947 } else { 6948 return url; 6949 } 6950 } 6951 } 6952 } else { 6953 return url; 6954 } 6955 return ""; 6956 }, 6957 6958 ajxFetched = function( elm, fetchObj ) { 6959 var $elm = $( elm ), 6960 ajxInfo = getAjxInfo( elm ), 6961 ajaxType = ajxInfo.type, 6962 content, jQueryCaching, 6963 settings = wb.getData( $( elm ), shortName ) || {}, 6964 doEncode = settings.encode, 6965 hasSelector = fetchObj.hasSelector, 6966 resultString = ""; 6967 6968 // ajax-fetched event 6969 content = fetchObj.response; 6970 if ( content && content.length > 0 ) { 6971 6972 // If the fetched content need to be encoded 6973 if ( doEncode && hasSelector ) { 6974 if ( content.length > 1 ) { 6975 content.each( function( idx, itm ) { 6976 resultString += itm.outerHTML + "\n"; 6977 } ); 6978 6979 content = resultString; 6980 } else { 6981 content = content.html(); 6982 } 6983 6984 content = content.replaceAll( "<", "<" ); 6985 } else if ( doEncode && !hasSelector ) { 6986 content = fetchObj.xhr.responseText.replaceAll( "<", "<" ); 6987 } 6988 6989 //Prevents the force caching of nested resources 6990 jQueryCaching = jQuery.ajaxSettings.cache; 6991 jQuery.ajaxSettings.cache = true; 6992 6993 // "replace" is the only event that doesn't map to a jQuery function 6994 if ( ajaxType === "replace" ) { 6995 $elm.html( content ); 6996 } else { 6997 $elm[ ajaxType ]( content ); 6998 } 6999 7000 //Resets the initial jQuery caching setting 7001 jQuery.ajaxSettings.cache = jQueryCaching; 7002 7003 $elm.trigger( contentUpdatedEvent, { "ajax-type": ajaxType, "content": content } ); 7004 } 7005 }; 7006 7007$document.on( "timerpoke.wb " + initEvent + " " + updateEvent + " ajax-fetched.wb", selector, function( event ) { 7008 var eventTarget = event.target; 7009 7010 switch ( event.type ) { 7011 7012 case "timerpoke": 7013 case "wb-init": 7014 init( event ); 7015 break; 7016 case "wb-update": 7017 ajax( event ); 7018 break; 7019 default: 7020 7021 // Filter out any events triggered by descendants 7022 if ( event.currentTarget === eventTarget ) { 7023 ajxFetched( eventTarget, event.fetch ); 7024 } 7025 } 7026 7027 /* 7028 * Since we are working with events we want to ensure that we are being 7029 * passive about our control, so returning true allows for events to always 7030 * continue 7031 */ 7032 return true; 7033} ); 7034 7035// Re-run WET for elements that have just been loaded if WET is already done initializing 7036$document.on( contentUpdatedEvent, function( event ) { 7037 if ( !wb.isDisabled ) { 7038 let updtElm = event.target; 7039 7040 $( updtElm ) 7041 .find( wb.allSelectors ) 7042 .addClass( "wb-init" ) 7043 .filter( ":not(#" + updtElm.id + " .wb-init .wb-init)" ) 7044 .trigger( "timerpoke.wb" ); 7045 } 7046} ); 7047 7048// Add the timerpoke to initialize the plugin 7049for ( s = 0; s !== selectorsLength; s += 1 ) { 7050 wb.add( selectors[ s ] ); 7051} 7052 7053} )( jQuery, window, wb ); 7054 7055/** 7056 * Web Experience Toolkit (WET) / Boîte à outils de l'expérience Web (BOEW) 7057 * @title Data Fusion Query 7058 * @overview Map a query parameter value into an input value 7059 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7060 * @author @duboisp 7061 * 7062 */ 7063( function( document, $, wb ) { 7064"use strict"; 7065 7066/** 7067 * Variable and function definitions. 7068 * These are global to the plugin - meaning that they will be initialized once per page, 7069 * not once per instance of plugin on the page. So, this is a good place to define 7070 * variables that are common to all instances of the plugin on a page. 7071 */
7072var componentName = "wb-data-fusion-query", 7073 selector = "[data-fusion-query][name]", 7074 initEvent = "wb-init." + componentName, 7075 7076 7077 /** 7078 * @method init 7079 * @param {jQuery Event} event Event that triggered the function call 7080 */ 7081 init = function( event ) { 7082 7083 // Start initialization 7084 // returns DOM object = proceed with init 7085 // returns undefined = do not proceed with init (e.g., already initialized) 7086 var elm = wb.init( event, componentName, selector ), 7087 $elm, 7088 inputName, 7089 queryParamValue; 7090 7091 if ( elm ) { 7092 $elm = $( elm ); 7093 7094 // Retrieve the query parameter value and set it on the input 7095 inputName = $elm.attr( "name" ); 7096 queryParamValue = wb.pageUrlParts.params[ inputName ]; 7097 if ( queryParamValue ) { 7098 $elm.val( queryParamValue.replace( /\+/g, " " ) ); 7099 } 7100 7101 // Identify that initialization has completed 7102 wb.ready( $elm, componentName ); 7103 } 7104 }; 7105 7106// Bind the init event of the plugin 7107wb.doc.on( "timerpoke.wb " + initEvent, selector, init ); 7108 7109// Add the timer poke to initialize the plugin 7110wb.add( selector ); 7111 7112} )( document, jQuery, wb ); 7113 7114/** 7115 * @title WET-BOEW Data InView 7116 * @overview A simplified data-attribute driven plugin that responds to moving in and out of the viewport. 7117 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7118 * @author WET Community 7119 */ 7120( function( $, window, wb ) { 7121"use strict"; 7122 7123/* 7124 * Variable and function definitions. 7125 * These are global to the plugin - meaning that they will be initialized once per page, 7126 * not once per instance of plugin on the page. So, this is a good place to define 7127 * variables that are common to all instances of the plugin on a page. 7128 */ 7129var componentName = "wb-inview", 7130 selector = "." + componentName, 7131 initEvent = "wb-init" + selector, 7132 scrollEvent = "scroll" + selector, 7133 $document = wb.doc, 7134 $window = wb.win, 7135 $elms = $(), 7136 7137 /** 7138 * @method init 7139 * @param {jQuery Event} event Event that triggered this handler 7140 */ 7141 init = function( event ) { 7142 7143 // Start initialization 7144 // returns DOM object = proceed with init 7145 // returns undefined = do not proceed with init (e.g., already initialized) 7146 var elm = wb.init( event, componentName, selector ), 7147 $elm; 7148 7149 if ( elm ) { 7150 $elm = $( elm ); 7151 $elms = $elms.add( $elm ); 7152 7153 // Allow other plugins to run first 7154 setTimeout( function() { 7155 onInView( $elm ); 7156 7157 // Identify that initialization has completed 7158 wb.ready( $elm, componentName ); 7159 }, 1 ); 7160 } 7161 }, 7162 7163 /** 7164 * @method onInView 7165 * @param {jQuery DOM element} $elm The plugin element 7166 */ 7167 onInView = function( $elm ) { 7168 var elementWidth = $elm.outerWidth(), 7169 elementHeight = $elm.outerHeight(), 7170 scrollTop = $window.scrollTop(), 7171 scrollBottom = scrollTop + $window.height(), 7172 scrollRight = $window.scrollLeft() + elementWidth, 7173 x1 = $elm.offset().left, 7174 x2 = x1 + elementWidth, 7175 y1 = $elm.offset().top, 7176 y2 = y1 + elementHeight, 7177 oldViewState = $elm.attr( "data-inviewstate" ), 7178 inView = ( scrollBottom < y1 || scrollTop > y2 ) || ( scrollRight < x1 || scrollRight > x2 ), 7179 7180 // this is a bit of a play on true/false to get the desired effect. In short this variable depicts 7181 // the view state of the element 7182 // all - the whole element is in the viewport 7183 // partial - part of the element is in the viewport 7184 // none - no part of the element is in the viewport 7185 viewState = ( scrollBottom > y2 && scrollTop < y1 ) ? "all" : inView ? "none" : "partial", 7186 $dataInView = $( "#" + $elm.attr( "data-inview" ) ), 7187 show; 7188 7189 // Remove any element that no longer exists in the DOM 7190 if ( elementWidth === 0 || elementHeight === 0 ) { 7191 $elms = $elms.not( $elm ); 7192 $dataInView.addClass( "user-closed" ); 7193 $dataInView.trigger( { 7194 type: ( "close" ), 7195 namespace: "wb-overlay", 7196 noFocus: true 7197 } ); 7198 7199 return; 7200 } 7201 7202 // Link the overlay close button to the dismiss action if the inview content is dismissable 7203 if ( $elm.hasClass( "wb-dismissable" ) ) { 7204 if ( $dataInView.hasClass( "wb-overlay" ) ) { 7205 $dataInView.children( ".overlay-close" ).on( "click", function( event ) { 7206 var which = event.which; 7207 7208 // Ignore middle/right mouse buttons 7209 if ( !which || which === 1 ) { 7210 $elm.parent().siblings( ".content-dismiss" ).trigger( "click" ); 7211 } 7212 } ); 7213 } 7214 } 7215 7216 // Only if the view state has changed 7217 if ( viewState !== oldViewState ) { 7218
7219 // Show on "partial"/"none" (default) or just "none" (requires "show-none" class) 7220 show = inView || ( $elm.hasClass( "show-none" ) ? false : viewState === "partial" ); 7221 7222 $elm.attr( "data-inviewstate", viewState ); 7223 7224 if ( $dataInView.length !== 0 ) { 7225 7226 // Keep closed if the user closed the inView result 7227 if ( !$dataInView.hasClass( "user-closed" ) ) { 7228 if ( $dataInView.hasClass( "wb-overlay" ) ) { 7229 if ( !oldViewState ) { 7230 $dataInView.addClass( "outside-off" ); 7231 } 7232 $dataInView.trigger( { 7233 type: ( show ? "open" : "close" ), 7234 namespace: "wb-overlay", 7235 noFocus: true 7236 } ); 7237 } else { 7238 if ( !$dataInView.attr( "data-hasPlayed" ) ) { 7239 $dataInView.toggleClass( "in", !show ); 7240 if ( !$dataInView.hasClass( "in-only" ) ) { 7241 $dataInView.toggleClass( "out", show ); 7242 } 7243 } 7244 if ( wb.isReady && viewState === "all" && $dataInView.hasClass( "in-only" ) ) { 7245 $dataInView.attr( "data-hasPlayed", "true" ); 7246 } 7247 } 7248 } 7249 } 7250 7251 // Trigger an event on the element identifying that the view state has changed 7252 // (e.g., "all.wb-inview", "partial.wb-inview", "none.wb-inview") 7253 $elm.trigger( viewState + selector ); 7254 } 7255 }; 7256 7257// Bind the init event of the plugin 7258$document.on( "timerpoke.wb " + initEvent + " " + scrollEvent, selector, function( event ) { 7259 var eventTarget = event.target, 7260 eventType = event.type; 7261 7262 switch ( eventType ) { 7263 case "timerpoke": 7264 case "wb-init": 7265 init( event ); 7266 break; 7267 7268 case "scroll": 7269 7270 // Filter out any events triggered by descendants 7271 if ( event.currentTarget === eventTarget ) { 7272 onInView( $( eventTarget ) ); 7273 } 7274 break; 7275 } 7276 7277 /* 7278 * Since we are working with events we want to ensure that we are being passive about our control, 7279 * so returning true allows for events to always continue 7280 */ 7281 return true; 7282} ); 7283 7284$window.on( "scroll scrollstop", function() { 7285 $elms.trigger( scrollEvent ); 7286} ); 7287 7288$document.on( "txt-rsz.wb win-rsz-width.wb win-rsz-height.wb", function() { 7289 $elms.trigger( scrollEvent ); 7290} ); 7291 7292$document.on( "refresh.wb", function() { 7293 $elms.each( function() { 7294 onInView( $( this ) ); 7295 } ); 7296} ); 7297 7298// Add the timer poke to initialize the plugin 7299wb.add( selector ); 7300 7301} )( jQuery, window, wb ); 7302 7303/** 7304 * @title WET-BOEW Data Picture 7305 * @overview Event driven port of the Picturefill library: https://github.com/scottjehl/picturefill 7306 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7307 * @author @patheard 7308 */ 7309( function( $, window, wb ) { 7310"use strict"; 7311 7312/* 7313 * Variable and function definitions. 7314 * These are global to the plugin - meaning that they will be initialized once per page, 7315 * not once per instance of plugin on the page. So, this is a good place to define 7316 * variables that are common to all instances of the plugin on a page. 7317 */ 7318var imgClass, 7319 componentName = "wb-pic", 7320 selector = "[data-pic]", 7321 initEvent = "wb-init." + componentName, 7322 picturefillEvent = "picfill." + componentName, 7323 $document = wb.doc, 7324 7325 /** 7326 * @method init 7327 * @param {jQuery Event} event Event that triggered this handler 7328 */ 7329 init = function( event ) { 7330 7331 // Start initialization 7332 // returns DOM object = proceed with init 7333 // returns undefined = do not proceed with init (e.g., already initialized) 7334 var elm = wb.init( event, componentName, selector ), 7335 $elm; 7336 7337 if ( elm ) { 7338 $elm = $( elm ); 7339 7340 // Store the class attribute of the plugin element. It 7341 // will be added to the image created by the plugin. 7342 imgClass = $elm.data( "class" ) || ""; 7343 7344 $elm.trigger( picturefillEvent ); 7345 7346 // Identify that initialization has completed 7347 wb.ready( $elm, componentName ); 7348 } 7349 }, 7350 7351 /** 7352 * Updates the image displayed according to media queries. 7353 * This is the logic ported from Picturefill. 7354 * @method picturefill 7355 * @param {DOM element} elm The element containing the images to be updated 7356 */ 7357 picturefill = function( elm ) { 7358 var matches = [], 7359 img = elm.getElementsByTagName( "img" )[ 0 ], 7360 sources = elm.getElementsByTagName( "span" ), 7361 i, len, matchedElm, media; 7362 7363 // Loop over the data-media elements and find matching media queries 7364 for ( i = 0, len = sources.length; i !== len; i += 1 ) { 7365 media = sources[ i ].getAttribute( "data-media" ); 7366 7367 // Check if the media query matches or if no media query is defined. 7368 // Vérifie si le media query correspond ou si aucun media query n'est défini. 7369 if ( !media || window.matchMedia( media ).matches ) { 7370 matches.push( sources[ i ] ); 7371 } 7372 } 7373 7374 // If a media query match was found, add the image to the page 7375 if ( matches.length !== 0 ) { 7376 matchedElm = matches.pop(); 7377 if ( !img ) { 7378 img = $document[ 0 ].createElement( "img" ); 7379 img.alt = elm.getAttribute( "data-alt" ); 7380 img.className = imgClass; 7381 } 7382 img.src = matchedElm.getAttribute( "data-src" ); 7383 matchedElm.appendChild( img ); 7384 7385 // No match and an image exists: delete it 7386 } else if ( img ) { 7387 img.parentNode.removeChild( img ); 7388 } 7389 7390 // Identify that the picture has been updated 7391 $( elm ).trigger( "wb-updated." + componentName ); 7392 }; 7393 7394// Bind the init event of the plugin 7395$document.on( "timerpoke.wb " + initEvent + " " + picturefillEvent, selector, function( event ) { 7396 var eventTarget = event.target, 7397 eventType = event.type; 7398 7399 switch ( eventType ) { 7400 case "timerpoke": 7401 case "wb-init": 7402 init( event ); 7403 break; 7404 7405 case "picfill": 7406 7407 // Filter out any events triggered by descendants 7408 if ( event.currentTarget === eventTarget ) { 7409 picturefill( eventTarget ); 7410 } 7411 break; 7412 } 7413} ); 7414 7415// Handles window resize so images can be updated as new media queries match 7416$document.on( "txt-rsz.wb win-rsz-width.wb win-rsz-height.wb", function() { 7417 $( selector ).trigger( picturefillEvent ); 7418} ); 7419 7420// Add the timer poke to initialize the plugin
7421wb.add( selector ); 7422 7423} )( jQuery, window, wb ); 7424 7425/** 7426 * @title WET-BOEW Details closed on small screen 7427 * @overview Closes details on defined viewport and down if they were not engaged, default is small 7428 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7429 * @author @thomasgohard 7430 */ 7431( function( $, wb ) { 7432"use strict"; 7433 7434/* 7435* Variable and function definitions. 7436* These are global to the plugin - meaning that they will be initialized once per page, 7437* not once per instance of plugin on the page. So, this is a good place to define 7438* variables that are common to all instances of the plugin on a page. 7439*/ 7440var componentName = "wb-details-close", 7441 selector = ".provisional." + componentName, 7442 initEvent = "wb-init" + selector, 7443 $document = wb.doc, 7444 views = [ "xxs", "xs", "sm", "md", "lg", "xl" ], 7445 viewsClass = [ "xxsmallview", "xsmallview", "smallview", "mediumview", "largeview", "xlargeview" ], 7446 breakpoint, 7447 7448 /** 7449 * @method init 7450 * @param {jQuery Event} event Event that triggered the function call 7451 */ 7452 init = function( event ) { 7453 7454 // Start initialization 7455 // returns DOM object = proceed with init 7456 // returns undefined = do not proceed with init (e.g., already initialized) 7457 var elm = wb.init( event, componentName, selector ), 7458 $elm, i; 7459 7460 if ( elm ) { 7461 $elm = $( elm ); 7462 7463 // Get the plugin JSON configuration set on attribute data-wb-details-close 7464 // Will define one set settings for all .wb-details-close on the page 7465 breakpoint = $elm.data( "breakpoint" ) || "sm"; 7466 7467 // reset breakpoint if config is passed 7468 if ( views.length === viewsClass.length ) { 7469 i = views.indexOf( breakpoint ); 7470 viewsClass = viewsClass.slice( 0, i + 1 ); 7471 } 7472 7473 hideOnBreakpoint(); 7474 7475 // Identify that initialization has completed 7476 wb.ready( $elm, componentName ); 7477 } 7478 }, 7479 7480 /** 7481 * Toggle details depending on breakpoint 7482 * @method hideOnBreakpoint 7483 * @param {jQuery DOM element | jQuery Event} $elm Element targetted by this plugin, which is the details 7484 */ 7485 hideOnBreakpoint = function() { 7486 var $elm = $( selector ), 7487 viewsSelector = "html." + viewsClass.join( ", html." ); 7488 7489 // If within the targetted views, keep details closed 7490 if ( $( viewsSelector ).length ) { 7491 $elm.removeAttr( "open" ); 7492 } else { 7493 7494 // If not, keep opened 7495 $elm.attr( "open", "" ); 7496 } 7497 }; 7498 7499// Bind the init event of the plugin 7500$document.on( "timerpoke.wb " + initEvent, selector, init ); 7501 7502// Add the timer poke to initialize the plugin 7503wb.add( selector ); 7504 7505} )( jQuery, wb ); 7506 7507/** 7508 * @title WET-BOEW Dismissable content plugin 7509 * @overview Enables content to be dismissed 7510 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7511 * @author @thomasgohard 7512 */ 7513( function( $, window, wb ) { 7514"use strict"; 7515 7516/* 7517 * Variable and function definitions. 7518 * These are global to the event - meaning that they will be initialized once per page, 7519 * not once per instance of event on the page. 7520 */ 7521var componentName = "wb-dismissable", 7522 selector = "." + componentName, 7523 initEvent = "wb-init." + componentName, 7524 containerClass = "wb-dismissable-container", 7525 wrapperClass = "wb-dismissable-wrapper", 7526 dismissClass = "content-dismiss", 7527 idKey = "dismissable-item-id", 7528 $document = wb.doc, 7529 i18n, i18nText, 7530 7531 /** 7532 * @method init 7533 * @param {jQuery Event} event Event that triggered the function call 7534 */ 7535 init = function( event ) { 7536 7537 // Start initialization 7538 // returns DOM object = proceed with init 7539 // returns undefined = do not proceed with init (e.g., already initialized) 7540 var elm = wb.init( event, componentName, selector ), 7541 itemId, dismissedState, contentContainer, contentWrapper, dismissButton; 7542 7543 if ( elm ) { 7544 7545 // Only initialize the i18nText once 7546 if ( !i18nText ) { 7547 i18n = wb.i18n; 7548 i18nText = { 7549 dismiss: i18n( "dismiss" ) 7550 }; 7551 } 7552 7553 // Give the dismissable element a unique ID 7554 if ( elm.hasAttribute( "id" ) ) { 7555 itemId = elm.getAttribute( "id" ); 7556 7557 // Ignore an ID assigned by wb 7558 if ( itemId.indexOf( "wb-auto-" ) === 0 ) { 7559 itemId = undefined; 7560 } 7561 } 7562 if ( itemId === undefined ) { 7563 itemId = wb.hashString( wb.stripWhitespace( elm.innerHTML ) ); 7564 } 7565 7566 dismissedState = getDismissedState( itemId ); 7567 7568 if ( dismissedState === "true" ) { 7569 7570 // Remove the element if it has been dismissed 7571 if ( elm.parentNode ) { 7572 elm.parentNode.removeChild( elm ); 7573 } 7574 } else { 7575 $( elm ).wrap( "<div class='" + wrapperClass + "'>" ); 7576 contentWrapper = elm.parentNode; 7577 7578 $( contentWrapper ).wrap( "<div class='" + containerClass + "'>" ); 7579 contentContainer = contentWrapper.parentNode; 7580
7581 dismissButton = "<button type='button' class='mfp-close " + dismissClass + 7582 "' title='" + i18nText.dismiss + "'>×<span class='wb-inv'> " + 7583 i18nText.dismiss + "</span></button>"; 7584 $( contentContainer ).append( dismissButton ); 7585 7586 contentContainer.setAttribute( "data-" + idKey, itemId ); 7587 } 7588 7589 // Identify that initialization has completed 7590 wb.ready( $document, componentName ); 7591 } 7592 }, 7593 7594 getDismissedState = function( id ) { 7595 var dismissState = localStorage.getItem( id ); 7596 7597 if ( dismissState === null ) { 7598 return false; 7599 } 7600 7601 return dismissState; 7602 }, 7603 7604 dismissContent = function( elm ) { 7605 localStorage.setItem( elm.getAttribute( "data-" + idKey ), true ); 7606 elm.parentNode.removeChild( elm ); 7607 $document.trigger( "refresh.wb" ); 7608 }; 7609 7610// Bind the init event of the plugin 7611$document.on( "timerpoke.wb " + initEvent, selector, init ); 7612 7613// Handler for clicking on the dismiss button 7614$document.on( "click", "." + dismissClass, function( event ) { 7615 var elm = event.currentTarget, 7616 which = event.which; 7617 7618 // Ignore middle/right mouse buttons 7619 if ( !which || which === 1 ) { 7620 dismissContent( elm.parentNode ); 7621 } 7622} ); 7623 7624// Add the timer poke to initialize the plugin 7625wb.add( selector ); 7626 7627} )( jQuery, window, wb ); 7628 7629/** 7630 * @title eqht 7631 * @overview Provide ability to have equal height containers and nested containers inside a WET-BOEW grid 7632 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7633 * @author @luc-bertrand-hrsdc 7634 */ 7635( function( $, window, wb ) { 7636"use strict"; 7637 7638/* 7639 * Variable and function definitions. 7640 * These are global to the plugin - meaning that they will be initialized once per page, 7641 * not once per instance of plugin on the page. So, this is a good place to define 7642 * variables that are common to all instances of the plugin on a page. 7643 */ 7644var componentName = "wb-eqht-grd", 7645 selector = "." + componentName + " .eqht-trgt", 7646 initEvent = "wb-init" + selector, 7647 $document = wb.doc, 7648 7649 /** 7650 * @method init 7651 * @param {jQuery Event} event Event that triggered the function call 7652 */ 7653 init = function( event ) { 7654 7655 // Start initialization 7656 // returns DOM object = proceed with init 7657 // returns undefined = do not proceed with init (e.g., already initialized) 7658 var elm = wb.init( event, componentName, selector ), 7659 $elm, 7660 $eqhtParents; 7661 7662 if ( elm ) { 7663 $elm = $( elm ); 7664 $elm.addClass( "hght-inhrt" ); 7665 $eqhtParents = $elm.parentsUntil( "[class*='" + componentName + "']" ); 7666 $eqhtParents.addClass( "hght-inhrt" ); 7667 7668 // Identify that initialization has completed 7669 wb.ready( $elm, componentName ); 7670 } 7671 }; 7672 7673// Bind the init event of the plugin 7674$document.on( "timerpoke.wb " + initEvent, selector, init ); 7675 7676// Add the timer poke to initialize the plugin 7677wb.add( selector ); 7678 7679} )( jQuery, window, wb ); 7680 7681/** 7682 * @title WET-BOEW Responsive equal height 7683 * @overview Sets the same height for all elements in a container that are rendered on the same baseline (row). Adapted from https://codepen.io/micahgodbolt/pen/FgqLc. 7684 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7685 * @author @thomasgohard 7686 */ 7687( function( $, window, wb ) { 7688"use strict"; 7689 7690/* 7691 * Variable and function definitions. 7692 * These are global to the plugin - meaning that they will be initialized once per page, 7693 * not once per instance of plugin on the page. So, this is a good place to define 7694 * variables that are common to all instances of the plugin on a page. 7695 */ 7696var componentName = "wb-eqht", 7697 selector = "." + componentName, 7698 $document = wb.doc, 7699 eventTimerpoke = "timerpoke.wb", 7700 initEvent = "wb-init" + selector, 7701 vAlignCSS = "vertical-align", 7702 vAlignDefault = "top", 7703 contentUpdatedEvent = "wb-contentupdated", 7704 minHeightCSS = "min-height", 7705 minHeightDefault = "0", 7706 cssValueSeparator = ":", 7707 cssPropertySeparator = ";", 7708 regexCSSValue = " ?[^;]+", 7709 regexVAlign = new RegExp( vAlignCSS + cssValueSeparator + " ?" + regexCSSValue + cssPropertySeparator + "?", "i" ), 7710 regexMinHeight = new RegExp( minHeightCSS + cssValueSeparator + " ?" + regexCSSValue + cssPropertySeparator + "?", "i" ), 7711 7712 /** 7713 * @method init 7714 * @param {jQuery Event} event Event that triggered the function call 7715 */ 7716 init = function( event ) { 7717 7718 // Start initialization 7719 // returns DOM object = proceed with init 7720 // returns undefined = do not proceed with init (e.g., already initialized) 7721 var elm = wb.init( event, componentName, selector ); 7722 7723 if ( elm ) { 7724 7725 // Remove the event handler since only want init fired once per page (not per element) 7726 $document.off( eventTimerpoke, selector ); 7727 7728 onResize(); 7729 7730 // Identify that initialization has completed 7731 wb.ready( $document, componentName ); 7732 } 7733 }, 7734 7735 /** 7736 * Re-equalise any time the window/document or a child element of 'selector' is resized. 7737 * @method onResize 7738 */ 7739 onResize = function() { 7740 var $elm, $children, $anchor, currentChild, childCSS, i, j, 7741 $elms = $( selector ), 7742 row = [], 7743 rowTop = -1, 7744 currentChildTop = -1, 7745 currentChildHeight = -1, 7746 tallestHeight = -1; 7747 7748 for ( i = $elms.length - 1; i !== -1; i -= 1 ) { 7749 $elm = $elms.eq( i ); 7750 $children = $elm.find( ".eqht-trgt" ); 7751 if ( !$children.length ) { 7752 $children = $elm.children(); 7753 } 7754 7755 // Reinitialize the row at the beginning of each section of equal height 7756 row = []; 7757 7758 $anchor = detachElement( $elm ); 7759 for ( j = $children.length - 1; j !== -1; j -= 1 ) { 7760 currentChild = $children[ j ]; 7761 childCSS = currentChild.style.cssText.toLowerCase(); 7762 7763 //Ensure the CSS string ends by a separator 7764 if ( childCSS.length > 0 && childCSS.substr( childCSS.length - 1 ) !== cssPropertySeparator ) { 7765 childCSS += cssPropertySeparator; 7766 } 7767 7768 // Ensure all children that are on the same baseline have the same 'top' value. 7769 if ( childCSS.indexOf( vAlignCSS ) !== -1 ) { 7770 childCSS = childCSS.replace( regexVAlign, vAlignCSS + cssValueSeparator + vAlignDefault + cssPropertySeparator ); 7771 } else { 7772 childCSS += " " + vAlignCSS + cssValueSeparator + vAlignDefault + cssPropertySeparator; 7773 } 7774 7775 // Remove any previously set min height 7776 if ( childCSS.indexOf( minHeightCSS ) !== -1 ) { 7777 childCSS = childCSS.replace( regexMinHeight, minHeightCSS + cssValueSeparator + minHeightDefault + cssPropertySeparator ); 7778 } else { 7779 childCSS += " " + minHeightCSS + cssValueSeparator + minHeightDefault + cssPropertySeparator; 7780 } 7781 7782 currentChild.style.cssText = childCSS; 7783 $children.eq( j ).data( minHeightCSS, minHeightDefault ); 7784 } 7785 $elm = reattachElement( $anchor ); 7786 7787 // set the top offset and tallest height to the first element 7788 rowTop = $children[ 0 ] ? $children[ 0 ].getBoundingClientRect().top + window.pageYOffset : 0; 7789 tallestHeight = $children[ 0 ] ? $children[ 0 ].offsetHeight : 0; 7790 7791 // first, the loop MUST be from start to end to work. 7792 for ( j = 0; j < $children.length; j++ ) { 7793 currentChild = $children[ j ]; 7794 7795 currentChildTop = currentChild.getBoundingClientRect().top + window.pageYOffset; 7796 currentChildHeight = currentChild.offsetHeight; 7797 7798 // if the current element is visible...
7799 // note: hidden elements need to be excluded since they have a different top offset than visible ones 7800 if ( currentChildHeight ) { 7801 7802 // as soon as we find an element not on this row (not the same top offset)... 7803 if ( currentChildTop !== rowTop ) { 7804 7805 // we need to equalize each item in that row to align the next row 7806 equalize( row, tallestHeight ); 7807 7808 // since the elements of the previous row was equalized 7809 // we need to get the new top offset of the current element 7810 currentChildTop = currentChild.getBoundingClientRect().top + window.pageYOffset; 7811 7812 // reset the row, rowTop and tallestHeight 7813 row.length = 0; 7814 rowTop = currentChildTop; 7815 tallestHeight = currentChildHeight; 7816 } 7817 7818 tallestHeight = Math.max( currentChildHeight, tallestHeight ); 7819 row.push( $children.eq( j ) ); 7820 } 7821 } 7822 7823 // equalize the last row 7824 equalize( row, tallestHeight ); 7825 7826 // Identify that the height equalization was updated 7827 $document.trigger( "wb-updated" + selector ); 7828 } 7829 }, 7830 7831 /** 7832 * @method equalize 7833 * @param {array} row the array of items to be equalized 7834 * @param {int} tallestHeight the tallest height to use to equalize 7835 */ 7836 equalize = function( row, tallestHeight ) { 7837 for ( var i = 0; i < row.length; i++ ) { 7838 7839 // added a +1 because some floated element got stuck if the 7840 // shortest element was the last element in the row 7841 var minHeight = tallestHeight + 1; 7842 row[ i ][ 0 ].style.minHeight = minHeight + "px"; 7843 } 7844 }, 7845 7846 /** 7847 * @method detachElement 7848 * @param {jQuery object} $elm The element to detach 7849 * @returns {object} The detached element 7850 */ 7851 detachElement = function( $elm ) { 7852 var $prev = $elm.prev(), 7853 $next = $elm.next(), 7854 $parent = $elm.parent(); 7855 7856 if ( $prev.length ) { 7857 $elm.data( { anchor: $prev, anchorRel: "prev" } ); 7858 } else if ( $next.length ) { 7859 $elm.data( { anchor: $next, anchorRel: "next" } ); 7860 } else if ( $parent.length ) { 7861 $elm.data( { anchor: $parent, anchorRel: "parent" } ); 7862 } 7863 7864 return $elm.detach(); 7865 }, 7866 7867 /** 7868 * @method reattachElement 7869 * @param {jQuery object} $elm The element to reattach 7870 * @returns {object} The reattached element 7871 */ 7872 reattachElement = function( $elm ) { 7873 var $anchor = $elm.data( "anchor" ), 7874 anchorRel = $elm.data( "anchorRel" ); 7875 7876 switch ( anchorRel ) { 7877 case "prev": 7878 $anchor.after( $elm ); 7879 break; 7880 case "next": 7881 $anchor.before( $elm ); 7882 break; 7883 case "parent": 7884 $anchor.append( $elm ); 7885 break; 7886 } 7887 7888 return $elm; 7889 }; 7890 7891// Bind the init event of the plugin 7892$document.on( eventTimerpoke + " " + initEvent, selector, init ); 7893 7894// Handle text and window resizing 7895$document.on( "txt-rsz.wb win-rsz-width.wb win-rsz-height.wb " + contentUpdatedEvent + " wb-updated.wb-tables wb-update" + selector, onResize ); 7896 7897// Add the timer poke to initialize the plugin 7898wb.add( selector ); 7899 7900} )( jQuery, window, wb ); 7901 7902/** 7903* @title WET-BOEW Exit script plugin 7904* @overview Plugin redirects users to non secure site 7905* @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 7906* @author @ipaksc 7907*/ 7908( function( $, window, wb, crypto ) { 7909"use strict"; 7910var componentName = "wb-exitscript", 7911 selector = "." + componentName, 7912 initEvent = "wb-init" + selector, 7913 $document = wb.doc, 7914 exiturlparam = componentName + "-urlparam", 7915 keyForKeyHolder = componentName + "key", 7916 moDalId = componentName + "-modal", 7917 i18n, 7918 i18nDict = { 7919 en: { 7920 "msgboxHeader": "Warning", 7921 "exitMsg": "You are about to leave a secure site, do you wish to continue?", 7922 "targetWarning": "The link will open in a new browser window.", 7923 "yesBtn": "Yes", 7924 "cancelBtn": "Cancel" 7925 7926 }, 7927 fr: { 7928 "msgboxHeader": "Avertissement", 7929 "exitMsg": "Vous êtes sur le point de quitter un site sécurisé. Voulez-vous continuer?", 7930 "targetWarning": "Le lien s'ouvrira dans une nouvelle fenêtre de navigateur.", 7931 "yesBtn": "Oui", 7932 "cancelBtn": "Annuler" 7933 7934 } 7935 }, 7936 7937 /** 7938 * @method init 7939 * @param {jQuery Event} event Event that triggered the function call 7940 */ 7941 init = function( event ) { 7942 var elm = wb.init( event, componentName, selector ), 7943 settings, 7944 queryString = window.location.search,
7945 urlParams = new URLSearchParams( queryString ), 7946 counterInUrl = wb.string.fromHexString( urlParams.get( "exturl" ) ), 7947 encryptedUrl = localStorage.getItem( componentName ), 7948 jwt = JSON.parse( localStorage.getItem( keyForKeyHolder ) ), 7949 $elm; 7950 if ( elm ) { 7951 $elm = $( elm ); 7952 settings = $.extend( 7953 true, 7954 window[ componentName ], 7955 wb.getData( $elm, componentName ) 7956 7957 ); 7958 7959 $elm.data( componentName, settings ); 7960 7961 if ( settings.url && crypto ) { 7962 7963 crypto.subtle.generateKey( 7964 { 7965 name: "AES-CTR", 7966 length: 256 7967 }, 7968 true, 7969 [ "encrypt", "decrypt" ] 7970 ).then( function( keyToEncrypt ) { 7971 7972 var enc, messageEncoded, counter; 7973 7974 // Save the key in the anchor 7975 crypto.subtle.exportKey( "jwk", keyToEncrypt ) 7976 .then( function( exportedJwtKey ) { 7977 elm[ keyForKeyHolder ] = exportedJwtKey; 7978 } ); 7979 7980 // Encrypt the URL 7981 enc = new TextEncoder(); 7982 messageEncoded = enc.encode( elm.href ); 7983 counter = crypto.getRandomValues( new Uint8Array( 16 ) ); 7984 crypto.subtle.encrypt( 7985 { 7986 name: "AES-CTR", 7987 counter: counter, 7988 length: 64 7989 }, 7990 keyToEncrypt, 7991 messageEncoded 7992 ).then( function( ciphertext ) { 7993 elm[ componentName ] = ciphertext; 7994 } ); 7995 7996 // Change the link URL by passing the counter as a key 7997 $elm.attr( "href", settings.url + "?exturl=" + wb.string.toHexString( counter ) ); 7998 } ); 7999 8000 } 8001 8002 i18n = i18nDict[ wb.lang || "en" ]; 8003 8004 // This conditional statement for a middle static exit page, to retrieve the URL to the non-secure site. 8005 if ( $elm.hasClass( exiturlparam ) && encryptedUrl !== null && jwt !== null ) { 8006 8007 crypto.subtle.importKey( 8008 "jwk", 8009 jwt, 8010 { 8011 name: "AES-CTR", 8012 length: 256 8013 }, 8014 true, 8015 [ "decrypt" ] 8016 ).then( function( key ) { 8017 8018 crypto.subtle.decrypt( 8019 { 8020 name: "AES-CTR", 8021 counter: counterInUrl, 8022 length: 64 8023 }, 8024 key, 8025 wb.string.base64ToArrayBuffer( encryptedUrl ) 8026 ).then( function( decrypted ) { 8027 8028 var dec = new TextDecoder(), 8029 urlToRedirect = dec.decode( decrypted ); 8030 8031 // Check if the decrypted message is an valid URL and silently fail if the pattern don't match 8032 if ( urlToRedirect.match( /^(http|https):\/\//g ) ) { 8033 elm.outerHTML = "<a href='" + urlToRedirect + "'>" + urlToRedirect + "</a>"; 8034 } 8035 8036 } ); 8037 } ); 8038 8039 } 8040 8041 // Remove the plugin data and ensure it is removed from the localStorage 8042 localStorage.removeItem( componentName ); 8043 localStorage.removeItem( keyForKeyHolder ); 8044 8045 wb.ready( $elm, componentName ); 8046 } 8047 }; 8048 8049$document.on( "click", selector, function( event ) { 8050 8051 var elm = event.currentTarget, 8052 $elm = $( elm ), 8053 wrapper, 8054 targetAttribute = "", 8055 moDal = document.createDocumentFragment(), 8056 tpl = document.createElement( "div" ), 8057 settings = $elm.data( componentName ),
8058 msgboxHeader = i18n.msgboxHeader, 8059 yesBtn = i18n.yesBtn, 8060 cancelBtn = i18n.cancelBtn, 8061 exitMsg = i18n.exitMsg, 8062 targetWarning = i18n.targetWarning; 8063 8064 if ( settings.i18n ) { 8065 msgboxHeader = settings.i18n.msgboxHeader || i18n.msgboxHeader; 8066 yesBtn = settings.i18n.yesBtn || i18n.yesBtn; 8067 cancelBtn = settings.i18n.cancelBtn || i18n.cancelBtn; 8068 exitMsg = settings.i18n.exitMsg || i18n.exitMsg; 8069 targetWarning = settings.i18n.targetWarning || i18n.targetWarning; 8070 } 8071 8072 if ( !settings.url ) { 8073 8074 event.preventDefault(); 8075 } 8076 8077 if ( this.hasAttribute( "target" ) ) { 8078 targetAttribute = "target='" + this.getAttribute( "target" ) + "'"; 8079 } else { 8080 targetAttribute = "target='" + targetAttribute + "'"; 8081 } 8082 8083 if ( this.getAttribute( "target" ) === "_blank" ) { 8084 exitMsg = exitMsg + " " + targetWarning; 8085 } 8086 8087 if ( document.getElementById( moDalId ) ) { 8088 document.getElementById( moDalId ).remove(); 8089 8090 } 8091 8092 if ( !settings.url ) { 8093 tpl.innerHTML = "<section id='" + moDalId + "' " + "class='mfp-hide modal-dialog modal-content overlay-def'>" + 8094 "<header class='modal-header'><h2 class='modal-title'>" + msgboxHeader + "</h2></header>" + 8095 "<div class='modal-body'>" + 8096 "<p>" + exitMsg + "</p>" + 8097 "</div>" + 8098 "<div class='modal-footer'>" + 8099 "<ul class='list-inline text-center'>" + 8100 "<li><a class='btn btn-default pull-right popup-modal-dismiss'" + targetAttribute + " href='" + this.getAttribute( "href" ) + "'>" + yesBtn + "</a></li>" + 8101 "<li><button class='btn btn-primary popup-modal-dismiss pull-left'>" + cancelBtn + "</button></li>" + 8102 "</ul></div></section>"; 8103 moDal.appendChild( tpl ); 8104 wrapper = moDal.firstChild; 8105 wrapper = wrapper.firstChild; 8106 document.body.appendChild( wrapper ); 8107 8108 $( wrapper ).trigger( "open.wb-lbx", [ 8109 [ { 8110 src: "#" + moDalId, 8111 type: "inline" 8112 } ], 8113 8114 true 8115 8116 ] ); 8117 8118 } else if ( crypto && this[ componentName ] ) { 8119 8120 // Save to localStorage, the plugin init will ensure this data is only used once 8121 localStorage.setItem( componentName, wb.string.arrayBufferToBase64( this[ componentName ] ) ); 8122 localStorage.setItem( keyForKeyHolder, JSON.stringify( this[ keyForKeyHolder ] ) ); 8123 } 8124 8125} ); 8126 8127// Bind the init event of the plugin 8128$document.on( "timerpoke.wb " + initEvent, selector, init ); 8129 8130// Add the timer poke to initialize the plugin 8131wb.add( selector ); 8132 8133} )( jQuery, window, wb, crypto ); 8134 8135/** 8136* @title WET-BOEW Facebook embedded page 8137* @overview Helps with implementing Facebook embedded pages. 8138* @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 8139* @author @pjackson28 8140*/ 8141( function( $, window, wb ) { 8142"use strict"; 8143 8144/* 8145* Variable and function definitions. 8146* These are global to the plugin - meaning that they will be initialized once per page, 8147* not once per instance of plugin on the page. So, this is a good place to define 8148* variables that are common to all instances of the plugin on a page. 8149*/ 8150var componentName = "wb-facebook", 8151 selector = "." + componentName, 8152 initEvent = "wb-init" + selector, 8153 $document = wb.doc, 8154 fbinited = false, 8155 8156 /** 8157 * @method init 8158 * @param {jQuery Event} event Event that triggered the function call 8159 */ 8160 init = function( event ) { 8161 8162 // Start initialization 8163 // returns DOM object = proceed with init 8164 // returns undefined = do not proceed with init (e.g., already initialized) 8165 var ele = wb.init( event, componentName, selector ), 8166 protocol = wb.pageUrlParts.protocol; 8167 8168 if ( ele ) { 8169 Modernizr.load( 8170 { 8171 load: [ ( protocol.indexOf( "http" ) === -1 ? "http:" : protocol ) + "//connect.facebook.net/" + wb.lang + "_US/sdk.js" ], 8172 complete: function() { 8173 if ( !fbinited ) { 8174 window.FB.init( { 8175 version: "v2.4" 8176 } ); 8177 fbinited = true; 8178 } 8179 8180 window.FB.XFBML.parse( ele[ 0 ] ); 8181 wb.ready( $( ele ), componentName ); 8182 } 8183 } 8184 ); 8185 } 8186 }; 8187 8188$document.on( "timerpoke.wb " + initEvent, selector, init ); 8189 8190// Add the timer poke to initialize the plugin 8191wb.add( selector ); 8192 8193} )( jQuery, window, wb ); 8194 8195/** 8196 * @title WET-BOEW Favicon Plugin 8197 * @overview Provides the ability to add and update a page's favicons 8198 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 8199 * @author @patheard 8200 * 8201 * This plugin provides the ability to add and update the favicon's on a web page. Its default behaviour is to add a mobile favicon to web pages that have a favicon defined by a `<link rel='icon'>` element. 8202 * 8203 * The mobile favicon's file name, rel, path and sizes can be set with data attributes on the `<link rel='icon'/>`: 8204 * 8205 * -**data-filename:** filename of the mobile favicon (defaults to "favicon-mobile.png"). This will be appended to the favicon's path. 8206 * -**data-path:** path to the mobile favicon (defaults to using the same path as the shortcut icon). 8207 * -**data-rel:** rel attribute of the mobile favicon (defaults to "apple-touch-icon"). 8208 * -**data-sizes:** sizes attribute of the mobile favicon (defaults to "57x57 72x72 114x114 144x144 150x150"). 8209 * 8210 * For example, the following overrides the rel and file name attributes of the mobile favicon: 8211 * 8212 * <link href="favicon.ico" rel='icon' data-rel="apple-touch-icon-precomposed" data-filename="my-mobile-favicon.ico"/> 8213 */ 8214( function( $, document, wb ) { 8215"use strict"; 8216 8217/* 8218 * Variable and function definitions. 8219 * These are global to the plugin - meaning that they will be initialized once per page, 8220 * not once per instance of plugin on the page. So, this is a good place to define 8221 * variables that are common to all instances of the plugin on a page. 8222 */
8223var componentName = "wb-favicon", 8224 selector = "link[rel='icon']", 8225 initEvent = "wb-init." + componentName, 8226 updatedEvent = "wb-updated." + componentName, 8227 mobileEvent = "mobile." + componentName, 8228 iconEvent = "icon." + componentName, 8229 $document = wb.doc, 8230 8231 /* 8232 * Plugin users can override these defaults by setting attributes on the html elements that the 8233 * selector matches. 8234 * For example, adding the attribute data-option1="false", will override option1 for that plugin instance. 8235 */ 8236 defaults = { 8237 filename: "favicon-mobile.png", 8238 path: null, 8239 rel: "apple-touch-icon", 8240 sizes: "57x57 72x72 114x114 144x144 150x150" 8241 }, 8242 8243 /** 8244 * @method init 8245 * @param {jQuery Event} event Event that triggered the function call 8246 */ 8247 init = function( event ) { 8248 8249 // Start initialization 8250 // returns DOM object = proceed with init 8251 // returns undefined = do not proceed with init (e.g., already initialized) 8252 var elm = wb.init( event, componentName, selector, true ), 8253 $favicon, settings; 8254 8255 if ( elm ) { 8256 $favicon = $( elm ); 8257 8258 // Merge default settings with overrides from the selected plugin element. 8259 settings = $.extend( {}, defaults, $favicon.data() ); 8260 8261 $favicon.trigger( mobileEvent, settings ); 8262 8263 // Identify that initialization has completed 8264 wb.ready( $document, componentName ); 8265 } 8266 }, 8267 8268 /** 8269 * Adds, or updates, the mobile favicon on a page. Mobile favicons are identified by the 8270 * `apple` prefix in the `<link>` elements rel attribute. 8271 * @method mobile 8272 * @param {DOM element} favicon Favicon element 8273 * @param {jQuery Event} event Event that triggered this handler 8274 * @param {Object} data Key-value data object passed with the event 8275 */ 8276 mobile = function( favicon, event, data ) { 8277 var faviconPath, 8278 faviconMobile = $( "link[rel^='apple']" ), 8279 isFaviconMobile = faviconMobile.length !== 0; 8280 8281 // Create the mobile favicon if it doesn't exist 8282 if ( !isFaviconMobile ) { 8283 var lnk = document.createElement( "link" ); 8284 lnk.setAttribute( "rel", data.rel ); 8285 lnk.setAttribute( "sizes", data.sizes ); 8286 lnk.setAttribute( "class", componentName ); 8287 8288 document.head.appendChild( lnk ); 8289 8290 faviconMobile = $( lnk ); 8291 } 8292 8293 // Only add/update a mobile favicon that was created by the plugin 8294 if ( faviconMobile.hasClass( componentName ) ) { 8295 faviconPath = data.path !== null ? data.path : getPath( favicon.getAttribute( "href" ) ); 8296 faviconMobile.attr( "href", faviconPath + data.filename ); 8297 8298 if ( !isFaviconMobile ) { 8299 favicon.parentNode.insertBefore( faviconMobile[ 0 ], favicon ); 8300 } 8301 } 8302 8303 $document.trigger( updatedEvent, [ "mobile" ] ); 8304 }, 8305 8306 /** 8307 * Updates the the page's shortcut icon 8308 * @method icon 8309 * @param {DOM element} favicon Favicon element 8310 * @param {jQuery Event} event Event that triggered this handler 8311 * @param {Object} data Key-value data object passed with the event 8312 */ 8313 icon = function( favicon, event, data ) { 8314 var faviconPath = data.path !== null ? data.path : getPath( favicon.getAttribute( "href" ) ); 8315 favicon.setAttribute( "href", faviconPath + data.filename ); 8316 8317 $document.trigger( updatedEvent, [ "icon" ] ); 8318 }, 8319 8320 /** 8321 * Given a full file path, returns the path without the filename 8322 * @method getPath 8323 * @param {string} filepath The full path to file, including filename 8324 * @returns {string} The path to the file 8325 */ 8326 getPath = function( filepath ) { 8327 return filepath.substring( 0, filepath.lastIndexOf( "/" ) + 1 ); 8328 }; 8329 8330// Bind the init event 8331$document.on( "timerpoke.wb " + initEvent, selector, init ); 8332 8333// Bind the mobile and icon events 8334$document.on( mobileEvent + " " + iconEvent, selector, function( event, data ) { 8335 var eventTarget = event.target; 8336 8337 // Filter out any events triggered by descendants 8338 if ( event.currentTarget === eventTarget ) { 8339 switch ( event.type ) { 8340 case "mobile": 8341 mobile( eventTarget, event, data ); 8342 break; 8343 8344 case "icon": 8345 icon( eventTarget, event, data ); 8346 break; 8347 } 8348 } 8349 8350 /* 8351 * Since we are working with events we want to ensure that we are being passive about our control, 8352 * so returning true allows for events to always continue 8353 */ 8354 return true; 8355} ); 8356 8357// Add the timer poke to initialize the plugin
8358wb.add( selector ); 8359 8360} )( jQuery, document, wb ); 8361 8362/** 8363 * @title WET-BOEW Feeds 8364 * @overview Aggregates and displays entries from one or more Web feeds. 8365 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 8366 * @author @pjackson28 8367 */ 8368( function( $, window, wb, undef ) { 8369"use strict"; 8370 8371/* 8372 * Variable and function definitions. 8373 * These are global to the plugin - meaning that they will be initialized once per page, 8374 * not once per instance of plugin on the page. So, this is a good place to define 8375 * variables that are common to all instances of the plugin on a page. 8376 */ 8377var componentName = "wb-feeds", 8378 selector = "." + componentName, 8379 feedLinkSelector = "li > a", 8380 initEvent = "wb-init" + selector, 8381 $document = wb.doc, 8382 patt = /\\u([\d\w]{4})/g, 8383 8384 /** 8385 * @object Templates 8386 * @properties {function} 8387 * @param {object} requires a entry object of various ATOM based properties 8388 * @returns {string} modified string with appropriate markup/format for a entry object 8389 */ 8390 Templates = { 8391 8392 /** 8393 * [flickr template] 8394 * @param {entry object} data 8395 * @return {string} HTML string for creating a photowall effect 8396 */ 8397 flickr: function( data ) { 8398 var media = data.media.m, 8399 flickrData = { 8400 title: data.title, 8401 thumbnail: media.replace( "_m.", "_s." ), 8402 image: media.replace( "_m", "" ), 8403 description: data.description.replace( /^\s*<p>(.*?)<\/p>\s*<p>(.*?)<\/p>/i, "" ) 8404 }; 8405 8406 // due to CORS we cannot default to simple ajax pulls of the image. We have to inline the content box 8407 return "<li><a class='feed-flickr' href='#' data-flickr='" + 8408 wb.escapeAttribute( JSON.stringify( flickrData ) ) + "'><img src='" + flickrData.thumbnail + "' alt='" + 8409 wb.escapeAttribute( flickrData.title ) + "' title='" + wb.escapeAttribute( flickrData.title ) + 8410 "' class='img-responsive'/></a></li>"; 8411 }, 8412 8413 /** 8414 * [YouTube template] 8415 * @param {entry object} data 8416 * @return {string} HTML string for creating a photowall effect 8417 */ 8418 youtube: function( data ) { 8419 var youtubeDate = { 8420 title: data.title, 8421 videoId: data.id 8422 }; 8423 8424 // Due to CORS we cannot default to simple ajax pulls of the image. We have to inline the content box 8425 return "<li class='col-md-4 col-sm-6'><button class='btn btn-lnk feed-youtube' data-youtube='" + 8426 wb.escapeAttribute( JSON.stringify( youtubeDate ) ) + "'><img src='" + 8427 wb.pageUrlParts.protocol + "//img.youtube.com/vi/" + youtubeDate.videoId + "/mqdefault.jpg' alt='" + 8428 wb.escapeAttribute( youtubeDate.title ) + "' title='" + wb.escapeAttribute( youtubeDate.title ) + 8429 "' class='img-responsive' /></button></li>"; 8430 }, 8431 8432 /** 8433 * [pinterest template] 8434 * @param {entry object} data 8435 * @return {string} HTML string of formatted using a simple list / anchor view 8436 */ 8437 pinterest: function( data ) { 8438 var content = fromCharCode( data.description ).replace( /<a href="\/pin[^"]*"><img ([^>]*)><\/a>([^<]*)(<a .*)?/, "<a href='" + 8439 data.link + "'><img alt='' class='center-block' $1><br/>$2</a>$3" ); 8440 return "<li class='media'>" + content + 8441 ( data.publishedDate !== "" ? " <small class='small feeds-date'><time>" + 8442 wb.date.toDateISO( data.publishedDate, true ) + "</time></small>" : "" ) + "</li>"; 8443 }, 8444 8445 /** 8446 * [generic template] 8447 * @param {entry object} data 8448 * @return {string} HTML string of formatted using a simple list / anchor view 8449 */ 8450 generic: function( data ) { 8451 var title = data.title; 8452 8453 if ( typeof( title ) === "object" ) { 8454 if ( title.content ) { 8455 title = title.content; 8456 } else if ( title.type === "xhtml" && title.div ) { 8457 title = title.div.content; 8458 } 8459 } 8460 return "<li><a href='" + data.link + "'>" + title + "</a><br />" + 8461 ( data.publishedDate !== "" ? " <small class='feeds-date'><time>" + 8462 wb.date.toDateISO( data.publishedDate, true ) + "</time></small>" : "" ) + "</li>"; 8463 } 8464 }, 8465 8466 /** 8467 * Helper function that returns the string representation of a unicode character 8468 * @method decode 8469 * @param {regex} match unicode pattern 8470 * @param {string} code string where unicode is needed to be converted 8471 * @return {string} unicode string character 8472 */ 8473 decode = function( match, code ) { 8474 return String.fromCharCode( parseInt( code, 16 ) ); 8475 }, 8476 8477 /**
8478 * Helper wrapper function that performs unicode decodes on a string 8479 * @method fromCharCode 8480 * @param {string} s string to sanitize with escaped unicode characters 8481 * @return {string} sanitized string 8482 */ 8483 fromCharCode = function( s ) { 8484 return s.replace( patt, decode ); 8485 }, 8486 8487 /** 8488 * Helper function that returns a class-based set limit on plugin instances 8489 * @method getLimit 8490 * @param {DOM object} elm The element to search for a class of the form limit-5 8491 * @return {number} 0 if none found, which means the plugin default 8492 */ 8493 getLimit = function( elm ) { 8494 var count = elm.className.match( /\blimit-\d+/ ); 8495 if ( !count ) { 8496 return 0; 8497 } 8498 return Number( count[ 0 ].replace( /limit-/i, "" ) ); 8499 }, 8500 8501 /** 8502 * @method init 8503 * @param {jQuery Event} event Event that triggered the function call 8504 */ 8505 init = function( event ) { 8506 8507 // Start initialization 8508 // returns DOM object = proceed with init 8509 // returns undefined = do not proceed with init (e.g., already initialized) 8510 var elm = wb.init( event, componentName, selector ), 8511 fetch, url, $content, limit, feeds, fType, last, i, callback, fElem, fIcon, youtubeData, $elm; 8512 if ( elm ) { 8513 $elm = $( elm ); 8514 $content = $elm.find( ".feeds-cont" ); 8515 limit = getLimit( elm ); 8516 feeds = $content.find( feedLinkSelector ); 8517 last = feeds.length - 1; 8518 8519 // Lets bind some variables to the node to ensure safe ajax thread counting 8520 8521 $content.data( "toProcess", feeds.length ) 8522 .data( "feedLimit", limit ) 8523 .data( "entries", [] ); 8524 8525 for ( i = last; i !== -1; i -= 1 ) { 8526 fElem = feeds.eq( i ); 8527 fIcon = fElem.find( "> img" ); 8528 8529 fetch = { 8530 dataType: "jsonp", 8531 timeout: 10000 8532 }; 8533 8534 if ( fElem.attr( "data-ajax" ) ) { 8535 8536 if ( fElem.attr( "href" ).indexOf( "flickr" ) !== -1 ) { 8537 fType = "flickr"; 8538 callback = "jsoncallback"; 8539 $content.data( componentName + "-postProcess", [ ".wb-lbx" ] ); 8540 } else { 8541 fType = "generic"; 8542 callback = false; 8543 fetch.dataType = "json"; 8544 } 8545 8546 // We need a Gallery so lets add another plugin 8547 // #TODO: Lightbox review for more abstraction we should not have to add a wb.add() for overlaying 8548 fetch.url = fElem.attr( "data-ajax" ); 8549 fetch.jsonp = callback; 8550 } else if ( fElem.attr( "data-youtube" ) ) { 8551 youtubeData = wb.getData( fElem, "youtube" ); 8552 8553 $content.data( componentName + "-postProcess", [ ".wb-lbx", ".wb-mltmd" ] ); 8554 8555 if ( youtubeData.playlist ) { 8556 fElem.trigger( { 8557 type: "data-ready.wb-feeds", 8558 feedsData: youtubeData.playlist 8559 }, { 8560 feedType: "youtube", 8561 _content: $content 8562 } ); 8563 } 8564 8565 } else { 8566 8567 url = fElem.attr( "href" ); 8568 fetch.dataType = "xml"; 8569 fetch.url = url; 8570 8571 // Let's bind the template to the Entries 8572 if ( url.indexOf( "pinterest.com" ) > -1 ) { 8573 fType = "pinterest"; 8574 } else { 8575 fType = "generic"; 8576 } 8577 } 8578 8579 fetch.jsonp = callback; 8580 8581 fetch.context = { 8582 fIcon: ( fIcon.length !== 0 ) ? fIcon.attr( "src" ) : "", 8583 feedType: fType, 8584 _content: $content 8585 }; 8586 8587 fElem.trigger( { 8588 type: "ajax-fetch.wb", 8589 fetch: fetch 8590 } ); 8591 } 8592 } 8593 }, 8594 8595 /** 8596 * Process Feed/JSON Entries for CORS Enabled 8597 * @method corsEntry 8598 */ 8599 corsEntry = function( xmlDoc, limit ) { 8600 var entries = xmlDoc.getElementsByTagName( "entry" ).length, 8601 arr_entry = [], 8602 corsObj = {}, 8603 jsonString = JSON.stringify( xmlToJson( xmlDoc ) ), 8604 jsonObj = JSON.parse( jsonString ), 8605 i, iCache; 8606 if ( limit > entries || limit === 0 || limit === null ) { 8607 limit = entries; 8608 } 8609 if ( entries === 1 ) { 8610 iCache = jsonObj.feed.entry; 8611 corsObj = { 8612 title: iCache.title[ "#text" ], 8613 link: ( iCache.link ? iCache.link[ "@attributes" ].href : iCache.id[ "#text" ] ), 8614 updated: iCache.updated[ "#text" ] 8615 }; 8616 arr_entry.push( corsObj ); 8617 } else if ( entries ) { 8618 for ( i = 0; i < limit; i++ ) { 8619 iCache = jsonObj.feed.entry[ i ]; 8620 corsObj = { 8621 title: iCache.title[ "#text" ], 8622 link: ( iCache.link ? iCache.link[ "@attributes" ].href : iCache.id[ "#text" ] ), 8623 updated: iCache.updated[ "#text" ] 8624 }; 8625 arr_entry.push( corsObj ); 8626 } 8627 } 8628 return arr_entry; 8629 }, 8630 8631 /** 8632 * Process XML to JSON 8633 * @method xmlToJson 8634 * @param {xml} 8635 */ 8636 xmlToJson = function( xml ) { 8637 8638 var obj = {}, 8639 i, iCache, nodeName, old, 8640 xmlAttributes, xmlChildNodes, 8641 xmlNodeType = xml.nodeType; 8642 8643 if ( xmlNodeType === 1 ) { 8644 xmlAttributes = xml.attributes; 8645 if ( xmlAttributes.length ) { 8646 obj[ "@attributes" ] = {}; 8647 for ( i = 0; i < xmlAttributes.length; i++ ) { 8648 iCache = xmlAttributes.item( i ); 8649 obj[ "@attributes" ][ iCache.nodeName ] = iCache.nodeValue; 8650 } 8651 } 8652 } else if ( xmlNodeType === 3 ) { 8653 obj = xml.nodeValue; 8654 } 8655 8656 if ( xml.hasChildNodes() ) { 8657 xmlChildNodes = xml.childNodes; 8658 for ( i = 0; i < xmlChildNodes.length; i++ ) { 8659 iCache = xmlChildNodes.item( i ); 8660 nodeName = iCache.nodeName; 8661 if ( typeof( obj[ nodeName ] ) === "undefined" ) { 8662 obj[ nodeName ] = xmlToJson( iCache ); 8663 } else { 8664 if ( typeof( obj[ nodeName ].push ) === "undefined" ) { 8665 old = obj[ nodeName ]; 8666 obj[ nodeName ] = []; 8667 obj[ nodeName ].push( old ); 8668 } 8669 obj[ nodeName ].push( xmlToJson( iCache ) ); 8670 } 8671 } 8672 } 8673 return obj; 8674 }, 8675 8676 /** 8677 * Process Feed/JSON Entries 8678 * @method processEntries 8679 * @param {data} JSON formatted data to process 8680 * @return {string} of HTML output 8681 */ 8682 processEntries = function( data ) { 8683 var items = data, 8684 entries = [], 8685 icon = this.fIcon, 8686 $content = this._content, 8687 toProcess = $content.data( "toProcess" ), 8688 i, len; 8689 8690 len = items.length; 8691 for ( i = 0; i !== len; i += 1 ) { 8692 items[ i ].fIcon = icon; 8693 8694 if ( items[ i ].publishedDate === undef ) { 8695 items[ i ].publishedDate = ( items[ i ].published || items[ i ].pubDate || items[ i ].updated || "" ); 8696 } 8697 8698 var link = items[ i ].link; 8699 8700 if ( link && link.href ) { 8701 items[ i ].link = link.href; 8702 } 8703 8704 entries.push( items[ i ] ); 8705 } 8706 8707 // lets merge with latest entries 8708 entries = $.merge( entries, $content.data( "entries" ) ); 8709 8710 if ( toProcess === 1 ) { 8711 parseEntries( entries, $content.data( "feedLimit" ), $content, this.feedType ); 8712 return 0; 8713 } 8714 8715 toProcess -= 1; 8716 $content.data( { 8717 "toProcess": toProcess, 8718 "entries": entries 8719 } ); 8720 8721 return toProcess; 8722 }, 8723 8724 /** 8725 * Parses the results from a JSON request and appends to an element 8726 * @method parseEntries 8727 * @param {object} entries Results from a JSON request. 8728 * @param {integer} limit Limit on the number of results to append to the element. 8729 * @param {jQuery DOM element} $elm Element to which the elements will be appended. 8730 * @return {url} The URL for the JSON request 8731 */ 8732 parseEntries = function( entries, limit, $elm, feedtype ) { 8733 var cap = ( limit > 0 && limit < entries.length ? limit : entries.length ), 8734 result = "", 8735 compare = wb.date.compare, 8736 $details = $elm.closest( "details" ), 8737 activate = true, 8738 feedContSelector = ".feeds-cont", 8739 hasVisibilityHandler = "vis-handler", 8740 i, sorted, sortedEntry, $tabs; 8741 8742 if ( feedtype !== "youtube" ) { 8743 sorted = entries.sort( function( a, b ) { 8744 return compare( b.publishedDate, a.publishedDate ); 8745 } ); 8746 } else { 8747 sorted = entries; 8748 } 8749 8750 for ( i = 0; i !== cap; i += 1 ) { 8751 sortedEntry = sorted[ i ]; 8752 result += Templates[ feedtype ]( sortedEntry ); 8753 } 8754 $elm.data( componentName + "-result", result ); 8755 8756 // Check to see if feed should be activated (only if visible) 8757 // and add handler to determine visibility 8758 if ( $details.length !== 0 ) { 8759 if ( $details.attr( "role" ) === "tabpanel" ) { 8760 if ( $details.attr( "aria-hidden" ) === "true" ) { 8761 activate = false;
8762 $elm.empty().addClass( "waiting" ); 8763 $tabs = $details.closest( ".wb-tabs" ); 8764 if ( !$tabs.hasClass( hasVisibilityHandler ) ) { 8765 $tabs 8766 .on( "wb-updated.wb-tabs", function( event, $newPanel ) { 8767 var $feedCont = $newPanel.find( feedContSelector ); 8768 if ( !$feedCont.hasClass( "feed-active" ) ) { 8769 activateFeed( $feedCont ); 8770 } 8771 } ) 8772 .addClass( hasVisibilityHandler ); 8773 } 8774 } 8775 } else if ( !$details.attr( "open" ) ) { 8776 activate = false; 8777 $elm.empty().addClass( "waiting" ); 8778 $details 8779 .children( "summary" ) 8780 .on( "click.wb-feeds", function( event ) { 8781 var $summary = $( event.currentTarget ).off( "click.wb-feeds" ); 8782 activateFeed( $summary.parent().find( feedContSelector ) ); 8783 } ); 8784 } 8785 } 8786 8787 if ( activate ) { 8788 activateFeed( $elm ); 8789 } 8790 8791 return true; 8792 }, 8793 8794 /** 8795 * Activates feed results view 8796 * @method activateFeed 8797 * @param = {jQuery object} $elm Feed container 8798 */ 8799 activateFeed = function( $elm ) { 8800 var result = $elm.data( componentName + "-result" ), 8801 postProcess = $elm.data( componentName + "-postProcess" ), 8802 i, postProcessSelector; 8803 8804 if ( !result ) { 8805 return; 8806 } 8807 8808 $elm.empty() 8809 .removeClass( "waiting" ) 8810 .addClass( "feed-active" ) 8811 .append( result ); 8812 8813 if ( postProcess ) { 8814 for ( i = postProcess.length - 1; i !== -1; i -= 1 ) { 8815 postProcessSelector = postProcess[ i ]; 8816 $elm.find( postProcessSelector ) 8817 .trigger( "wb-init" + postProcessSelector ); 8818 } 8819 } 8820 8821 // Identify that the feed has now been displayed 8822 $elm.trigger( "wb-feed-ready" + selector ); 8823 }; 8824 8825$document.on( "ajax-fetched.wb data-ready.wb-feeds", selector + " " + feedLinkSelector, function( event, context ) { 8826 var eventTarget = event.target, 8827 data, response, responseRaw, 8828 $emlRss, limit, results; 8829 8830 // Filter out any events triggered by descendants 8831 if ( event.currentTarget === eventTarget ) { 8832 $emlRss = $( eventTarget ).parentsUntil( selector ).parent(); 8833 switch ( event.type ) { 8834 case "ajax-fetched": 8835 responseRaw = event.fetch.response; 8836 if ( typeof responseRaw === "string" ) { 8837 response = JSON.parse( responseRaw ); // Assuming we have fetch a JSON document, try to parse it. 8838 } else { 8839 response = responseRaw.get( 0 ); // fetched an HTML or XML document which has been parsed by jQuery and sanitized by DomPurify 8840 } 8841 if ( response.documentElement ) { 8842 limit = getLimit( $emlRss[ Object.keys( $emlRss )[ 0 ] ] ); 8843 data = corsEntry( response, limit ); 8844 } else if ( response.query ) { 8845 results = response.query.results; 8846 if ( !results ) { 8847 data = results.item; // Flicker feeds 8848 if ( !Array.isArray( data ) ) { 8849 data = [ data ]; 8850 } 8851 } else { 8852 data = []; 8853 } 8854 } else { 8855 data = ( response.responseData ) ? response.responseData.feed.entries : response.items || response.feed.entry; 8856 } 8857 break; 8858 default: 8859 data = event.feedsData; 8860 } 8861 8862 // Identify that initialization has completed 8863 // if there are no entries left to process 8864 if ( processEntries.apply( context, [ data ] ) === 0 ) { 8865 wb.ready( $( eventTarget ).closest( selector ), componentName ); 8866 } 8867 } 8868} ); 8869 8870$document.on( "click", selector + " .feed-youtube", function( event ) { 8871 var youTubeOverlaySelector = "#wb-feeds-youtube-lbx", 8872 $youTubeOverlay = $( youTubeOverlaySelector ), 8873 youtubeData = wb.getData( event.currentTarget, "youtube" ), 8874 videoUrl = wb.pageUrlParts.protocol + "//www.youtube.com/watch?v=" + youtubeData.videoId, 8875 videoSource = "<figure class='wb-mltmd'><video title='" + youtubeData.title + "'>" + 8876 "<source type='video/youtube' src='" + videoUrl + "'></source>" + 8877 "</video><figcaption><p>" + youtubeData.title + "</p>" + 8878 "</figcaption></figure>"; 8879 8880 if ( $youTubeOverlay.length === 0 ) { 8881 $youTubeOverlay = $( "<section id='wb-feeds-youtube-lbx' class='mfp-hide modal-dialog modal-content overlay-def'>" + 8882 "<header class='modal-header'><h2 class='modal-title'>" + youtubeData.title + "</h2></header>" + 8883 "<div class='modal-body'>" + 8884 videoSource + 8885 "</div></section>" ).insertAfter( "main" ); 8886 } else { 8887 8888 //Modify lightbox 8889 $youTubeOverlay.find( ".modal-title" ).text( youtubeData.title ); 8890 $youTubeOverlay.find( ".modal-body" ).empty().append( videoSource ); 8891 } 8892 8893 //Temporary fix until lightbox auto initialize the multimedia player 8894 $youTubeOverlay.find( ".wb-mltmd" ).trigger( "wb-init.wb-mltmd" ); 8895 8896 $( document ).trigger( "open.wb-lbx", [ { 8897 src: youTubeOverlaySelector, 8898 type: "inline" 8899 } ] ); 8900} ); 8901 8902$document.on( "click", selector + " .feed-flickr", function( event ) { 8903 var flickrOverlaySelector = "#wb-feeds-flick-lbx", 8904 $flickrOverlay = $( flickrOverlaySelector ), 8905 flickrData = wb.getData( event.currentTarget, "flickr" ), 8906 body = "<img src='" + flickrData.image + "' class='thumbnail center-block' alt='" + flickrData.title + "' /><span>" + 8907 flickrData.description + "</span>"; 8908 8909 if ( $flickrOverlay.length === 0 ) { 8910 $flickrOverlay = $( "<section id='wb-feeds-flick-lbx' class='mfp-hide modal-dialog modal-content overlay-def'>" + 8911 "<header class='modal-header'><h2 class='modal-title'>" + flickrData.title + "</h2></header>" + 8912 "<div class='modal-body'>" + body + "</div></section>" ).insertAfter( "main" ); 8913 } else { 8914 8915 //Modify lightbox 8916 $flickrOverlay.find( ".modal-title" ).text( flickrData.title ); 8917 $flickrOverlay.find( ".modal-body" ).empty().append( body ); 8918 } 8919 8920 $( document ).trigger( "open.wb-lbx", [ { 8921 src: flickrOverlaySelector, 8922 type: "inline" 8923 } ] ); 8924} ); 8925 8926// Bind the init event to the plugin 8927$document.on( "timerpoke.wb " + initEvent, selector, init ); 8928 8929// Add the timer poke to initialize the plugin
8930wb.add( selector ); 8931 8932} )( jQuery, window, wb ); 8933 8934( function( $, window, wb ) { 8935"use strict"; 8936 8937var componentName = "wb-filter", 8938 selector = "." + componentName, 8939 initEvent = "wb-init" + selector, 8940 $document = wb.doc, 8941 filterClass = "wb-fltr-out", 8942 tagFilterClass = "wb-tgfltr-out", 8943 notFilterClassSel = ":not(." + filterClass + "):not(." + tagFilterClass + ")", 8944 inputClass = "wb-fltr-inpt", 8945 dtNameFltrArea = "wbfltrid", 8946 selectorInput = "." + inputClass, 8947 defaults = { 8948 std: { 8949 selector: "li" 8950 }, 8951 grp: { 8952 selector: "li", 8953 section: ">section" 8954 }, 8955 tbl: { 8956 selector: "tr", 8957 section: ">tbody" 8958 }, 8959 tblgrp: { 8960 selector: " th:not([scope])" + notFilterClassSel, 8961 hdnparentuntil: "tbody", 8962 section: ">tbody" 8963 } 8964 }, 8965 i18n, i18nText, 8966 wait, 8967 8968 init = function( event ) { 8969 var elm = wb.init( event, componentName, selector ), 8970 $elm, elmTagName, filterUI, prependUI, 8971 settings, setDefault, 8972 itemsObserver, 8973 inptId, totalEntries, 8974 secSelector, 8975 uiTemplate, uiInpt, uiInfo, 8976 uiNbItems, uiTotal, uiInfoID; 8977 8978 if ( elm ) { 8979 $elm = $( elm ); 8980 elmTagName = elm.nodeName; 8981 uiInfoID = elm.id + "-info"; 8982 8983 if ( [ "DIV", "SECTION", "ARTICLE" ].indexOf( elmTagName ) >= 0 ) { 8984 setDefault = defaults.grp; 8985 prependUI = true; 8986 } else if ( elmTagName === "TABLE" ) { 8987 if ( $elm.find( "tbody" ).length > 1 ) { 8988 setDefault = defaults.tblgrp; 8989 } else { 8990 setDefault = defaults.tbl; 8991 } 8992 } else { 8993 setDefault = defaults.std; 8994 } 8995 8996 settings = $.extend( true, {}, setDefault, window[ componentName ], wb.getData( $elm, componentName ) ); 8997 $elm.data( settings ); 8998 8999 if ( !i18nText ) { 9000 i18n = wb.i18n; 9001 i18nText = { 9002 filter_label: i18n( "fltr-lbl" ), 9003 fltr_info: i18n( "fltr-info" ) 9004 }; 9005 } 9006 9007 Modernizr.addTest( "stringnormalize", "normalize" in String ); 9008 Modernizr.load( { 9009 test: Modernizr.stringnormalize, 9010 nope: [ 9011 "site!deps/unorm" + wb.getMode() + ".js" 9012 ] 9013 } ); 9014 9015 if ( !elm.id ) { 9016 elm.id = wb.getId(); 9017 } 9018 9019 if ( settings.uiTemplate ) { 9020 uiTemplate = document.querySelector( settings.uiTemplate ); 9021 uiInpt = uiTemplate.querySelector( "input[type=search]" ); 9022 9023 if ( uiInpt ) { 9024 uiInfo = uiTemplate.querySelector( ".wb-fltr-info" ); 9025 9026 uiInpt.classList.add( inputClass ); 9027 uiInpt.setAttribute( "data-" + dtNameFltrArea, elm.id ); 9028 uiInpt.setAttribute( "aria-controls", elm.id ); 9029 9030 if ( uiInfo ) { 9031 uiInfoID = uiInfo.id || uiInfoID; 9032 uiInfo.id = uiInfoID; 9033 uiInfo.setAttribute( "role", "status" ); 9034 } 9035 } else { 9036 console.error( componentName + ": " + "an <input type=\"search\"> is required in your UI template." ); 9037 } 9038 9039 if ( settings.source ) { 9040 console.warn( componentName + ": " + "the 'source' option is not compatible with the 'uiTemplate' option. If both options are defined, only 'uiTemplate' will be registered." ); 9041 } 9042 } else { 9043 inptId = elm.id + "-inpt"; 9044 filterUI = $( "<div class=\"input-group\">" + 9045 "<label for=\"" + inptId + "\" class=\"input-group-addon\"><span class=\"glyphicon glyphicon-filter\" aria-hidden=\"true\"></span> " + i18nText.filter_label + "</label>" + 9046 "<input id=\"" + inptId + "\" class=\"form-control " + inputClass + "\" data-" + dtNameFltrArea + "=\"" + elm.id + "\" aria-controls=\"" + elm.id + "\" type=\"search\">" + 9047 "</div>" + 9048 "<p role=\"status\" id=\"" + uiInfoID + "\">" + i18nText.fltr_info + "</p>" ); 9049 9050 if ( settings.source ) { 9051 $( settings.source ).prepend( filterUI ); 9052 } else if ( prependUI ) { 9053 $elm.prepend( filterUI ); 9054 } else { 9055 $elm.before( filterUI ); 9056 } 9057 } 9058 9059 secSelector = ( settings.section || "" ) + " "; 9060 totalEntries = $elm.find( secSelector + settings.selector ).length; 9061 uiNbItems = document.querySelector( "#" + uiInfoID + " [data-nbitem]" ); 9062 uiTotal = document.querySelector( "#" + uiInfoID + " [data-total]" ); 9063 9064 if ( uiNbItems ) { 9065 uiNbItems.textContent = totalEntries; 9066 9067 itemsObserver = new MutationObserver( function() { 9068 uiNbItems.textContent = $elm.find( secSelector + settings.selector + notFilterClassSel ).length; 9069 } ); 9070 9071 itemsObserver.observe( elm, { attributes: true, subtree: true } ); 9072 } 9073 9074 if ( uiTotal ) { 9075 uiTotal.textContent = totalEntries; 9076 } 9077 9078 wb.ready( $elm, componentName ); 9079 } 9080 }, 9081 9082 /* 9083 * Takes in the text from the filter box 9084 * Returns: 9085 * An array of search words 9086 * Special characters are escaped 9087 * Double and single quotes removed 9088 */ 9089 filterQueryParser = function( filter ) { 9090
9091 // Pattern to separate the filter text into "words" 9092 var pattern = /[^\s"]+|"([^"]*)"/gi; 9093 9094 // Make strings safe again for regex 9095 // Source: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions 9096 filter = filter.replace( /[.*+?^${}()|[\]\\]/g, "\\$&" ); 9097 9098 // Apply the word match pattern and return 9099 return filter.match( pattern ); 9100 }, 9101 9102 /** 9103 * Build the Regular Expression that we are going 9104 * to use to filter content 9105 * This involves identifying the type of search being 9106 * applied and then breaking up the search string into 9107 * words 9108 */ 9109 buildSearchFilterRegularExp = function( filterType, filter ) { 9110 9111 var words, wordRegExFilter = filter, 9112 i, i_len; 9113 9114 switch ( filterType ) { 9115 9116 case "and": 9117 words = filterQueryParser( filter ); 9118 if ( words ) { 9119 wordRegExFilter = ".*"; 9120 i_len = words.length; 9121 for ( i = 0; i < i_len; i++ ) { 9122 wordRegExFilter = wordRegExFilter + ( "(?=.*" + words[ i ] + ")" ); 9123 } 9124 } 9125 break; 9126 9127 case "or": // If one word fall back on default 9128 words = filterQueryParser( filter ); 9129 if ( words ) { 9130 wordRegExFilter = words.join( "|" ); 9131 } 9132 break; 9133 9134 default: 9135 break; 9136 9137 } 9138 9139 return new RegExp( wordRegExFilter, "i" ); 9140 9141 }, 9142 9143 filter = function( $field, $elm, settings ) { 9144 var unAccent = function( str ) { 9145 return str.normalize( "NFD" ).replace( /[\u0300-\u036f]/g, "" ); 9146 }, 9147 filter = unAccent( $field.val().trim() ), 9148 fCallBack = settings.filterCallback, 9149 secSelector = ( settings.section || "" ) + " ", 9150 hndParentSelector = settings.hdnparentuntil, 9151 $items = $elm.find( secSelector + settings.selector ), 9152 itemsLength = $items.length, 9153 i, $item, text, searchFilterRegularExp; 9154 9155 $elm.find( "." + filterClass ).removeClass( filterClass ); 9156 9157 searchFilterRegularExp = buildSearchFilterRegularExp( settings.filterType, filter ); 9158 9159 for ( i = 0; i < itemsLength; i += 1 ) { 9160 $item = $items.eq( i ); 9161 text = unAccent( $item.text() ); 9162 9163 if ( !searchFilterRegularExp.test( text ) ) { 9164 if ( hndParentSelector ) { 9165 $item = $item.parentsUntil( hndParentSelector ); 9166 } 9167 $item.addClass( filterClass ); 9168 } 9169 } 9170 9171 if ( !fCallBack || typeof fCallBack !== "function" ) { 9172 fCallBack = filterCallback; 9173 } 9174 fCallBack.apply( this, arguments ); 9175 9176 $elm.trigger( "wb-contentupdated" ); 9177 }, 9178 filterCallback = function( $field, $elm, settings ) { 9179 var $sections = $elm.find( settings.section ), 9180 sectionsLength = $sections.length, 9181 fndSelector = notFilterClassSel + settings.selector, 9182 s, $section; 9183 9184 for ( s = 0; s < sectionsLength; s += 1 ) { 9185 $section = $sections.eq( s ); 9186 if ( $section.find( fndSelector ).length === 0 ) { 9187 $section.addClass( filterClass ); 9188 } 9189 } 9190 }; 9191 9192$document.on( "keyup", selectorInput, function( event ) { 9193 var target = event.target, 9194 $input = $( target ), 9195 $elm = $( "#" + $input.data( dtNameFltrArea ) ); 9196 9197 if ( wait ) { 9198 clearTimeout( wait ); 9199 } 9200 wait = setTimeout( filter.bind( this, $input, $elm, $elm.data() ), 250 ); 9201 9202} ); 9203 9204$document.on( "timerpoke.wb " + initEvent, selector, init ); 9205
9206wb.add( selector ); 9207} )( jQuery, window, wb ); 9208 9209/** 9210 * @title WET-BOEW Footnotes 9211 * @overview Provides a consistent, accessible way of handling footnotes across websites. 9212 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 9213 * @author @EricDunsworth 9214 */ 9215( function( $, window, wb ) { 9216"use strict"; 9217 9218/* 9219 * Variable and function definitions. 9220 * These are global to the plugin - meaning that they will be initialized once per page, 9221 * not once per instance of plugin on the page. So, this is a good place to define 9222 * variables that are common to all instances of the plugin on a page. 9223 */ 9224var componentName = "wb-fnote", 9225 selector = "." + componentName, 9226 modFlag = "data-" + componentName, 9227 initEvent = "wb-init" + selector, 9228 setFocusEvent = "setfocus.wb", 9229 $document = wb.doc, 9230 9231 /** 9232 * @method init 9233 * @param {jQuery Event} event Event that triggered the function call 9234 */ 9235 init = function( event ) { 9236 9237 // Start initialization 9238 // returns DOM object = proceed with init 9239 // returns undefined = do not proceed with init (e.g., already initialized) 9240 var elm = wb.init( event, componentName, selector ), 9241 $elm, footnoteDd, footnoteDt, i, len, dd, dt; 9242 9243 if ( elm ) { 9244 $elm = $( elm ); 9245 footnoteDd = elm.getElementsByTagName( "dd" ); 9246 footnoteDt = elm.getElementsByTagName( "dt" ); 9247 9248 // Set initial event handlers for return to referrer links 9249 len = footnoteDd.length; 9250 for ( i = 0; i !== len; i += 1 ) { 9251 dd = footnoteDd[ i ]; 9252 dt = footnoteDt[ i ]; 9253 dd.setAttribute( "tabindex", "-1" ); 9254 dt.id = dd.id + "-dt"; 9255 } 9256 9257 // Remove "first/premier/etc"-style text from certain footnote return links (via the child spans that hold those bits of text) 9258 $elm.find( "dd p.fn-rtn a span span" ).remove(); 9259 9260 // Listen for footnote reference links that get clicked 9261 $document.on( "click", "main :not(" + selector + ") sup a.fn-lnk", function( event ) { 9262 var eventTarget = event.target, 9263 which = event.which, 9264 refId, $refLinkDest; 9265 9266 // Ignore middle/right mouse button 9267 if ( !which || which === 1 ) { 9268 refId = "#" + wb.jqEscape( eventTarget.getAttribute( "href" ).substring( 1 ) ); 9269 $refLinkDest = $document.find( refId ); 9270 9271 $refLinkDest.find( "p.fn-rtn a" ) 9272 .attr( "href", "#" + eventTarget.parentNode.id ) 9273 .attr( modFlag, true ); 9274 9275 // Assign focus to $refLinkDest 9276 $refLinkDest.trigger( setFocusEvent ); 9277 return false; 9278 } 9279 } ); 9280 9281 // Listen for footnote return links that get clicked 9282 $document.on( "click", selector + " dd p.fn-rtn a", function( event ) { 9283 var which = event.which, 9284 $elmTarget = $( event.target ), 9285 ref, 9286 refId, 9287 refIdSrc, 9288 refIdDashIdx, 9289 searchRefId; 9290 9291 // Ignore middle/right mouse button 9292 if ( !which || which === 1 ) { 9293 ref = $elmTarget.attr( "href" ); 9294 9295 // Focus on associated referrer link (if the return link points to an ID) 9296 if ( ref.charAt( 0 ) === "#" ) { 9297 refId = wb.jqEscape( ref.substring( 1 ) ); 9298 9299 // When first clicked, ensure we send the user on the first instance when the id follow the recommend pattern 9300 refIdDashIdx = refId.indexOf( "-" ); 9301 if ( refIdDashIdx !== -1 && !$elmTarget.attr( modFlag ) ) { 9302 searchRefId = refId.substring( 0, refIdDashIdx + 1 ); 9303 refIdSrc = wb.jqEscape( $( "sup[id^='" + searchRefId + "']:first()" ).attr( "id" ) ); 9304 if ( !refIdSrc || refId !== refIdSrc ) { 9305 console.warn( componentName + " - Relink first reference of " + ref + " for #" + refIdSrc ); 9306 refId = refIdSrc; 9307 $elmTarget 9308 .attr( "href", "#" + refId ) 9309 .attr( modFlag, true ); 9310 } 9311 } 9312 9313 // Assign focus to the link 9314 $document.find( "#" + refId + " a" ).trigger( setFocusEvent ); 9315 return false; 9316 } 9317 } 9318 } ); 9319 9320 // Identify that initialization has completed 9321 wb.ready( $elm, componentName ); 9322 } 9323 }; 9324 9325// Bind the init event of the plugin 9326$document.on( "timerpoke.wb " + initEvent, selector, init ); 9327 9328// Add the timer poke to initialize the plugin 9329wb.add( selector ); 9330 9331} )( jQuery, window, wb ); 9332 9333/** 9334 * @title WET-BOEW Form validation 9335 * @overview Provides generic validation and error message handling for Web forms. 9336 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 9337 * @author @pjackson28 9338 */ 9339( function( $, window, document, wb ) { 9340"use strict"; 9341 9342/* 9343* Variable and function definitions. 9344* These are global to the plugin - meaning that they will be initialized once per page, 9345* not once per instance of plugin on the page. So, this is a good place to define 9346* variables that are common to all instances of the plugin on a page. 9347*/ 9348var componentName = "wb-frmvld", 9349 selector = "." + componentName, 9350 initEvent = "wb-init" + selector, 9351 setFocusEvent = "setfocus.wb", 9352 $document = wb.doc, 9353 idCount = 0, 9354 i18n, i18nText, 9355 9356 defaults = { 9357 hdLvl: "h2", 9358 ignore: ":hidden" 9359 }, 9360 9361 /** 9362 * @method init 9363 * @param {jQuery Event} event Event that triggered the function call 9364 */ 9365 init = function( event ) { 9366 9367 // Start initialization 9368 // returns DOM object = proceed with init 9369 // returns undefined = do not proceed with init (e.g., already initialized) 9370 var eventTarget = wb.init( event, componentName, selector ), 9371 elmId, modeJS; 9372 9373 if ( eventTarget ) { 9374 elmId = eventTarget.id; 9375 9376 // Ensure there is a unique id on the element 9377 if ( !elmId ) { 9378 elmId = componentName + "-id-" + idCount; 9379 idCount += 1; 9380 eventTarget.id = elmId; 9381 } 9382 9383 // Read the selector node for parameters 9384 modeJS = wb.getMode() + ".js"; 9385 9386 // Only initialize the i18nText once 9387 if ( !i18nText ) { 9388 i18n = wb.i18n; 9389 i18nText = { 9390 colon: i18n( "colon" ), 9391 hyphen: i18n( "hyphen" ), 9392 error: i18n( "err" ), 9393 errorFound: i18n( "err-fnd" ), 9394 errorsFound: i18n( "errs-fnd" ), 9395 formNotSubmitted: i18n( "frm-nosubmit" ), 9396 errorCorrect: i18n( "err-correct" ) 9397 }; 9398 } 9399 9400 Modernizr.load( { 9401 load: [ "site!deps/jquery.validate" + modeJS ], 9402 testReady: function() { 9403 return ( $.validator ); 9404 }, 9405 complete: function() { 9406 Modernizr.load( { 9407 9408 // For loading multiple dependencies 9409 load: [ "site!deps/additional-methods" + modeJS ], 9410 testReady: function() { 9411 return ( $.validator.methods.bic ); 9412 }, 9413 complete: function() { 9414 var $elm = $( "#" + elmId ), 9415 $form = $elm.find( "form" ), 9416 formDOM = $form.get( 0 ), 9417 formId = $form.attr( "id" ), 9418 labels = formDOM.getElementsByTagName( "label" ), 9419 submitted = false, 9420 showSummary = false, 9421 errorFormId = "errors-" + ( !formId ? "default" : formId ), 9422 settings = $.extend( 9423 true, 9424 {}, 9425 defaults, 9426 window[ componentName ], 9427 wb.getData( $elm, componentName ) 9428 ), 9429 summaryHeading = settings.hdLvl, 9430 i, len, validator; 9431 9432 if ( wb.lang === "fr" ) { 9433 9434 // alphanumeric regex is changed to allow french characters; 9435 $.validator.addMethod( "alphanumeric", function( value, element ) { 9436 return this.optional( element ) || /^[a-zà âçéèêëîïôûùüÿæÅ0-9_]+$/i.test( value ); 9437 }, "Letters, numbers, and underscores only please." ); 9438 9439 // error french text is adjusted to remove the word "spaces" 9440 $.extend( $.validator.messages, {
9441 alphanumeric: "Veuillez fournir seulement des lettres, nombres et soulignages." 9442 } ); 9443 } 9444 9445 // Append the aria-live region (for provide message updates to screen readers) 9446 $elm.append( "<div class='arialive wb-inv' aria-live='polite' aria-relevant='all'></div>" ); 9447 9448 // Add space to the end of the labels (so separation between label and error when CSS turned off) 9449 len = labels.length; 9450 for ( i = 0; i !== len; i += 1 ) { 9451 labels[ i ].insertAdjacentHTML( "beforeend", " " ); 9452 } 9453 9454 // Hide "required" label text in older forms from screen readers 9455 // Prevents redundant "required" announcements on semantically-required fields whose labels mention they're required 9456 $form.find( "strong.required:not([aria-hidden='true'])" ).each( function() { 9457 const $requiredText = $( this ), 9458 $label = $requiredText.closest( "label" ), 9459 fieldId = $label.attr( "for" ); 9460 let $field = fieldId ? $( "#" + fieldId ) : $label.find( ":input" ).first(); 9461 9462 // If the label's field has yet to be found, look for fields that refer to the label via aria-labelledby 9463 if ( !$field.length ) { 9464 const labelId = $label.attr( "id" ) || $requiredText.closest( "id" ); 9465 $field = $form.find( "[aria-labelledby~='" + labelId + "']:input" ).first(); 9466 } 9467 9468 // Hide the "required" text if its field is semantically-required 9469 if ( $field.is( "[required], [aria-required='true']" ) ) { 9470 $requiredText.attr( "aria-hidden", "true" ); 9471 } 9472 } ); 9473 9474 // The jQuery validation plug-in in action 9475 validator = $form.validate( { 9476 meta: "validate", 9477 focusInvalid: false, 9478 ignore: settings.ignore, 9479 9480 // Set the element which will wrap the inline error messages 9481 errorElement: "strong", 9482 9483 // Location for the inline error messages 9484 // In this case we will place them in the associated label element 9485 errorPlacement: function( $error, $element ) { 9486 var type = $element.attr( "type" ), 9487 group = $element.attr( "data-rule-require_from_group" ), 9488 $fieldset, $legend; 9489 9490 $error.data( "element-id", $element.attr( "id" ) ); 9491 if ( type ) { 9492 type = type.toLowerCase(); 9493 if ( type === "radio" || type === "checkbox" ) { 9494 $fieldset = $element.closest( "fieldset" ); 9495 if ( $fieldset.length !== 0 ) { 9496 $legend = $fieldset.find( "legend" ).first(); 9497 if ( $legend.length !== 0 && $fieldset.find( "input[name='" + $element.attr( "name" ) + "']" ) !== 1 ) { 9498 $error.appendTo( $legend ); 9499 return; 9500 } 9501 } 9502 } 9503 } 9504 9505 if ( group ) { 9506 $fieldset = $element.closest( "fieldset" ); 9507 if ( $fieldset.length !== 0 ) { 9508 $legend = $fieldset.find( "legend" ).first(); 9509 if ( $legend.length !== 0 && $fieldset.find( "input[name='" + $element.attr( "name" ) + "']" ) !== 1 ) { 9510 var $strong = $legend.find( "strong.error" ), 9511 id = $legend.attr( "id" ); 9512 9513 if ( $strong.length > 0 ) { 9514 $strong.remove(); 9515 } 9516 9517 if ( !id ) { 9518 id = "required-group-" + idCount; 9519 idCount += 1; 9520 9521 $legend.attr( "id", id ); 9522 } 9523 9524 $error.data( "element-id", id ); 9525 $error.attr( "id", id ); 9526 $error.appendTo( $legend ); 9527 9528 return; 9529 } 9530 } 9531 } 9532 9533 //Std If we have a label and the input field is inside the label 9534 // need to add a css-implicite-input 9535 if ( $form.find( "label" ).find( ".wb-server-error + input.css-implicite-input[name='" + $element.attr( "name" ) + "']" ).length > 0 ) { 9536 $error.insertBefore( $form.find( ".wb-server-error + input.css-implicite-input[name='" + $element.attr( "name" ) + "']" ) ); 9537 return; 9538 } 9539 9540 $error.appendTo( $form.find( "label[for='" + $element.attr( "id" ) + "']" ) ); 9541 return; 9542 }, 9543 9544 // Create our error summary that will appear before the form 9545 showErrors: function( errorMap ) { 9546 this.defaultShowErrors(); 9547 var $errors = $form.find( ".wb-server-error, strong.error" ).filter( ":not(:hidden)" ), 9548 $errorfields = $form.find( "input.error, select.error, textarea.error" ), 9549 prefixStart = "<span class='prefix'>" + i18nText.error + " ", 9550 prefixEnd = i18nText.colon + " </span>", 9551 separator = i18nText.hyphen, 9552 ariaLive = $form.closest( ".wb-frmvld" ).find( ".arialive" )[ 0 ], 9553 $summaryContainer, summary, key, i, len, $error, prefix, $fieldName, $f
9553ieldset, label, labelString, el; 9554 9555 // Correct the colouring of fields that are no longer invalid 9556 $form 9557 .find( ".has-error [aria-invalid=false]" ) 9558 .closest( ".has-error" ) 9559 .removeClass( "has-error" ); 9560 9561 if ( $errors.length !== 0 ) { 9562 9563 // Post process 9564 summary = "<" + summaryHeading + ">" + 9565 i18nText.formNotSubmitted + $errors.length + 9566 ( 9567 $errors.length !== 1 ? 9568 i18nText.errorsFound : 9569 i18nText.errorFound 9570 ) + "</" + summaryHeading + "><ul>"; 9571 $errorfields 9572 .closest( ".form-group" ) 9573 .addClass( "has-error" ); 9574 len = $errors.length; 9575 for ( i = 0; i !== len; i += 1 ) { 9576 $error = $errors.eq( i ); 9577 prefix = prefixStart + ( i + 1 ) + prefixEnd; 9578 $fieldName = $error.closest( "label" ).find( ".field-name" ); 9579 9580 // Try to find the field name in the legend (if one exists) 9581 if ( $fieldName.length === 0 ) { 9582 $fieldset = $error.closest( "fieldset" ); 9583 if ( $fieldset.length !== 0 ) { 9584 $fieldName = $fieldset.find( "legend .field-name" ); 9585 } 9586 } 9587 9588 $error.find( "span.prefix" ).detach(); 9589 9590 //Verify if it is a wb-server-error 9591 if ( $errors[ i ].classList.contains( "wb-server-error" ) ) { 9592 if ( $errors[ i ].id ) { 9593 var myParent = document.getElementById( $errors[ i ].id ).parentElement; 9594 if ( myParent === null ) { 9595 summary += "<li><a>" + prefix + ( $fieldName.length !== 0 ? $fieldName.html() + separator : "" ) + $error.text() + separator + i18nText.errorCorrect + "</a></li>"; 9596 } else { 9597 if ( myParent.hasAttribute( "for" ) && myParent.getAttribute( "for" ).length > 0 ) { 9598 summary += "<li><a href='#" + myParent.getAttribute( "for" ) + "'>" + prefix + ( $fieldName.length !== 0 ? $fieldName.html() + separator : "" ) + $error.text() + separator + i18nText.errorCorrect + "</a></li>"; 9599 } else { 9600 if ( myParent.getElementsByTagName( "input" )[ 0 ] && myParent.getElementsByTagName( "input" )[ 0 ].name.length > 0 ) { 9601 summary += "<li><a href='#" + myParent.getElementsByTagName( "input" )[ 0 ].id + "'>" + prefix + ( $fieldName.length !== 0 ? $fieldName.html() + separator : "" ) + $error.text() + separator + i18nText.errorCorrect + "</a></li>"; 9602 } else { 9603 if ( myParent.tagName === ( "LEGEND" ) && ( myParent.parentElement.getElementsByTagName( "input" )[ 0 ].type === "checkbox" || myParent.parentElement.getElementsByTagName( "input" )[ 0 ].type === "radio" && myParent.parentElement.getElementsByTagName( "input" )[ 0 ].name.length > 0 ) ) { 9604 summary += "<li><a href='#" + myParent.parentElement.getElementsByTagName( "input" )[ 0 ].id + "'>" + prefix + ( $fieldName.length !== 0 ? $fieldName.html() + separator : "" ) + $error.text() + separator + i18nText.errorCorrect + "</a></li>"; 9605 } else { 9606 summary += "<li><a>" + prefix + ( $fieldName.length !== 0 ? $fieldName.html() + separator : "" ) + $error.text() + separator + i18nText.errorCorrect + "</a></li>"; 9607 } 9608 } 9609 } 9610 } 9611 $error.html( "<strong>" + prefix + $error.text() + "</strong>" ); 9612 } 9613 9614 //Force display of summary if wb-server-error present. 9615 showSummary = true; 9616 } else { 9617 summary += "<li><a href='#" + $error.data( "element-id" ) + "'>" + prefix + ( $fieldName.length !== 0 ? $fieldName.html() + separator : "" ) + $error.text() + "</a></li>"; 9618 $error.html( "<span class='label label-danger'>" + prefix + $error.text() + "</span>" ); 9619 } 9620 } 9621 summary += "</ul>"; 9622 9623 if ( !submitted ) { 9624 9625 // Update the aria-live region as necessary 9626 i = 0; 9627 for ( key in errorMap ) { 9628 if ( Object.prototype.hasOwnProperty.call( errorMap, key ) ) { 9629 i += 1; 9630 break; 9631 } 9632 } 9633 if ( i !== 0 ) { 9634 len = $errors.length; 9635 for ( i = 0; i !== len; i += 1 ) { 9636 label = $errors[ i ].parentNode; 9637 if ( label.getAttribute( "for" ) === key ) { 9638 el = document.getElementById( key ); 9639 if ( el.matches( ":focus" ) ) { 9640 ariaLive.innerHTML = ""; 9641 } else { 9642 labelString = label.innerHTML; 9643 if ( labelString !== ariaLive.innerHTML ) { 9644 ariaLive.innerHTML = labelString; 9645 } 9646 } 9647 break; 9648 } 9649 } 9650 } else if ( ariaLive.innerHTML.length !== 0 ) { 9651 ariaLive.innerHTML = ""; 9652 } 9653 } 9654 if ( showSummary ) { 9655 9656 // Delay updating the summary container in case a summary link was clicked 9657 setTimeout( function() { 9658 $summaryContainer = $form.find( "#" + errorFormId ); 9659 9660 // Output our error summary and place it in the error container 9661 // Create our container if one doesn't already exist 9662 if ( $summaryContainer.length === 0 ) { 9663 $summaryContainer = $( "<section id='" + errorFormId + "' class='alert alert-danger' tabindex='-1'>
9663" + summary + "</section>" ).prependTo( $form ); 9664 9665 // Replace current error summary content only if it is different 9666 // from the incoming error summary content 9667 } else if ( $summaryContainer.html() !== summary.replace( /'/g, "\"" ).replace( / /g, " " ) ) { 9668 9669 // if the summary container is currently focused then 9670 // we will focus the last element in the summary 9671 // since we recreate the error list on blur/change 9672 var $isFocused = $summaryContainer.find( "a" ).is( ":focus" ); 9673 $summaryContainer.empty().append( summary ); 9674 if ( $isFocused ) { 9675 $summaryContainer.find( "a" ).last().trigger( "focus" ); 9676 } 9677 } 9678 9679 // Put focus on the error if the errors are generated by an attempted form submission 9680 if ( submitted ) { 9681 9682 // Assign focus to $summaryContainer 9683 $summaryContainer.trigger( setFocusEvent ); 9684 submitted = false; 9685 } 9686 }, 100 ); 9687 } 9688 } else { 9689 9690 // Update the aria-live region as necessary 9691 if ( ariaLive.innerHTML.length !== 0 ) { 9692 ariaLive.innerHTML = ""; 9693 } 9694 $form.find( "#" + errorFormId ).detach(); 9695 showSummary = false; 9696 } 9697 }, 9698 9699 /* End of showErrors() */ 9700 9701 invalidHandler: function() { 9702 submitted = true; 9703 showSummary = true; 9704 } 9705 9706 } ); /* end of validate() */ 9707 9708 /* fixes validation issue (see PR #7913) */ 9709 $form.on( "change", "input[type=date], input[type=file], select", function() { 9710 $form.validate().element( this ); 9711 } ); 9712 9713 // Clear the form and remove error messages on reset 9714 $document.on( "click", selector + " input[type=reset]", function( event ) { 9715 var which = event.which, 9716 ariaLive; 9717 9718 // Ignore middle/right mouse buttons 9719 if ( !which || which === 1 ) { 9720 validator.resetForm(); 9721 $( "#" + errorFormId ).detach(); 9722 9723 ariaLive = $form.closest( ".wb-frmvld" ).find( ".arialive" )[ 0 ]; 9724 if ( ariaLive.innerHTML.length !== 0 ) { 9725 ariaLive.innerHTML = ""; 9726 } 9727 9728 // Correct the colouring of fields that are no longer invalid 9729 $form.find( ".has-error" ).removeClass( "has-error" ); 9730 } 9731 } ); 9732 9733 //Trigger validation on wb-server-error 9734 $form.find( ".wb-server-error" ).filter( ":not( :hidden )" ).parent().each( function() { 9735 if ( this.attributes.for && this.attributes.for.value.length > 0 ) { 9736 $form.validate().element( $( "[id =" + this.attributes.for.value + "]" ) ); 9737 } else if ( $( this ).find( "input" )[ 0 ] ) { 9738 $form.validate().element( $( this ).find( "input" )[ 0 ] ); 9739 } else if ( $( this ).next( ".radio, .checkbox" ).children( "label" ).children( "input" )[ 0 ] ) { 9740 if ( $( this ).find( $( this ).next( ".radio, .checkbox" ).children( "label" ).children( "input" )[ 0 ].id ) ) { 9741 $form.validate().element( $( this ).next( ".radio, .checkbox" ).children( "label" ).children( "input" )[ 0 ] ); 9742 } 9743 } else if ( $( this ).next( ".radio-inline, .checkbox-inline, .label-inline" ).children( "input" )[ 0 ] ) { 9744 if ( $( this ).find( $( this ).next( ".radio-inline, .checkbox-inline, .label-inline" ).children( "input" )[ 0 ].id ) ) { 9745 $form.validate().element( $( this ).next( ".radio-inline, .checkbox-inline, .label-inline" ).children( "input" )[ 0 ] ); 9746 } 9747 } 9748 } ); 9749 9750 // Tell the i18n file to execute to run any $.validator extends 9751 $form.trigger( "formLanguages.wb" ); 9752 9753 // Identify that initialization has completed 9754 wb.ready( $( eventTarget ), componentName ); 9755 } 9756 } ); 9757 } 9758 } ); 9759 } 9760 }; 9761 9762// Bind the init event of the plugin 9763$document.on( "timerpoke.wb " + initEvent, selector, init ); 9764 9765// Add the timer poke to initialize the plugin 9766wb.add( selector ); 9767 9768} )( jQuery, window, document, wb ); 9769 9770/* 9771 * @title WET-BOEW Geomap 9772 * @overview Displays a dynamic map over which information from additional sources can be overlaid. 9773 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 9774 * @author @pjackson28 9775 */ 9776( function( $, wb ) { 9777"use strict"; 9778 9779var componentName = "wb-geomap", 9780 selector = "." + componentName, 9781 initEvent = "wb-init" + selector, 9782 $document = wb.doc, 9783 9784 /** 9785 * @method init 9786 * @param {jQuery Event} event Event that triggered the function call 9787 */ 9788 init = function( event ) { 9789 9790 // Start initialization 9791 // returns DOM object = proceed with init 9792 // returns undefined = do not proceed with init (e.g., already initialized) 9793 var elm = wb.init( event, componentName, selector ), 9794 $elm, $map, $leg, modeJS; 9795 9796 if ( elm ) { 9797 $elm = $( elm ); 9798 $map = $elm.find( ".wb-geomap-map" ); 9799 $leg = $elm.find( ".wb-geomap-legend" ); 9800 9801 // Initialize the map height for the loader 9802 $map.height( $map.width() * 0.8 ); 9803 $map.append( "<div class='geomap-progress'><span class='wb-inv'>" + wb.i18n( "load" ) + "</span></div>" ); 9804 $leg.append( "<div class='skeleton-lgnd-1'><span class='skeleton-lgnd-3'></span></div>" + 9805 "<div class='skeleton-lgnd-2'><span class='skeleton-lgnd-3'></span></div>" + 9806 "<div class='skeleton-lgnd-2'><span class='skeleton-lgnd-3'></span></div>" + 9807 "<div class='skeleton-lgnd-2'><span class='skeleton-lgnd-3'></span></div>" + 9808 "<div class='skeleton-lgnd-1'><span class='skeleton-lgnd-3'></span></div>" ); 9809 9810 modeJS = wb.getMode() + ".js"; 9811 9812 Modernizr.load( [ { 9813 9814 // For loading multiple dependencies 9815 both: [ 9816 "site!deps/proj4" + modeJS, 9817 "site!deps/ol" + modeJS, 9818 "site!deps/geomap-lib" + modeJS 9819 ], 9820 complete: function() { 9821 $elm.trigger( "geomap.wb" ); 9822 } 9823 } ] ); 9824 } 9825 }; 9826 9827// Bind the init function to the timerpoke event 9828$document.on( "timerpoke.wb " + initEvent, selector, init ); 9829 9830// Add the timer poke to initialize the plugin
9831wb.add( selector ); 9832 9833} )( jQuery, wb ); 9834 9835/** 9836 * @title WET-BOEW JSON Fetch [ json-fetch ] 9837 * @overview Load and filter data from a JSON file 9838 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 9839 * @author @duboisp 9840 */ 9841/*global jsonpointer */ 9842( function( $, wb, DOMPurify ) { 9843"use strict"; 9844 9845/* 9846 * Variable and function definitions. 9847 * These are global to the plugin - meaning that they will be initialized once per page, 9848 * not once per instance of plugin on the page. So, this is a good place to define 9849 * variables that are common to all instances of the plugin on a page. 9850 */ 9851var $document = wb.doc, 9852 component = "json-fetch", 9853 fetchEvent = component + ".wb", 9854 jsonCache = { }, 9855 jsonCacheBacklog = { }, 9856 completeJsonFetch = function( callerId, refId, response, status, xhr, selector, fetchedOpts ) { 9857 if ( !window.jsonpointer ) { 9858 9859 // JSON pointer library is loaded but not executed in memory yet, we need to wait a tick before to continue 9860 setTimeout( function() { 9861 completeJsonFetch( callerId, refId, response, status, xhr, selector, fetchedOpts ); 9862 }, 100 ); 9863 return false; 9864 } 9865 if ( selector ) { 9866 try { 9867 response = jsonpointer.get( response, selector ); 9868 } catch ( ex ) { 9869 console.error( "JSON fetch - Bad JSON selector: " + selector ); 9870 console.error( response ); 9871 console.error( $( "#" + callerId ).get( 0 ) ); 9872 } 9873 } 9874 $( "#" + callerId ).trigger( { 9875 type: "json-fetched.wb", 9876 fetch: { 9877 response: response, 9878 status: status, 9879 xhr: xhr, 9880 refId: refId, 9881 fetchedOpts: fetchedOpts 9882 } 9883 }, this ); 9884 }; 9885 9886// Event binding 9887$document.on( fetchEvent, function( event ) { 9888 9889 var caller = event.element || event.target, 9890 fetchOpts = event.fetch || { url: "" }, 9891 urlParts = fetchOpts.url.split( "#" ), 9892 url = urlParts[ 0 ], 9893 fetchNoCache = fetchOpts.nocache, 9894 fetchNoCacheKey = fetchOpts.nocachekey || wb.cacheBustKey || "wbCacheBust", 9895 fetchNoCacheValue, 9896 fetchCacheURL, 9897 hashPart, 9898 datasetName, 9899 selector = urlParts[ 1 ] || false, 9900 callerId, refId = fetchOpts.refId, 9901 cachedResponse; 9902 9903 // Filter out any events triggered by descendants 9904 if ( caller === event.target || event.currentTarget === event.target ) { 9905 9906 if ( !caller.id ) { 9907 caller.id = wb.getId(); 9908 } 9909 callerId = caller.id; 9910 9911 if ( selector ) { 9912 9913 // If a Dataset Name exist let it managed by wb-jsonpatch plugin 9914 hashPart = selector.split( "/" ); 9915 datasetName = hashPart[ 0 ]; 9916 9917 // A dataset name must start with "[" character, if it is a letter, then follow JSON Schema (to be implemented) 9918 if ( datasetName.charCodeAt( 0 ) === 91 ) { 9919 9920 // Let the wb-jsonpatch plugin to manage it 9921 $( "#" + callerId ).trigger( { 9922 type: "postpone.wb-jsonmanager", 9923 postpone: { 9924 callerId: callerId, 9925 refId: refId, 9926 dsname: datasetName, 9927 selector: selector.substring( datasetName.length ) 9928 } 9929 } ); 9930 return; 9931 } 9932 fetchOpts.url = url; 9933 } 9934 9935 if ( fetchNoCache ) { 9936 if ( fetchNoCache === "nocache" ) { 9937 fetchNoCacheValue = wb.guid(); 9938 } else { 9939 fetchNoCacheValue = wb.sessionGUID(); 9940 } 9941 fetchCacheURL = fetchNoCacheKey + "=" + fetchNoCacheValue; 9942 9943 if ( url.indexOf( "?" ) !== -1 ) { 9944 url = url + "&" + fetchCacheURL; 9945 } else { 9946 url = url + "?" + fetchCacheURL; 9947 } 9948 fetchOpts.url = url; 9949 } 9950 9951 Modernizr.load( { 9952 load: "site!deps/jsonpointer" + wb.getMode() + ".js", 9953 complete: function() { 9954 9955 // Ensure this fetch has an URL. There is no URL when only using dataset name (a virtual JSON file). 9956 if ( !url ) { 9957 return; 9958 } 9959 9960 if ( !fetchOpts.nocache ) { 9961 cachedResponse = jsonCache[ url ]; 9962 9963 if ( cachedResponse ) { 9964 completeJsonFetch( callerId, refId, cachedResponse, "success", undefined, selector, fetchOpts ); 9965 return; 9966 } else { 9967 if ( !jsonCacheBacklog[ url ] ) { 9968 jsonCacheBacklog[ url ] = [ ]; 9969 } else { 9970 jsonCacheBacklog[ url ].push( { 9971 "callerId": callerId, 9972 "refId": refId, 9973 "selector": selector 9974 } ); 9975 return; 9976 } 9977 } 9978 } 9979 9980 // Ensure we only receive JSON data and don't allow jsonp 9981 // jQuery will raise an error if other data format is received 9982 fetchOpts.dataType = "json"; 9983 if ( fetchOpts.jsonp ) { 9984 fetchOpts.jsonp = false;
9985 } 9986 9987 // Sending Data 9988 if ( fetchOpts.data ) { 9989 try { 9990 fetchOpts.data = ( typeof fetchOpts.data === "string" ? fetchOpts.data : JSON.stringify( fetchOpts.data ) ); 9991 } catch ( err ) { 9992 throw "JSON fetch - Data being sent to server - " + err; 9993 } 9994 9995 fetchOpts.method = fetchOpts.method || "POST"; 9996 fetchOpts.contentType = fetchOpts.contentType || "application/json"; 9997 } 9998 9999 $.ajax( fetchOpts ) 10000 .done( function( response, status, xhr ) { 10001 var i, i_len, i_cache, backlog; 10002 10003 if ( !fetchOpts.nocache ) { 10004 try { 10005 jsonCache[ url ] = response; 10006 } catch ( error ) { 10007 return; 10008 } 10009 } 10010 10011 completeJsonFetch( callerId, refId, response, status, xhr, selector, fetchOpts ); 10012 10013 if ( jsonCacheBacklog[ url ] ) { 10014 backlog = jsonCacheBacklog[ url ]; 10015 10016 i_len = backlog.length; 10017 10018 for ( i = 0; i !== i_len; i += 1 ) { 10019 i_cache = backlog[ i ]; 10020 completeJsonFetch( i_cache.callerId, i_cache.refId, response, status, xhr, i_cache.selector, fetchOpts ); 10021 } 10022 } 10023 10024 } ) 10025 .fail( function( xhr, status, error ) { 10026 xhr.responseText = DOMPurify.sanitize( xhr.responseText ); 10027 $( "#" + callerId ).trigger( { 10028 type: "json-failed.wb", 10029 fetch: { 10030 xhr: xhr, 10031 status: status, 10032 error: error, 10033 refId: refId, 10034 fetchOpts: fetchOpts 10035 } 10036 }, this ); 10037 }, this ); 10038 } 10039 } ); 10040 } 10041} ); 10042 10043} )( jQuery, wb, DOMPurify ); 10044 10045/** 10046 * @title WET-BOEW Lightbox 10047 * @overview Helps build a photo gallery on a web page. 10048 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 10049 * @author @pjackson28 10050 */ 10051( function( $, DOMPurify, window, document, wb, undef ) { 10052"use strict"; 10053 10054/* 10055 * Variable and function definitions. 10056 * These are global to the plugin - meaning that they will be initialized once per page, 10057 * not once per instance of plugin on the page. So, this is a good place to define 10058 * variables that are common to all instances of the plugin on a page. 10059 */ 10060var componentName = "wb-lbx", 10061 selector = "." + componentName, 10062 initEvent = "wb-init" + selector, 10063 setFocusEvent = "setfocus.wb", 10064 dependenciesLoadedEvent = "deps-loaded" + selector, 10065 modalHideSelector = "#wb-tphp, body > header, body > main, body > footer", 10066 $document = wb.doc, 10067 callbacks, i18n, i18nText, 10068 defaults = { 10069 10070 // exclude 'times' from screen reader with aria-hidden span 10071 closeMarkup: "<button type='button' class='mfp-close'><span class='mfp-close' aria-hidden='true'>×</span><span class='wb-inv'>%title%</span></button>" 10072 }, 10073 10074 /** 10075 * @method init 10076 * @param {jQuery Event} event Event that triggered the function call 10077 */ 10078 init = function( event ) { 10079 10080 // Start initialization 10081 // returns DOM object = proceed with init 10082 // returns undefined = do not proceed with init (e.g., already initialized) 10083 var elm = wb.init( event, componentName, selector ), 10084 elmId; 10085 10086 if ( elm ) { 10087 elmId = elm.id; 10088 10089 // Ensure the dependencies are loaded first 10090 $document.one( dependenciesLoadedEvent, function() { 10091 var elm = document.getElementById( elmId ), 10092 $elm = $( elm ), 10093 settings = {}, 10094 firstLink; 10095 10096 if ( !elm ) { 10097 return; 10098 } 10099 10100 // TODO: Add swipe support 10101 10102 settings.callbacks = callbacks; 10103 10104 if ( elm.nodeName.toLowerCase() !== "a" ) { 10105 settings.delegate = "a:not(" + selector + "-skip)"; 10106 firstLink = elm.getElementsByTagName( "a" )[ 0 ]; 10107 10108 // Is the element a gallery? 10109 if ( elm.className.indexOf( "-gal" ) !== -1 ) { 10110 settings.gallery = { 10111 enabled: true 10112 }; 10113 } 10114 } else { 10115 firstLink = elm; 10116 } 10117 10118 if ( firstLink ) { 10119 if ( firstLink.getAttribute( "href" ).charAt( 0 ) === "#" ) { 10120 settings.type = "inline"; 10121 } else if ( firstLink.className.indexOf( "lbx-iframe" ) !== -1 ) { 10122 settings.type = "iframe"; 10123 } else if ( firstLink.getElementsByTagName( "img" ).length === 0 ) { 10124 settings.type = "ajax"; 10125 } else { 10126 settings.type = "image"; 10127 } 10128 10129 if ( elm.className.indexOf( "lbx-modal" ) !== -1 ) { 10130 settings.modal = true; 10131 } 10132 if ( elm.className.indexOf( "lbx-ajax" ) !== -1 ) { 10133 settings.type = "ajax"; 10134 } 10135 if ( elm.className.indexOf( "lbx-image" ) !== -1 ) { 10136 settings.type = "image"; 10137 } 10138 if ( elm.className.indexOf( "lbx-inline" ) !== -1 ) { 10139 settings.type = "inline"; 10140 } 10141 10142 // Extend the settings with window[ "wb-lbx" ] then data-wb-lbx 10143 settings = $.extend( 10144 true, 10145 settings, 10146 window[ componentName ], 10147 wb.getData( $elm, componentName ) 10148 ); 10149 $elm.magnificPopup( 10150 settings 10151 ).data( "wbLbxFilter", settings.filter ); 10152 } 10153 10154 // Identify that initialization has completed 10155 wb.ready( $elm, componentName ); 10156 } ); 10157
10158 // Load dependencies as needed 10159 setup(); 10160 } 10161 }, 10162 10163 /** 10164 * @method setup 10165 */ 10166 setup = function() { 10167 10168 // Only initialize the i18nText and callbacks once 10169 if ( !i18nText ) { 10170 i18n = wb.i18n; 10171 i18nText = { 10172 close: i18n( "close" ), 10173 oClose: i18n( "overlay-close" ), 10174 tClose: i18n( "close" ) + i18n( "space" ) + i18n( "overlay-close" ) + i18n( "space" ) + i18n( "esc-key" ), 10175 tLoading: i18n( "load" ), 10176 gallery: { 10177 tPrev: i18n( "prv-l" ), 10178 tNext: i18n( "nxt-r" ), 10179 tCounter: i18n( "lb-curr" ) 10180 }, 10181 image: { 10182 tError: i18n( "lb-img-err" ) + " (<a href=\"url%\">)" 10183 }, 10184 ajax: { 10185 tError: i18n( "lb-xhr-err" ) + " (<a href=\"url%\">)" 10186 } 10187 }; 10188 10189 callbacks = { 10190 open: function() { 10191 10192 // TODO: Better if dealt with upstream by Magnific popup 10193 var $item = this.currItem, 10194 $content = this.contentContainer, 10195 $wrap = this.wrap, 10196 $container = $wrap.find( ".mfp-container" ), 10197 $containerParent = $container.parent(), 10198 $modal = $wrap.find( ".modal-dialog" ), 10199 $buttons = $wrap.find( ".mfp-arrow" ), 10200 len = $buttons.length, 10201 i, button; 10202 10203 createCloseButton( $modal ); 10204 10205 $document.find( "body" ).addClass( "wb-modal" ); 10206 $document.find( modalHideSelector ).attr( "aria-hidden", "true" ); 10207 for ( i = 0; i !== len; i += 1 ) { 10208 button = $buttons[ i ]; 10209 button.innerHTML += "<span class='wb-inv'> " + button.title + "</span>"; 10210 } 10211 10212 if ( $item.type === "image" ) { 10213 $content.find( ".mfp-bottom-bar" ).attr( "id", "lbx-title" ); 10214 } else { 10215 $content.attr( "role", "document" ); 10216 } 10217 10218 this.contentContainer.attr( "data-pgtitle", document.getElementsByTagName( "H1" )[ 0 ].textContent ); 10219 10220 trapTabbing( $wrap ); 10221 10222 if ( !$containerParent.is( "dialog" ) ) { 10223 $container.wrap( "<dialog class='mfp-container' open='open'></dialog>" ); 10224 } else { 10225 $containerParent.attr( "open", "open" ); 10226 } 10227 }, 10228 close: function() { 10229 $document.find( "body" ).removeClass( "wb-modal" ); 10230 $document.find( modalHideSelector ).removeAttr( "aria-hidden" ); 10231 this.wrap.find( "dialog" ).removeAttr( "open" ); 10232 10233 }, 10234 change: function() { 10235 var $item = this.currItem, 10236 $content = this.contentContainer, 10237 $el, $bottomBar, $source, $target, 10238 description, altTitleId, altTitle; 10239 10240 if ( $item.type === "image" ) { 10241 $el = $item.el; 10242 $target = $item.img; 10243 $bottomBar = $content.find( ".mfp-bottom-bar" ); 10244 10245 if ( $el ) { 10246 $source = $el.find( "img" ); 10247 $target.attr( "alt", $source.attr( "alt" ) ); 10248 10249 // Replicate aria-describedby if it exists 10250 description = $source.attr( "aria-describedby" ); 10251 if ( description ) { 10252 $target.attr( "aria-describedby", description ); 10253 } 10254 10255 // Replicate longdesc if it exists 10256 description = $source.attr( "longdesc" ); 10257 if ( description ) { 10258 $target.attr( "longdesc", description ); 10259 } 10260 10261 // Handle alternate titles 10262 altTitleId = $el.attr( "data-title" ); 10263 if ( altTitleId ) { 10264 altTitle = document.getElementById( altTitleId ); 10265 if ( altTitle !== null ) { 10266 $bottomBar.find( ".mfp-title" ).html( altTitle.innerHTML ); 10267 } 10268 } 10269 } else { 10270 $target.attr( "alt", $bottomBar.find( ".mfp-title" ).html() ); 10271 } 10272 } else { 10273 $content 10274 .find( ".modal-title, h1" ) 10275 .first() 10276 .attr( "id", "lbx-title" ); 10277 } 10278 10279 $content.attr( "aria-labelledby", "lbx-title" ); 10280 }, 10281 parseAjax: function( mfpResponse ) { 10282 var currItem = this.currItem, 10283 currEl = currItem.el, 10284 urlHash = currItem.src.split( "#" )[ 1 ], 10285 filter = currEl ? currEl.data( "wbLbxFilter" ) : undef, 10286 selector = filter || ( urlHash ? "#" + urlHash : false ), 10287 $response; 10288 10289 // Sanitize the response 10290 mfpResponse.data = DOMPurify.sanitize( mfpResponse.data ); 10291 10292 // Provide the ability to filter the AJAX response HTML 10293 // by the URL hash or a selector 10294 // TODO: Should be dealt with upstream by Magnific Popup 10295 if ( selector ) { 10296 $response = $( "<div>" + mfpResponse.data + "</div>" ).find( selector ); 10297 } else { 10298 $response = $( mfpResponse.data ); 10299 } 10300 createCloseButton( $response ); 10301 10302 $response 10303 .find( ".modal-title, h1" ) 10304 .first() 10305 .attr( "id", "lbx-title" ); 10306 10307 mfpResponse.data = $response; 10308 }, 10309 ajaxContentAdded: function() { 10310 trapTabbing( this.wrap ); 10311 } 10312 }; 10313 } 10314 10315 // Load Magnific Popup dependency and bind the init event handler 10316 Modernizr.load( { 10317 load: "site!deps/jquery.magnific-popup" + wb.getMode() + ".js", 10318 testReady: function() { 10319 return $.magnificPopup; 10320 },
10321 complete: function() { 10322 10323 // Set the dependency i18nText only once 10324 $.extend( true, $.magnificPopup.defaults, i18nText, defaults ); 10325 10326 $document.trigger( dependenciesLoadedEvent ); 10327 } 10328 } ); 10329 }, 10330 createCloseButton = function( $modal ) { 10331 if ( $modal !== null && $modal.hasClass( "modal-dialog" ) ) { 10332 var footer = $modal.find( ".modal-footer" ).first(), 10333 hasFooter = footer.length, 10334 hasButton = hasFooter && $( footer ).find( ".popup-modal-dismiss" ).length !== 0, 10335 closeClassFtr = "popup-modal-dismiss", 10336 closeTextFtr = i18nText.close, 10337 spanTextFtr = i18nText.oClose, 10338 overlayCloseFtr; 10339 10340 if ( !hasButton ) { 10341 if ( !hasFooter ) { 10342 footer = document.createElement( "div" ); 10343 footer.setAttribute( "class", "modal-footer" ); 10344 } 10345 10346 overlayCloseFtr = "<button type='button' class='btn btn-sm btn-primary pull-left " + closeClassFtr + 10347 "'>" + 10348 closeTextFtr + 10349 "<span class='wb-inv'>" + spanTextFtr + "</span></button>"; 10350 10351 $( footer ).append( overlayCloseFtr ); 10352 if ( !hasFooter ) { 10353 $( footer ).insertAfter( $modal.find( ".modal-body" ) ); 10354 } 10355 } 10356 } 10357 }, 10358 trapTabbing = function( $wrap ) { 10359 10360 $wrap.on( "keydown", function( e ) { 10361 if ( e.which === 9 ) { 10362 var tabbable = $wrap.find( ".mfp-container :tabbable" ), 10363 firstTabbable = tabbable.first()[ 0 ], 10364 lastTabbable = tabbable.last()[ 0 ], 10365 currentFocus = $( document.activeElement )[ 0 ]; 10366 10367 if ( !e.shiftKey && currentFocus === lastTabbable ) { 10368 e.preventDefault(); 10369 firstTabbable.focus(); 10370 } else if ( e.shiftKey && ( currentFocus === firstTabbable || currentFocus === $wrap[ 0 ] ) ) { 10371 e.preventDefault(); 10372 lastTabbable.focus(); 10373 } 10374 } 10375 } ); 10376 }; 10377 10378// Bind the init event of the plugin 10379$document.on( "timerpoke.wb " + initEvent, selector, init ); 10380 10381// Handler for clicking on a same page link within the overlay to outside the overlay 10382$document.on( "click", ".mfp-wrap a[href^='#']", function( event ) { 10383 var which = event.which, 10384 eventTarget = event.currentTarget, 10385 $lightbox, linkTarget; 10386 10387 // Ignore middle/right mouse buttons 10388 if ( !which || which === 1 ) { 10389 $lightbox = $( eventTarget ).closest( ".mfp-wrap" ); 10390 linkTarget = document.getElementById( eventTarget.getAttribute( "href" ).substring( 1 ) ); 10391 10392 // Ignore same page links to within the overlay 10393 if ( linkTarget && !$.contains( $lightbox[ 0 ], linkTarget ) ) { 10394 10395 // Stop propagation of the click event 10396 if ( event.stopPropagation ) { 10397 event.stopImmediatePropagation(); 10398 } else { 10399 event.cancelBubble = true; 10400 } 10401 10402 // Close the overlay and set focus to the same page link 10403 $.magnificPopup.close(); 10404 $( linkTarget ).trigger( setFocusEvent ); 10405 } 10406 } 10407} ); 10408 10409// Event handler for closing a modal popup via the close button 10410$( document ).on( "click", ".popup-modal-dismiss", function( event ) { 10411 if ( !this.hasAttribute( "target" ) ) { 10412 event.preventDefault(); 10413 } 10414 10415 $.magnificPopup.close(); 10416} ); 10417 10418// Event handler for closing a modal popup via the Escape key 10419$( document ).on( "keydown", ".mfp-wrap:not(.mfp-close-btn-in)", function( event ) { 10420 10421 // If the Escape key was pressed... 10422 if ( event.key === "Escape" ) { 10423 const closeButtons = event.currentTarget.querySelectorAll( ".popup-modal-dismiss" ); 10424 10425 // Trigger a "fake" click on the last close button 10426 // Notes: 10427 // -Allows Escape key presses to "piggyback" on additional functionality in close button click handlers (such as preventDefault and the session timeout plugin's confirm method) 10428 // -Targets the last close button link to accomodate plugins that use multiple close buttons (such as exit script)... the last button is more likely to represent no in those scenarios 10429 $( closeButtons[ closeButtons.length - 1 ] ).trigger( "click" ); 10430 } 10431} ); 10432 10433// Event handler for opening a popup via a button link and the spacebar key 10434$( document ).on( "keydown", "." + componentName, function( event ) { 10435 const sourceLink = event.currentTarget; 10436 10437 // If the link contains a role="button" attribute and the spacebar key was pressed... 10438 if ( sourceLink.getAttribute( "role" ) === "button" && event.key === " " ) { 10439 10440 // Don't scroll down (typical spacebar behaviour) 10441 event.preventDefault(); 10442 10443 // Trigger a "fake" click on the button link 10444 $( sourceLink ).trigger( "click" ); 10445 } 10446} ); 10447 10448// Event handler for opening a popup without a link 10449$( document ).on( "open" + selector, function( event, items, modal, title, ajax ) { 10450 if ( event.namespace === componentName ) { 10451 var isGallery = items.length > 1, 10452 isModal = modal && !isGallery ? modal : false, 10453 titleSrc = title ? function() { 10454 return title[ $.magnificPopup.instance.index ]; 10455 } : "title"; 10456 10457 event.preventDefault(); 10458 10459 // Ensure the dependencies are loaded first 10460 $document.one( dependenciesLoadedEvent, function() { 10461 $.magnificPopup.open( { 10462 items: items, 10463 modal: isModal, 10464 gallery: { 10465 enabled: isGallery 10466 }, 10467 image: { 10468 titleSrc: titleSrc 10469 }, 10470 callbacks: callbacks, 10471 ajax: ajax 10472 } ); 10473 10474 wb.ready( undef, componentName ); 10475 } ); 10476
10477 // Load dependencies as needed 10478 setup(); 10479 } 10480} ); 10481 10482// Add the timer poke to initialize the plugin 10483wb.add( selector ); 10484 10485} )( jQuery, DOMPurify, window, document, wb ); 10486 10487/** 10488 * @title WET-BOEW Menu plugin 10489 * @overview A Menu plugin for WET 10490 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 10491 * @author WET community 10492 */ 10493( function( $, window, document, wb ) { 10494"use strict"; 10495 10496/* 10497 * Variable and function definitions. 10498 * These are global to the plugin - meaning that they will be initialized once per page, 10499 * not once per instance of plugin on the page. So, this is a good place to define 10500 * variables that are common to all instances of the plugin on a page. 10501 */ 10502var componentName = "wb-menu", 10503 selector = "." + componentName, 10504 initEvent = "wb-init" + selector, 10505 breadcrumb = document.getElementById( "wb-bc" ), 10506 navCurrentEvent = "navcurr.wb", 10507 focusEvent = "setfocus.wb", 10508 detailsInitEvent = "wb-init.wb-details", 10509 menuItemSelector = "> a, > details > summary", 10510 $document = wb.doc, 10511 10512 // Used for half second delay on showing/hiding menus because of mouse hover 10513 hoverDelay = 500, 10514 menuCount = 0, 10515 globalTimeout, 10516 10517 /** 10518 * @method init 10519 * @param {jQuery Event} event Event that triggered the function call 10520 */ 10521 init = function( event ) { 10522 10523 // Start initialization 10524 // returns DOM object = proceed with init 10525 // returns undefined = do not proceed with init (e.g., already initialized) 10526 var elm = wb.init( event, componentName, selector ), 10527 $elm, ajaxFetch; 10528 10529 if ( elm ) { 10530 $elm = $( elm ); 10531 10532 // Ensure the container has an id attribute 10533 if ( !$elm.attr( "id" ) ) { 10534 $elm.attr( "id", componentName + "-" + menuCount ); 10535 } 10536 menuCount += 1; 10537 10538 // Lets test to see if we have any menus to fetch 10539 // This is required for backwards compatibility. In previous versions, the menu was not integrated with the data ajax plugin. 10540 ajaxFetch = $elm.data( "ajax-fetch" ); 10541 if ( ajaxFetch ) { 10542 $elm.trigger( { 10543 type: "ajax-fetch.wb", 10544 fetch: { 10545 url: ajaxFetch 10546 } 10547 } ); 10548 } else { 10549 10550 //Enhance menus that don't rely on the data-ajax plugin 10551 ajaxFetch = $elm.data( "ajax-replace" ) || $elm.data( "ajax-append" ) || $elm.data( "ajax-prepend" ); 10552 if ( !ajaxFetch ) { 10553 onAjaxLoaded( $elm, $elm ); 10554 } 10555 } 10556 } 10557 }, 10558 10559 /** 10560 * Lets set some aria states and attributes 10561 * @method drizzleAria 10562 * @param {jQuery DOM elements} $elements The collection of elements 10563 */ 10564 drizzleAria = function( $elements ) { 10565 var length = $elements.length, 10566 $elm, $subMenu, i; 10567 10568 // Lets tweak for aria 10569 for ( i = 0; i !== length; i += 1 ) { 10570 $elm = $elements.eq( i ); 10571 $subMenu = $elm.siblings( "ul" ); 10572 10573 $elm.attr( { 10574 "aria-posinset": ( i + 1 ), 10575 "aria-setsize": length, 10576 role: "menuitem" 10577 } ); 10578 10579 // if there is a submenu lets put in the aria for it 10580 if ( $subMenu.length !== 0 ) { 10581 10582 $elm.attr( "aria-haspopup", "true" ); 10583 10584 $subMenu.attr( { 10585 "aria-expanded": "false", 10586 "aria-hidden": "true" 10587 } ); 10588 10589 // recurse into submenu 10590 drizzleAria( $subMenu.children( "li" ).find( menuItemSelector ) ); 10591 } 10592 } 10593 }, 10594 10595 /** 10596 * @method createCollapsibleSection 10597 * @return {string} 10598 */ 10599 createCollapsibleSection = function( section, sectionIndex, sectionsLength, $items, itemsLength ) { 10600 10601 // Use details/summary for the collapsible mechanism 10602 var k, $elm, elm, $item, $subItems, subItemsLength, 10603 $section = $( section ), 10604 posinset = "' aria-posinset='", 10605 menuitem = " role='menuitem' aria-setsize='", 10606 sectionHtml = "<li><details>" + "<summary class='mb-item" + 10607 ( $section.hasClass( "wb-navcurr" ) || $section.children( ".wb-navcurr" ).length !== 0 ? " wb-navcurr'" : "'" ) + 10608 menuitem + sectionsLength + posinset + ( sectionIndex + 1 ) + 10609 "' aria-haspopup='true'>" + $section.text() + "</summary>" + 10610 "<ul class='list-unstyled mb-sm' role='menu' aria-expanded='false' aria-hidden='true'>"; 10611 10612 // Convert each of the list items into WAI-ARIA menuitems 10613 for ( k = 0; k !== itemsLength; k += 1 ) { 10614 $item = $items.eq( k ); 10615 $elm = $item.find( menuItemSelector ); 10616 elm = $elm[ 0 ]; 10617 $subItems = $elm.parent().find( "> ul > li" ); 10618 subItemsLength = $subItems.length; 10619 10620 if ( elm && subItemsLength === 0 && elm.nodeName.toLowerCase() === "a" ) { 10621 sectionHtml += "<li>" + $item[ 0 ].innerHTML.replace( 10622 /(<a\s)/, 10623 "$1" + menuitem + itemsLength + 10624 posinset + ( k + 1 ) + 10625 "' tabindex='-1' " 10626 ) + "</li>"; 10627 } else { 10628 sectionHtml += createCollapsibleSection( elm, k, itemsLength, $subItems, $subItems.length ); 10629 } 10630 } 10631 10632 return sectionHtml + "</ul></details></li>"; 10633 }, 10634 10635 /** 10636 * @method createMobilePanelMenu 10637 * @param {array} allProperties Properties used to build the menu system 10638 * @return {string} 10639 */ 10640 createMobilePanelMenu = function( allProperties ) { 10641 var panel = "", 10642 sectionHtml, properties, sections, section, parent, $items, 10643 linkHtml, i, j, len, sectionsLength, itemsLength; 10644 10645 // Process the secondary and site menus 10646 len = allProperties.length; 10647 for ( i = 0; i !== len; i += 1 ) { 10648 properties = allProperties[ i ]; 10649 sectionHtml = ""; 10650 sections = properties[ 0 ]; 10651 sectionsLength = sections.length; 10652 for ( j = 0; j !== sectionsLength; j += 1 ) { 10653 section = sections[ j ]; 10654 $items = $( section.parentNode ).find( "> ul > li" ); 10655 itemsLength = $items.length; 10656 10657 // Collapsible section 10658 if ( itemsLength !== 0 ) { 10659 sectionHtml += createCollapsibleSection( section, j, sectionsLength, $items, itemsLength ); 10660 } else { 10661 parent = section.parentNode; 10662 10663 // Menu item without a section 10664 if ( parent.nodeName.toLowerCase() === "li" ) { 10665 linkHtml = parent.innerHTML; 10666 10667 // Non-list menu items without a section and that contain their own link 10668 } else if ( parent.getElementsByTagName( "a" )[ 0 ] === section.getElementsByTagName( "a" )[ 0 ] ) { 10669 linkHtml = section.innerHTML; 10670 10671 // Non-list menu item without a section and whose siblings contain a link 10672 } else { 10673 linkHtml = "<a href='" + 10674 parent.getElementsByTagName( "a" )[ 0 ].href + "'>" + 10675 section.innerHTML + "</a>"; 10676 } 10677 10678 // Convert the list item to a WAI-ARIA menuitem 10679 sectionHtml += "<li class='no-sect'>" + 10680 linkHtml.replace( 10681 /(<a\s)/, 10682 "$1 class='mb-item' " + "role='menuitem' aria-setsize='" + 10683 sectionsLength + "' aria-posinset='" + ( j + 1 ) + 10684 "' tabindex='-1' " 10685 ) + "</li>"; 10686 } 10687 } 10688 10689 // Create the panel section 10690 panel += "<nav role='navigation' typeof='SiteNavigationElement' id='" + 10691 properties[ 1 ] + "' class='" + properties[ 1 ] + " wb-menu wb-menu-inited'>" + 10692 "<h3>" + properties[ 2 ] + "</h3>" + 10693 "<ul class='list-unstyled mb-menu' role='menu'>" + 10694 sectionHtml + "</ul></nav>"; 10695 } 10696 10697 return panel.replace( /['"]?list-group-item['"]?/gi, "\"\"" ); 10698 }, 10699 10700 /** 10701 * @method onAjaxLoaded 10702 * @param {jQuery DOM element} $elm The plugin element 10703 * @param {jQuery DOM element}
10703 $ajaxResult The AJAXed in menu content to import 10704 */ 10705 onAjaxLoaded = function( $elm, $ajaxResult ) { 10706 var $info = $( "#wb-info" ), 10707 inner = function() { 10708 var $ajaxed = $ajaxResult && $ajaxResult.attr( "data-type" ) === "string" ? $ajaxResult : $elm, 10709 $menubar = $ajaxed.find( ".menu" ), 10710 $menu = $menubar.find( "> li > a" ), 10711 target = $elm.data( "trgt" ), 10712 $secnav = $( "#wb-sec" ), 10713 $language = $( "#wb-lng" ), 10714 search = document.getElementById( "wb-srch" ), 10715 panel = "", 10716 panelDOM = document.getElementById( target ), 10717 $panel = $( panelDOM ), 10718 allProperties = [], 10719 $navCurr, $menuItem, $langItems, len, i; 10720 10721 /* 10722 * Build the mobile panel 10723 */ 10724 10725 // Add search 10726 if ( search !== null ) { 10727 panel += "<section class='srch-pnl'>" + 10728 search.innerHTML 10729 .replace( /h2>/i, "h3>" ) 10730 .replace( /(for|id)="([^"]+)"/gi, "$1='$2-imprt'" ) + 10731 "</section>"; 10732 } 10733 10734 // Add active language offer 10735 if ( $language.length !== 0 ) { 10736 $langItems = $language.find( "li:not(.curr)" ); 10737 len = $langItems.length; 10738 panel += "<section class='lng-ofr'>" + 10739 "<h3>" + $language.children( "h2" ).html() + "</h3>" + 10740 "<ul class='list-inline'>"; 10741 for ( i = 0; i !== len; i += 1 ) { 10742 panel += $langItems[ i ].innerHTML 10743 .replace( /(<a\s.*<\/a>?)/, "<li>$1</li>" ); 10744 } 10745 panel += "</ul></section>"; 10746 } 10747 10748 // Create menu system 10749 if ( $secnav.length !== 0 || $menubar.length !== 0 || $info.length !== 0 ) { 10750 10751 // Add the secondary menu 10752 if ( $secnav.length !== 0 ) { 10753 allProperties.push( [ 10754 $secnav.find( "ul" ).filter( ":not(li > ul)" ).find( " > li > *:first-child" ).get(), 10755 "sec-pnl", 10756 $secnav.find( "h2" ).html() 10757 ] ); 10758 10759 if ( $secnav.find( ".wb-navcurr" ).length === 0 ) { 10760 10761 // Trigger the navcurrent plugin 10762 $secnav.trigger( navCurrentEvent, breadcrumb ); 10763 } 10764 } 10765 10766 // Add the site menu 10767 if ( $menubar.length !== 0 ) { 10768 10769 // Add the menubar role if it is missing 10770 if ( !$menubar.attr( "role" ) ) { 10771 $menubar.attr( "role", "menubar" ); 10772 } 10773 10774 allProperties.push( [ 10775 $menu.get(), 10776 "sm-pnl", 10777 $ajaxed.find( "h2" ).html() 10778 ] ); 10779 } 10780 10781 // Add the site information 10782 if ( $info.length !== 0 ) { 10783 allProperties.push( [ 10784 $info.find( "h3, a" ).not( "section a" ), 10785 "info-pnl", 10786 $info.find( "h2" ).html() 10787 ] ); 10788 10789 if ( $info.find( ".wb-navcurr" ).length === 0 ) { 10790 10791 // Trigger the navcurrent plugin 10792 $info.trigger( navCurrentEvent, breadcrumb ); 10793 } 10794 } 10795 10796 panel += createMobilePanelMenu( allProperties ); 10797 } 10798 10799 // Let's now populate the DOM since we have done all the work in a documentFragment 10800 panelDOM.innerHTML = "<header class='modal-header'><div class='modal-title'>" + 10801 document.getElementById( "wb-glb-mn" ) 10802 .getElementsByTagName( "h2" )[ 0 ] 10803 .innerHTML + 10804 "</div></header><div class='modal-body'>" + panel + "</div>"; 10805 panelDOM.className += " wb-overlay modal-content overlay-def wb-panel-r"; 10806 10807 // fix #8241 10808 if ( $.active > 0 ) { 10809 $( document ).ajaxStop( function() { 10810 initOverlay( $panel ); 10811 } ); 10812 } else { 10813 initOverlay( $panel ); 10814 } 10815 10816 /* 10817 * Build the regular mega menu 10818 */ 10819 $ajaxed 10820 .find( ":discoverable" ) 10821 .attr( "tabindex", "-1" ); 10822 10823 if ( $menu.length !== 0 ) { 10824 $menu[ 0 ].setAttribute( "tabindex", "0" ); 10825 drizzleAria( $menu ); 10826 $menu 10827 .filter( "[aria-haspopup=true]" ) 10828 .append( "<span class='expicon glyphicon glyphicon-chevron-down'></span>" ); 10829 } 10830 10831 // Replace elements 10832 $elm.html( $ajaxed.html() ); 10833 10834 // Trigger the navcurrent plugin 10835 setTimeout( function() { 10836 $elm.trigger( navCurrentEvent, breadcrumb ); 10837 $panel.find( "#sm-pnl" ).trigger( navCurrentEvent, breadcrumb ); 10838 10839 // Ensure that wb-navcurr is reflected in the top level 10840 $navCurr = $panel.find( ".wb-navcurr" ); 10841 len = $navCurr.length; 10842 for ( i = 0; i !== len; i += 1 ) { 10843 $menuItem = $navCurr.eq( i ); 10844 10845 // If not at the top level, then add wb-navcurr to the top level 10846 if ( !$menuItem.hasClass( ".mb-item" ) ) { 10847 $menuItem = $menuItem 10848 .closest( "details" ) 10849 .children( "summary" ) 10850 .addClass( "wb-navcurr" ); 10851 } 10852 } 10853 10854 // Open up the secondary menu if it has wb-navcurr and has a submenu 10855 $menuItem = $panel.find( "#sec-pnl .wb-navcurr.mb-item" ); 10856 if ( $menuItem.attr( "aria-haspopup" ) === "true" ) { 10857 $menuItem 10858 .trigger( "click" ) 10859 .parent() 10860 .prop( "open", "open" ); 10861 } 10862 10863 // Identify that initialization has completed 10864 wb.ready( $elm, componentName ); 10865 }, 1 ); 10866 }, 10867 $footerAjax = $info.find( "[data-ajax-replace],[data-ajax-append],[data-ajax-prepend]" ), 10868 footerAjaxLength = $footerAjax.length, 10869 ajaxCount = 0; 10870 10871 //Delay the execution the menu until any ajaxed footer content is in 10872 if ( footerAjaxLength === 0 ) { 10873 inner(); 10874 } else { 10875 $info.on( "wb-contentupdated ajax-failed.wb", function() { 10876 ajaxCount += 1; 10877 if ( ajaxCount === footerAjaxLength ) { 10878 inner(); 10879 } 10880 } ); 10881 } 10882 }, 10883 10884 // fix #8517 10885 /** 10886 * @method initOverlay 10887 * @param {jQuery object} $panel Current panel 10888 */ 10889 initOverlay = function( $panel ) { 10890 $panel 10891 .trigger( "wb-init.wb-overlay" ) 10892 .find( "summary" ) 10893 .attr( "tabindex", "-1" ) 10894 .trigger( detailsInitEvent ); 10895 $panel 10896 .find( ".mb-menu > li:first-child" ) 10897 .find( ".mb-item" ) 10898 .attr( "tabindex", "0" ); 10899 }, 10900 10901 /** 10902 * @method menuIncrement 10903 * @param {jQuery object} $menuItems Collection of of menu items to move between 10904 * @param {jQuery object} $current Current menu item 10905 * @param {integer} indexChange Requested relative change to the menu item index 10906 */ 10907 menuIncrement = function( $menuItems, $current, indexChange ) { 10908 var menuItemsLength = $menuItems.length, 10909 index = $menuItems.index( $current ) + indexChange; 10910 10911 // Correct out-of-range indexes 10912 index = index === menuItemsLength ? 0 : index === -1 ? menuItemsLength - 1 : index; 10913 10914 // Move to the new menu item 10915 $menuItems.eq( index ).trigger( focusEvent ); 10916 }, 10917 10918 /** 10919 * @method menuClose 10920 * @param {jQuery DOM element} $elm Parent of the element to close 10921 * @param {boolean} removeActive Whether or not to keep the active class 10922 */ 10923 menuClose = function( $elm, removeActive ) { 10924 $elm 10925 .removeClass( "sm-open" ) 10926 .children( ".open" ) 10927 .removeClass( "open" ) 10928 .attr( { 10929 "aria-hidden": "true", 10930 "aria-expanded": "false" 10931 } ) 10932 10933 // Close nested submenus 10934 .find( "details" ) 10935 .removeAttr( "open" ) 10936 .children( "ul" ) 10937 .attr( { 10938 "aria-hidden": "true", 10939 "aria-expanded": "false" 10940 } ); 10941 10942 if ( removeActive ) { 10943 $elm.removeClass( "active" ); 10944 } 10945 }, 10946 10947 /** 10948 * @method menuDisplay 10949 * @param {jQuery DOM element} $elm The plugin element 10950 * @param {jQuery event} menu The menu to display 10951 */ 10952 menuDisplay = function( $elm, menu ) { 10953 var menuLink = menu.children( "a" ); 10954 10955 menuClose( $elm.find( ".active" ), true ); 10956 10957 menu.addClass( "active" ); 10958 10959 // Ignore if doesn't have a submenu 10960 if ( menuLink.attr( "aria-haspopup" ) === "true" ) { 10961 10962 // Add the open state classes 10963 menu 10964 .addClass( "sm-open" ) 10965 .children( ".sm" ) 10966 .addClass( "open" ) 10967 .attr( { 10968 "aria-hidden": "false", 10969 "aria-expanded": "true" 10970 } ); 10971 } 10972 }, 10973 10974 /** 10975 * Searches for the next link that has link text starting with a specific letter 10976 * @method selectByLetter 10977 * @param {integer} charCode The charCode of the letter to search for 10978 * @param {DOM elements} links Collection of links to search 10979 */ 10980 selectByLetter = function( charCode, links ) { 10981 var len = links.length, 10982 keyChar = String.fromCharCode( charCode ), 10983 link, i; 10984 10985 for ( i = 0; i !== len; i += 1 ) { 10986 link = links[ i ]; 10987 if ( link.innerHTML.charAt( 0 ) === keyChar ) { 10988 $( link ).trigger( focusEvent ); 10989 return true; 10990 } 10991 } 10992 10993 return false;
10994 }; 10995 10996// Bind the events of the plugin 10997$document.on( "timerpoke.wb " + initEvent + " ajax-fetched.wb ajax-failed.wb", selector, function( event ) { 10998 10999 var eventType = event.type, 11000 elm, $elm; 11001 11002 switch ( eventType ) { 11003 case "ajax-fetched": 11004 case "ajax-failed": 11005 elm = event.target; 11006 11007 // Filter out any events triggered by descendants 11008 if ( event.currentTarget === elm ) { 11009 $elm = $( elm ); 11010 11011 // Only replace the menu if there isn't an error 11012 onAjaxLoaded( 11013 $elm, 11014 eventType === "ajax-fetched" ? event.fetch.pointer : $elm 11015 ); 11016 } 11017 return false; 11018 11019 case "timerpoke": 11020 case "wb-init": 11021 init( event ); 11022 break; 11023 } 11024 11025 /* 11026 * Since we are working with events we want to ensure that we are being passive about our control, 11027 * so returning true allows for events to always continue 11028 */ 11029 return true; 11030} ); 11031 11032$document.on( "mouseleave", selector + " .menu", function( event ) { 11033 var $currentTarget = $( event.currentTarget ); 11034 11035 // Clear the timeout for open/closing menus 11036 clearTimeout( globalTimeout ); 11037 11038 globalTimeout = setTimeout( function() { 11039 menuClose( $currentTarget.find( ".active" ), true ); 11040 }, hoverDelay ); 11041} ); 11042 11043// Prevent opening another menu if mouse re-enters already opened menu 11044$document.on( "mouseenter", selector + " .sm", function() { 11045 if ( $( this ).attr( "aria-expanded" ) === "true" ) { 11046 clearTimeout( globalTimeout ); 11047 } 11048} ); 11049 11050// Touchscreen "touches" on menubar items should close the submenu if it is open 11051$document.on( "click", selector + " .item[aria-haspopup=true]", function( event ) { 11052 var which = event.which, 11053 $this, $parent; 11054 11055 // Ignore middle and right mouse buttons 11056 if ( !which || which === 1 ) { 11057 event.preventDefault(); 11058 $this = $( this ); 11059 $parent = $this.parent(); 11060 11061 // Open the submenu if it is closed 11062 if ( !$parent.hasClass( "sm-open" ) ) { 11063 $this.trigger( "focusin" ); 11064 } 11065 } 11066} ); 11067 11068// Click on menu items with submenus should open and close those submenus 11069$document.on( "click", selector + " [role=menu] [aria-haspopup=true]", function( event ) { 11070 var menuItem = event.currentTarget, 11071 parent = menuItem.parentNode, 11072 submenu = parent.getElementsByTagName( "ul" )[ 0 ], 11073 isOpen = submenu.getAttribute( "aria-hidden" ) === "false", 11074 menuItemOffsetTop, menuContainer; 11075 11076 // Close any other open menus 11077 if ( !isOpen ) { 11078 $( parent ) 11079 .closest( "[role^='menu']" ) 11080 .find( "[aria-hidden=false]" ) 11081 .parent() 11082 .find( "[aria-haspopup=true]" ) 11083 .not( menuItem ) 11084 .trigger( "click" ); 11085 11086 // Ensure the opened menu is in view if in a mobile panel 11087 menuContainer = document.getElementById( "mb-pnl" ); 11088 menuItemOffsetTop = menuItem.offsetTop; 11089 if ( $.contains( menuContainer, menuItem ) && 11090 menuItemOffsetTop < menuContainer.scrollTop ) { 11091 11092 menuContainer.scrollTop = menuItemOffsetTop; 11093 } 11094 } 11095 11096 submenu.setAttribute( "aria-expanded", !isOpen ); 11097 submenu.setAttribute( "aria-hidden", isOpen ); 11098} ); 11099 11100// Clicks and touches outside of menus should close any open menus 11101$document.on( "click", function( event ) { 11102 var $openMenus, 11103 which = event.which; 11104 11105 // Ignore middle and right mouse buttons 11106 if ( event.type === "" || ( !which || which === 1 ) ) { 11107 $openMenus = $( selector + " .sm-open" ); 11108 if ( $openMenus.length !== 0 && 11109 $( event.target ).closest( selector ).length === 0 ) { 11110 11111 menuClose( $openMenus, true ); 11112 } 11113 } 11114} ); 11115 11116$document.on( "mouseover focusin", selector + " .item", function( event ) { 11117 var $elm = $( event.currentTarget ), 11118 $parent = $elm.parent(), 11119 $container = $parent.closest( selector ); 11120 11121 // Clear the timeout for open/closing menus 11122 clearTimeout( globalTimeout ); 11123 11124 if ( event.type === "focusin" ) { 11125 menuDisplay( $container, $parent ); 11126 } else { 11127 globalTimeout = setTimeout( function() { 11128 menuDisplay( $container, $parent ); 11129 }, hoverDelay ); 11130 } 11131} ); 11132 11133/* 11134 * Keyboard bindings 11135 */ 11136$document.on( "keydown", selector + " [role=menuitem]", function( event ) { 11137 var menuItem = event.currentTarget, 11138 which = event.which, 11139 $menuItem = $( menuItem ), 11140 hasPopup = $menuItem.attr( "aria-haspopup" ) === "true", 11141 $menu = $menuItem.parent().closest( "[role^='menu']" ), 11142 inMenuBar = $menu.attr( "role" ) === "menubar", 11143 $menuLink, $parentMenu, $parent, $subMenu, result, 11144 menuitemSelector, isOpen, menuItemOffsetTop, menuContainer; 11145 11146 // Define keycodes. (Make const when WET supports ES6) 11147 var TAB_KC = 9, 11148 ENTER_KC = 13, 11149 ESC_KC = 27, 11150 LEFT_KC = 37, 11151 UP_KC = 38, 11152 RIGHT_KC = 39, 11153 DOWN_KC = 40, 11154 SPACE_KC = 32; 11155 11156 if ( !( event.ctrlKey || event.altKey || event.metaKey ) ) { 11157 11158 // Tab key = Hide all sub-menus 11159 if ( which === TAB_KC ) { 11160 menuClose( $( selector + " .active" ), true ); 11161 11162 //Enter or spacebar on a link = follow the link and close menus 11163 } else if ( menuItem.nodeName === "A" && menuItem.hasAttribute( "href" ) && 11164 ( which === ENTER_KC || which === SPACE_KC ) ) { 11165 11166 event.preventDefault(); 11167 menuItem.click(); 11168 menuClose( $( selector + " .active" ), true ); 11169 11170 // Menu item is within a menu bar 11171 } else if ( inMenuBar ) { 11172 11173 // Left / right arrow = Previous / next menu item 11174 if ( which === LEFT_KC || which === RIGHT_KC ) { 11175 event.preventDefault(); 11176 menuIncrement( 11177 $menu.find( "> li > a" ), 11178 $menuItem, 11179 which === LEFT_KC ? -1 : 1 11180 ); 11181 11182 // Enter sub-menu 11183 } else if ( hasPopup && ( which === ENTER_KC || which === SPACE_KC || which === UP_KC || which === DOWN_KC ) ) { 11184 event.preventDefault(); 11185 $parent = $menuItem.parent(); 11186 $subMenu = $parent.find( ".sm" ); 11187 11188 // Open the submenu if it is not already open 11189 if ( !$subMenu.hasClass( "open" ) ) { 11190 menuDisplay( $menu.closest( selector ), $parent ); 11191 } 11192 11193 // Set focus on the first submenu item 11194 $subMenu.children( "li" ).eq( 0 ).find( menuItemSelector ).trigger( focusEvent ); 11195 11196 // Hide sub-menus and set focus 11197 } else if ( which === ESC_KC ) { 11198 event.preventDefault(); 11199 menuClose( $menu.closest( selector ).find( ".active" ), false ); 11200 11201 // Letters only 11202 } else if ( which > 64 && which < 91 ) { 11203 event.preventDefault(); 11204 selectByLetter( 11205 which, 11206 $menuItem.parent().find( "> ul > li > a, > ul > li > details > summary" ).get() 11207 ); 11208 } 11209 11210 // Menu item is not within a menu bar 11211 } else { 11212 menuitemSelector = menuItemSelector; 11213 11214 // Up / down arrow = Previous / next menu item 11215 if ( which === UP_KC || which === DOWN_KC ) { 11216 event.preventDefault(); 11217 menuIncrement( 11218 $menu.children( "li" ).find( menuitemSelector ), 11219 $menuItem, 11220 which === UP_KC ? -1 : 1 11221 ); 11222 11223 // Enter, space, or right arrow with a submenu 11224 } else if ( hasPopup && ( which === ENTER_KC || which === SPACE_KC || which === RIGHT_KC ) ) { 11225 $parent = $menuItem.parent(); 11226 11227 // Prevent handling by details.js polyfill 11228 event.stopImmediatePropagation(); 11229 event.preventDefault(); 11230 11231 // If the menu item is a summary element 11232 if ( menuItem.nodeName.toLowerCase( "summary" ) ) { 11233 isOpen = !!$parent.attr( "open" ); 11234 11235 // Close any other open menus 11236 if ( !isOpen ) { 11237 $( parent ) 11238 .closest( "[role^='menu']" ) 11239 .find( "[aria-hidden=false]" ) 11240 .parent() 11241 .find( "[aria-haspopup=true]" ) 11242 .not( menuItem ) 11243 .trigger( "click" ); 11244 11245 // Ensure the opened menu is in view if in a mobile panel 11246 menuContainer = document.getElementById( "mb-pnl" ); 11247 menuItemOffsetTop = menuItem.offsetTop; 11248 if ( $.contains( menuContainer, menuItem ) && 11249 menuItemOffsetTop < menuContainer.scrollTop ) { 11250 11251 menuContainer.scrollTop = menuItemOffsetTop; 11252 } 11253 11254 // Ensure the menu is opened or stays open 11255 $menuItem.trigger( "click" ); 11256 } 11257 11258 // Update the WAI-ARIA states and move focus to 11259 // the first submenu item 11260 $parent.children( "ul" ) 11261 .attr( { 11262 "aria-expanded": "true", 11263 "aria-hidden": "false" 11264 } ) 11265 .find( "[role=menuitem]:first" ) 11266 .trigger( "setfocus.wb" ); 11267 } 11268 11269 // Escape, left / right arrow without a submenu 11270 } else if ( which === ESC_KC || which === LEFT_KC || which === RIGHT_KC ) { 11271 $parent = $menu.parent(); 11272 $parentMenu = $parent.closest( "[role^='menu']" ); 11273 if ( which === LEFT_KC || which === RIGHT_KC ) { 11274 event.preventDefault(); 11275 } 11276 11277 // If the parent menu is a menubar 11278 if ( $parentMenu.attr( "role" ) === "menubar" ) { 11279 $menuLink = $menu.siblings( "a" ); 11280 11281 // Escape key = Close menu and return to menu bar item 11282 if ( which === ESC_KC ) { 11283 event.preventDefault(); 11284 $menuLink.trigger( focusEvent ); 11285 11286 // Close the menu but keep the referring link active 11287 setTimeout( function() { 11288 menuClose( $menuLink.parent(), false ); 11289 }, 100 ); 11290 11291 // Left / right key = Next / previous menu bar item 11292 } else if ( $parentMenu.attr( "role" ) === "menubar" ) { 11293 menuIncrement( 11294 $parentMenu.find( "> li > a" ), 11295 $menuLink, 11296 which === LEFT_KC ? -1 : 1 11297 ); 11298 } 11299 11300 // Escape or left arrow: Go up a level if there is a higher-level 11301 // menu or close the current submenu if there isn't 11302 } else if ( which !== RIGHT_KC ) { 11303 $subMenu = $parentMenu.length !== 0 ? $menu : $menuItem; 11304 11305 // There is a higher-level menu 11306 if ( $parentMenu.length !== 0 ) { 11307 event.preventDefault(); 11308 $menu.closest( "li" ) 11309 .find( menuitemSelector ) 11310 .trigger( "click" ) 11311 .trigger( "setfocus.wb" ); 11312 11313 // No higher-level menu but the current submenu is open 11314 } else if ( $menuItem.parent().children( "ul" ).attr( "aria-hidden" ) === "fal
11314se" ) { 11315 event.preventDefault(); 11316 $menuItem 11317 .trigger( "click" ) 11318 .trigger( "setfocus.wb" ); 11319 } 11320 } 11321 11322 // Select a menu item in the current menu by the first letter 11323 } else if ( which > 64 && which < 91 ) { 11324 event.preventDefault(); 11325 $parent = $menuItem.closest( "li" ); 11326 11327 // Try to find a match in the next siblings 11328 result = selectByLetter( 11329 which, 11330 $parent.nextAll().find( menuitemSelector ).get() 11331 ); 11332 11333 // If couldn't find a match, try the previous siblings 11334 if ( !result ) { 11335 result = selectByLetter( 11336 which, 11337 $parent.prevAll().find( menuitemSelector ).get() 11338 ); 11339 } 11340 } 11341 } 11342 } 11343} ); 11344 11345// Prevent Firefox from double-triggering menu behaviour 11346$document.on( "keyup", selector + " [role=menuitem]", function( event ) { 11347 event.preventDefault(); 11348 return false; 11349} ); 11350 11351// Close the mobile panel if switching to medium, large or extra large view 11352$document.on( "mediumview.wb largeview.wb xlargeview.wb", function() { 11353 var mobilePanel = document.getElementById( "mb-pnl" ); 11354 if ( mobilePanel && mobilePanel.getAttribute( "aria-hidden" ) === "false" ) { 11355 $( mobilePanel ).trigger( "close.wb-overlay" ); 11356 } 11357} ); 11358 11359// Add the timer poke to initialize the plugin 11360wb.add( selector ); 11361 11362} )( jQuery, window, document, wb ); 11363 11364/** 11365 * @title WET-BOEW Multimedia PLayer 11366 * @overview An accessible multimedia player for <audio> and <video> tags 11367 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 11368 * @author WET Community 11369 */ 11370/* globals YT */ 11371( function( $, DOMPurify, window, wb, undef ) { 11372"use strict"; 11373 11374/* Local scoped variables*/ 11375var componentName = "wb-mltmd", 11376 selector = "." + componentName, 11377 initEvent = "wb-init" + selector, 11378 ctrls = selector + " .wb-mm-ctrls", 11379 dispCtrls = selector + " .display," + ctrls, 11380 template, 11381 i18n, i18nText, 11382 youtubeReadyEvent = "ready.youtube", 11383 captionsLoadedEvent = "ccloaded" + selector, 11384 captionsLoadFailedEvent = "ccloadfail" + selector, 11385 captionsVisibleChangeEvent = "ccvischange" + selector, 11386 renderUIEvent = "renderui" + selector, 11387 initializedEvent = "inited" + selector, 11388 youtubeEvent = "youtube" + selector, 11389 resizeEvent = "resize" + selector, 11390 templateLoadedEvent = "templateloaded" + selector, 11391 cuepointEvent = "cuepoint" + selector, 11392 captionClass = "cc_on", 11393 multimediaEvents = [ 11394 "durationchange", 11395 "playing", 11396 "pause", 11397 "ended", 11398 "volumechange", 11399 "timeupdate", 11400 "waiting", 11401 "canplay", 11402 "seeked", 11403 "progress", 11404 captionsLoadedEvent, 11405 captionsLoadFailedEvent, 11406 captionsVisibleChangeEvent, 11407 cuepointEvent 11408 ].join( " " ), 11409 $document = wb.doc, 11410 $window = wb.win, 11411 11412 /** 11413 * @function init 11414 * @param {jQuery Event} event Event that triggered the function call 11415 */ 11416 init = function( event ) { 11417 11418 // Start initialization 11419 // returns DOM object = proceed with init 11420 // returns undefined = do not proceed with init (e.g., already initialized) 11421 var eventTarget = wb.init( event, componentName, selector ); 11422 11423 if ( eventTarget ) { 11424 11425 // Only initialize the i18nText once 11426 if ( !i18nText ) { 11427 11428 // YT workaround for when played inside a modal dialog, like with lightbox 11429 // 11430 // the issue is when the iFrame is moved, it reloads and then it doesn't emit the right event to 11431 // adjust the WET multimedia controller to represent its current state. 11432 // 11433 // This needs to be executed only once, that is why it is in the i18nText conditional 11434 window.addEventListener( "message", function( e ) { 11435 var data, frames, i, i_len, i_cache; 11436 11437 // Ensure we don't conflict with other postMessage listener 11438 try { 11439 data = JSON.parse( e.data ); 11440 11441 // Only for a specific YT message 11442 if ( data.event && data.event === "infoDelivery" && data.info && data.info.playerState ) { 11443 11444 // Find the iFrame and evaluate if it needs to be reposted 11445 frames = document.getElementsByTagName( "iframe" ); 11446 11447 i_len = frames.length; 11448 for ( i = 0; i < i_len; i++ ) { 11449 i_cache = frames[ i ]; 11450 if ( i_cache.dataset.L2 && i_cache.contentWindow === e.source ) { 11451 11452 // Prepare the event data and emulate the YT object for our event management need 11453 youTubeEvents.call( i_cache, { 11454 target: i_cache.parentElement.parentElement.object, 11455 data: data.info.playerState 11456 } ); 11457 break; 11458 } 11459 } 11460 } 11461 } catch ( err ) { 11462 11463 /* swallow error */ 11464 } 11465 } ); 11466 11467 // 11468 // END YT workaround 11469 // 11470 11471 i18n = wb.i18n; 11472 i18nText = { 11473 play: i18n( "mmp-play" ), 11474 pause: i18n( "pause" ), 11475 volume: i18n( "volume" ), 11476 cc_on: i18n( "cc", "on" ), 11477 cc_off: i18n( "cc", "off" ), 11478 cc_error: i18n( "cc-err" ), 11479 fs: i18n( "fs" ), 11480 mute_on: i18n( "mute", "on" ), 11481 mute_off: i18n( "mute", "off" ), 11482 duration: i18n( "dur" ), 11483 position: i18n( "pos" ) 11484 }; 11485 } 11486 11487 if ( template === undef ) { 11488 template = ""; 11489 $( eventTarget ).trigger( { 11490 type: "ajax-fetch.wb", 11491 fetch: {
11492 url: wb.getPath( "/assets" ) + "/mediacontrols.html" 11493 } 11494 } ); 11495 } else if ( template !== "" ) { 11496 $( eventTarget ).trigger( templateLoadedEvent ); 11497 } 11498 } 11499 }, 11500 11501 /* helper functions*/ 11502 11503 /** 11504 * @method formatTime 11505 * @description format a number of seconds to SMTPE Timecode format (HH:MM:SS.FF) 11506 * @param {Float} time The time to format 11507 * @returns {String} the formatted time 11508 */ 11509 formatTime = function( time ) { 11510 var index = 2, 11511 timecode = "", 11512 secondsIn, current, pad; 11513 11514 pad = function( number, digits ) { 11515 return new Array( Math.max( digits - String( number ).length + 1, 0 ) ).join( 0 ) + number; 11516 }; 11517 11518 time = Math.floor( time ); 11519 11520 //Loop to extract hours, minutes and seconds 11521 while ( index >= 0 ) { 11522 11523 //Get the number of seconds for the current iteration (hour, minute or second) 11524 secondsIn = Math.pow( 60, index ); 11525 current = Math.floor( time / secondsIn ); 11526 11527 if ( timecode !== "" ) { 11528 timecode += ":"; 11529 } 11530 11531 timecode += pad( current, 2 ); 11532 time -= secondsIn * current; 11533 index -= 1; 11534 } 11535 return timecode; 11536 }, 11537 11538 /** 11539 * @method parseTime 11540 * @description parse an SMTPE Timecode string (HH:MM:SS.FF) or duration (45s) and returns the number of seconds for the timecode 11541 * @param {String} time The timecode or duration string to parse 11542 * @returns {Float} the number of seconds in time 11543 */ 11544 parseTime = function( time ) { 11545 var i, parts, timeStringPortion, partLength, seconds; 11546 11547 if ( time !== undef ) { 11548 if ( time.charAt( time.length - 1 ) === "s" ) { 11549 11550 //Duration parsing 11551 return parseFloat( time.substring( 0, time.length - 1 ) ); 11552 } else { 11553 11554 //SMTPE Timecode Parsing 11555 parts = time.split( ":" ).reverse(); 11556 seconds = 0; 11557 11558 for ( i = 0, partLength = parts.length; i < partLength; i += 1 ) { 11559 timeStringPortion = i === 0 ? 11560 parseFloat( parts[ i ] ) : 11561 parseInt( parts[ i ], 10 ); 11562 seconds += timeStringPortion * Math.pow( 60, i ); 11563 } 11564 return seconds; 11565 } 11566 } 11567 return -1; 11568 }, 11569 11570 /* 11571 * Performant micro templater 11572 * @credit: https://github.com/premasagar/tim/blob/master/tinytim.js 11573 * @todo: caching 11574 */ 11575 tmpl = ( function() { 11576 var start = "{{", 11577 end = "}}", 11578 11579 // e.g. config.person.name 11580 path = "[a-z0-9_$][\\.a-z0-9_]*", 11581 pattern = new RegExp( start + "\\s*(" + path + ")\\s*" + end, "gi" ); 11582 return function( template, data ) { 11583 11584 // Merge data into the template string 11585 return template.replace( pattern, function( tag, token ) { 11586 var path = token.split( "." ), 11587 len = path.length, 11588 lookup = data, 11589 i = 0; 11590 for ( ; i < len; i += 1 ) { 11591 lookup = lookup[ path[ i ] ]; 11592 11593 // Property not found 11594 if ( lookup === undef ) { 11595 throw "tim: '" + path[ i ] + "' not found in " + tag; 11596 } 11597 11598 // Return the required value 11599 if ( i === len - 1 ) { 11600 return lookup; 11601 } 11602 } 11603 } ); 11604 }; 11605 }() ), 11606 11607 /** 11608 * @method parseHtml 11609 * @description parse an HTML fragment and extract embed captions 11610 * @param {String} content The HTML fragment containing the captions 11611 * @returns {Array} An array of captions objects (ex: {text: "Caption", begin: 0, end :10}) 11612 */ 11613 parseHtml = function( content ) { 11614 var captions = [], 11615 captionSelector = ".wb-tmtxt", 11616 captionElements = content.find( captionSelector ), 11617 len = captionElements.length, 11618 i, captionElement, json, begin, end; 11619 11620 for ( i = 0; i !== len; i += 1 ) { 11621 captionElement = $( captionElements[ i ] ); 11622 begin = -1; 11623 end = -1; 11624 11625 if ( captionElement.attr( "data-begin" ) !== undef ) { 11626 begin = parseTime( captionElement.attr( "data-begin" ) ); 11627 end = captionElement.attr( "data-end" ) !== undef ? 11628 parseTime( captionElement.attr( "data-end" ) ) : 11629 parseTime( captionElement.attr( "data-dur" ) ) + begin; 11630 } else if ( captionElement.attr( "data" ) !== undef ) { 11631 json = captionElement.attr( "data" ) 11632 .replace( /(begin|dur|end)/g, "\"$1\"" ) 11633 .replace( /'/g, "\"" ); 11634 json = $.parseJSON( json ); 11635 begin = parseTime( json.begin ); 11636 end = json.end !== undef ? 11637 parseTime( json.end ) : 11638 parseTime( json.dur ) + begin; 11639 } 11640 11641 //Removes nested captions if an 11642 captionElement = captionElement.clone(); 11643 captionElement.find( captionSelector ).detach(); 11644 11645 captions[ captions.length ] = { 11646 text: captionElement.html(), 11647 begin: begin, 11648 end: end 11649 }; 11650 } 11651 return captions; 11652 }, 11653 11654 /** 11655 * @method parseXml 11656 * @description parse an TTML (Xml) document and extract captions 11657 * @param {String} content The TTML fragment containing the captions 11658 * @returns {Array} An array of captions objects (ex: {text: "Caption", begin: 0, end :10}) 11659 */ 11660 parseXml = function( content ) { 11661 var captions = [], 11662 captionSelector = "[begin]", 11663 parser = new DOMParser(), 11664 doc = parser.parseFromString( content, "application/xml" ), 11665 captionElements = doc.querySelectorAll( captionSelector ), 11666 len = captionElements.length, 11667 i, captionElement, begin, end; 11668 11669 for ( i = 0; i !== len; i += 1 ) { 11670 captionElement = captionElements[ i ]; 11671 11672 begin = parseTime( captionElement.getAttribute( "begin" ) + "" ); 11673 end = captionElement.hasAttribute( "end" ) ? 11674 parseTime( captionElement.getAttribute( "end" ) + "" ) : 11675 parseTime( captionElement.getAttribute( "dur" ) + "" ) + begin; 11676 11677 captions[ captions.length ] = { 11678 text: DOMPurify.sanitize( captionElement.textContent ), 11679 begin: begin, 11680 end: end 11681 }; 11682 } 11683 return captions; 11684 }, 11685 11686 /** 11687 * @method loadCaptionsExternal 11688 * @description Loads captions from an external source (HTML embed or TTML) 11689 * @param {Object} elm The jQuery object for the multimedia player loading the captions 11690 * @param {String} url The url for the captions resource to load 11691 * @fires ccloaded.wb-mltmd 11692 * @fires ccloadfail.wb-mltmd 11693 */ 11694 loadCaptionsExternal = function( elm, url ) { 11695 $.ajax( { 11696 url: url, 11697 dataType: "html", 11698 11699 //Filters out images and objects from the content to avoid loading them 11700 dataFilter: function( data ) { 11701 return data.replace( /<img|object [^>]*>/g, "" ); 11702 }, 11703 success: function( data ) { 11704 11705 var captionItems; 11706 11707 if ( data.indexOf( "<html" ) !== -1 ) { 11708 11709 // Sanitize the response 11710 captionItems = parseHtml( $( DOMPurify.sanitize( data, { WHOLE_DOCUMENT: true } ) ) ); 11711 } else { 11712 11713 // Response is sanitized in the XML parser function 11714 captionItems = parseXml( data ); 11715 } 11716 11717 if ( captionItems.length ) { 11718 elm.trigger( { 11719 type: captionsLoadedEvent, 11720 captions: captionItems 11721 } ); 11722 } else { 11723 elm.trigger( { 11724 type: captionsLoadFailedEvent 11725 } ); 11726 } 11727 }, 11728 error: function( response, textStatus, errorThrown ) { 11729 elm.trigger( { 11730 type: captionsLoadFailedEvent, 11731 error: errorThrown 11732 } ); 11733 } 11734 } ); 11735 }, 11736 11737 /** 11738 * @method loadCaptionsInternal 11739 * @description Loads same page captions embedded in HTML 11740 * @param {Object} elm The jQuery object for the multimedia player loading the captions 11741 * @param {Object}
11741 obj The jQUery object containing the captions 11742 * @fires ccloaded.wb-mltmd 11743 */ 11744 loadCaptionsInternal = function( elm, obj ) { 11745 var captionItems = parseHtml( obj ); 11746 11747 if ( captionItems.length ) { 11748 elm.trigger( { 11749 type: captionsLoadedEvent, 11750 captions: captionItems 11751 } ); 11752 } else { 11753 elm.trigger( { 11754 type: captionsLoadFailedEvent 11755 } ); 11756 } 11757 }, 11758 11759 /** 11760 * @method updateCaptions 11761 * @description Update the captions for a multimedia player (called from the timeupdate event of the HTML5 media API) 11762 * @param {Object} area The jQuery object for the element where captions are displayed 11763 * @param {Float} seconds The current time of the media (use to sync the captions) 11764 * @param {Object} captions The JavaScript object containing the captions 11765 */ 11766 updateCaptions = function( area, seconds, captions ) { 11767 var caption, i, 11768 captionsLength = captions.length; 11769 11770 // added   (non-breaking space) to prevent caption space from collapsing 11771 // Used .html() instead of .append for performance purposes 11772 // https://jsperf.com/jquery-append-vs-html-list-performance/2 11773 area.html( " " ); 11774 11775 for ( i = 0; i < captionsLength; i += 1 ) { 11776 caption = captions[ i ]; 11777 if ( seconds >= caption.begin && seconds <= caption.end ) { 11778 area.html( $( "<div>" + caption.text + "</div>" ) ); 11779 } 11780 } 11781 }, 11782 11783 /** 11784 * @method playerApi 11785 * @description Normalizes the calls to the HTML5 media API 11786 * @param {String} fn The function to call 11787 * @param {object} args The arguments to send to the function call 11788 */ 11789 playerApi = function( fn, args ) { 11790 var $this, method; 11791 11792 switch ( fn ) { 11793 case "play": 11794 try { 11795 this.object.play(); 11796 } catch ( ex ) { 11797 this.object.doPlay(); 11798 } 11799 break; 11800 case "pause": 11801 try { 11802 this.object.pause(); 11803 } catch ( ex ) { 11804 this.object.doPause(); 11805 } 11806 break; 11807 case "getCaptionsVisible": 11808 return $( this ).hasClass( captionClass ); 11809 case "setCaptionsVisible": 11810 $this = $( this ); 11811 if ( args ) { 11812 $this.addClass( captionClass ); 11813 } else { 11814 $this.removeClass( captionClass ); 11815 } 11816 $this.trigger( captionsVisibleChangeEvent ); 11817 break; 11818 case "fullscreen": 11819 if ( this.object.requestFullscreen ) { 11820 this.object.requestFullscreen(); 11821 } else if ( this.object.webkitRequestFullscreen ) { /* Safari */ 11822 this.object.webkitRequestFullscreen(); 11823 } else if ( this.object.msRequestFullscreen ) { /* IE11 */ 11824 this.object.msRequestFullscreen(); 11825 } 11826 break; 11827 case "getBuffering": 11828 return this.object.buffering || false; 11829 case "setBuffering": 11830 this.object.buffering = args; 11831 break; 11832 case "getPreviousTime": 11833 return this.object.previousTime; 11834 case "setPreviousTime": 11835 this.object.previousTime = args; 11836 break; 11837 default: 11838 method = fn.charAt( 3 ).toLowerCase() + fn.substr( 4 ); 11839 switch ( fn.substr( 0, 3 ) ) { 11840 case "get": 11841 return typeof this.object[ method ] !== "function" ? 11842 this.object[ method ] : 11843 this.object[ method ](); 11844 case "set": 11845 typeof this.object[ method ] !== "function" ? 11846 this.object[ method ] = args : 11847 this.object[ fn ]( args ); 11848 } 11849 } 11850 }, 11851 11852 /** 11853 * @method youTubeApi 11854 * @description Normalizes the calls to the YouTube API 11855 * @param {String} fn The function to call 11856 * @param {object} args The arguments to send to the function call 11857 */ 11858 youTubeApi = function( fn, args ) { 11859 var $media = $( this.object.getIframe() ), 11860 state; 11861 11862 switch ( fn ) { 11863 case "play": 11864 this.object.wasMutedPlay = this.object.isMuted(); 11865 return this.object.playVideo(); 11866 case "pause": 11867 return this.object.pauseVideo(); 11868 case "getPaused": 11869 state = this.object.getPlayerState(); 11870 return state === -1 || state === 0 || state === 2 || state === 5; 11871 case "getPlayed": 11872 return this.object.getPlayerState() > -1; 11873 case "getEnded": 11874 return this.object.getPlayerState() === 0; 11875 case "getDuration": 11876 return this.object.getDuration(); 11877 case "getCurrentTime": 11878 return this.object.getCurrentTime(); 11879 case "setCurrentTime": 11880 return this.object.seekTo( args, true ); 11881 case "fullscreen": 11882 return this.object.getIframe().requestFullscreen(); 11883 case "getMuted": 11884 if ( !this.object.playedOnce && this.object.wasMutedPlay ) { 11885 state = this.object.wasMutedPlay; 11886 this.object.playedOnce = true; 11887 return state; 11888 } else { 11889 return this.object.isMuted(); 11890 } 11891 case "setMuted": 11892 if ( args ) { 11893 this.object.mute(); 11894 } else {
11895 this.object.unMute(); 11896 } 11897 setTimeout( function() { 11898 $media.trigger( "volumechange" ); 11899 }, ( wb.isReady ? 50 : 500 ) ); 11900 break; 11901 case "getVolume": 11902 return this.object.getVolume() / 100; 11903 case "setVolume": 11904 this.object.setVolume( args * 100 ); 11905 setTimeout( function() { 11906 $media.trigger( "volumechange" ); 11907 }, 50 ); 11908 break; 11909 case "getCaptionsVisible": 11910 return $( this ).hasClass( captionClass ); 11911 case "setCaptionsVisible": 11912 if ( args ) { 11913 $( this ).addClass( captionClass ); 11914 try { 11915 this.object.loadModule( "cc" ); 11916 this.object.setOption( "cc", "track", { languageCode: this.object.getOption( "cc", "tracklist" )[ 0 ].languageCode } ); 11917 } catch ( e ) { 11918 this.object.loadModule( "captions" ); 11919 this.object.setOption( "captions", "track", { languageCode: this.object.getOption( "captions", "tracklist" )[ 0 ].languageCode } ); 11920 } 11921 } else { 11922 $( this ).removeClass( captionClass ); 11923 this.object.unloadModule( "cc" ); 11924 this.object.unloadModule( "captions" ); 11925 } 11926 $media.trigger( "ccvischange" ); 11927 } 11928 }, 11929 11930 /** 11931 * @method youTubeEvents 11932 * @description YouTube API event manager 11933 * @param {object} event The event object for the triggered event 11934 */ 11935 youTubeEvents = function( event ) { 11936 var media = event.target.getIframe(), 11937 $media = $( media ), 11938 timeline = function() { 11939 $media.trigger( "timeupdate" ); 11940 }, 11941 $mltmPlayerElm, 11942 mltmPlayerElm, 11943 isMuted; 11944 11945 switch ( event.data ) { 11946 case null: // init 11947 $media 11948 .trigger( "canplay" ) 11949 .trigger( "durationchange" ); 11950 11951 // Put video on mute if the video is muted on init, run once 11952 $mltmPlayerElm = $media.parentsUntil( selector ).parent(); 11953 11954 // Mute the player, GUI 11955 if ( $mltmPlayerElm.data( "putMutedOnInit" ) ) { 11956 youTubeApi.call( $mltmPlayerElm.get( 0 ), "setMuted", true ); 11957 $mltmPlayerElm.data( "putMutedOnInit", false ); 11958 } 11959 break; 11960 case -1: 11961 event.target.unMute(); 11962 $media.trigger( "durationchange" ); 11963 break; 11964 case 0: 11965 $media.trigger( "ended" ); 11966 media.timeline = clearInterval( media.timeline ); 11967 break; 11968 case 1: // play 11969 11970 // Get the media player 11971 $mltmPlayerElm = $media.parentsUntil( selector ).parent(); 11972 mltmPlayerElm = $mltmPlayerElm.get( 0 ); 11973 11974 // Need to be muted here 11975 isMuted = mltmPlayerElm.player( "getMuted" ); 11976 11977 // Reset the close caption state when iframe was reloaded 11978 if ( media.dataset.L2 ) { 11979 youTubeApi.call( mltmPlayerElm, "setCaptionsVisible", $mltmPlayerElm.hasClass( captionClass ) ); 11980 } 11981 11982 // Play 11983 $media 11984 .trigger( "canplay" ) 11985 .trigger( "play" ) 11986 .trigger( "playing" ); 11987 11988 // Reset muted as needed because youtube onMute by default when playing 11989 if ( isMuted ) { 11990 youTubeApi.call( mltmPlayerElm, "setMuted", true ); 11991 } 11992 11993 media.timeline = setInterval( timeline, 250 ); 11994 break; 11995 case 2: 11996 $media.trigger( "pause" ); 11997 media.timeline = clearInterval( media.timeline ); 11998 break; 11999 case 3: 12000 media.timeline = clearInterval( media.timeline ); 12001 break; 12002 } 12003 }, 12004 12005 youTubeAPIReady = function() { 12006 var youTube = window.youTube; 12007 youTube.ready = true; 12008 $document.trigger( youtubeReadyEvent ); 12009 }, 12010 12011 onResize = function() { 12012 $( selector + " object, " + selector + " iframe, " + selector + " video" ).trigger( resizeEvent ); 12013 }; 12014 12015$document.on( "timerpoke.wb " + initEvent, selector, init ); 12016 12017$window.on( "resize", onResize ); 12018 12019$document.on( "ajax-fetched.wb " + templateLoadedEvent, selector, function( event ) { 12020 var $this = $( this ); 12021 12022 if ( event.type === "ajax-fetched" ) { 12023 template = event.fetch.pointer.html(); 12024 12025 //Notify all player waiting for the controls to load 12026 $this = $( selector ); 12027 } 12028 12029 $this.trigger( { 12030 type: initializedEvent 12031 } ); 12032} ); 12033 12034$document.on( initializedEvent, selector, function( event ) { 12035 if ( event.namespace === componentName ) { 12036 var $this = $( this ), 12037 $media = $this.children( "audio, video" ).eq( 0 ), 12038 captions = $media.children( "track[kind='captions']" ).attr( "src" ) || undef, 12039 id = $this.attr( "id" ), 12040 mId = $media.attr( "id" ) || id + "-md", 12041 type = $media.is( "audio" ) ? "audio" : "video", 12042 title = $media.attr( "title" ) || "", 12043 width = type === "video" ? $media.attr( "width" ) || $media.width() : 0, 12044 height = type === "video" ? $media.attr( "height" ) || $media.height() : 0,
12045 settings = wb.getData( $this, componentName ), 12046 data = $.extend( { 12047 media: $media, 12048 captions: captions, 12049 id: id, 12050 mId: mId, 12051 type: type, 12052 title: title, 12053 height: height, 12054 width: width 12055 }, i18nText ), 12056 media = $media.get( 0 ), 12057 youTube = window.youTube, 12058 url, 12059 i18n = wb.i18n; 12060 12061 if ( $media.attr( "id" ) === undef ) { 12062 $media.attr( "id", mId ); 12063 } 12064 12065 if ( settings !== undef ) { 12066 data.shareUrl = settings.shareUrl; 12067 data.fullscreen = settings.fullscreenBtn || false; 12068 } 12069 12070 $this.addClass( type ); 12071 12072 if ( $media.find( "[type='video/youtube']" ).length > 0 ) { 12073 12074 // lets tweak some variables and start the load sequence 12075 url = wb.getUrlParts( $this.find( "[type='video/youtube']" ).attr( "src" ) ); 12076 12077 // lets set the flag for the call back 12078 data.youTubeId = url.params.v ? url.params.v : url.pathname.substr( 1 ); 12079 12080 // Defaults config set on the video element 12081 data.isInitMuted = $media.get( 0 ).muted; 12082 12083 if ( youTube.ready === false ) { 12084 $document.one( youtubeReadyEvent, function() { 12085 $this.trigger( youtubeEvent, data ); 12086 } ); 12087 } else { 12088 $this.trigger( youtubeEvent, data ); 12089 } 12090 12091 // finally lets load safely 12092 return Modernizr.load( { 12093 load: "https://www.youtube.com/iframe_api", 12094 12095 //possible solution for multimedia and doaction conflict in corporate network 12096 complete: function() { 12097 12098 // Ensure that YouTube API is loading the iframe and if it fails, ensure that it will show a message, like accessing the web via our GC network. 12099 setTimeout( function() { 12100 var resources, arrIframesYt, $notifText; 12101 12102 resources = window.performance.getEntriesByType( "resource" ); 12103 12104 /* get all the iframe initiators that have the same YT url id */ 12105 arrIframesYt = resources.filter( function( obj ) { 12106 12107 return obj.initiatorType === "iframe" && obj.name.includes( data.youTubeId ); 12108 12109 } ); 12110 12111 12112 /* if none found, most probably wb is loaded in restricted network so wb.ready() is triggered for not preventing other wb components to load*/ 12113 if ( arrIframesYt.length < 1 ) { 12114 if ( !wb.isReady ) { 12115 12116 // show the video notification error 12117 $notifText = $( "<div aria-live='polite' class='pstn-lft-xs bg-dark text-white'><p class='mrgn-tp-md mrgn-rght-md mrgn-bttm-md mrgn-lft-md'>" + i18n( "msgYoutubeNotLoad" ) + "</p></div>" ); 12118 $this.prepend( $notifText ); 12119 data.notifyText = $notifText;
12120 wb.ready( $this, componentName ); 12121 } 12122 } 12123 }, 1000 ); 12124 12125 } 12126 } ); 12127 12128 } else if ( media.error === null && media.currentSrc !== "" && media.currentSrc !== undef ) { 12129 $this.trigger( renderUIEvent, [ type, data ] ); 12130 onResize(); 12131 12132 // Identify that initialization has completed 12133 wb.ready( $this, componentName ); 12134 } 12135 } 12136} ); 12137 12138/* 12139 * YouTube Video mode Event 12140 */ 12141$document.on( youtubeEvent, selector, function( event, data ) { 12142 if ( event.namespace === componentName ) { 12143 var mId = data.mId, 12144 $this = $( event.currentTarget ), 12145 $media, ytPlayer; 12146 12147 ytPlayer = new YT.Player( mId, { 12148 videoId: data.youTubeId, 12149 width: data.width, 12150 height: data.height, 12151 playerVars: { 12152 autoplay: 0, 12153 controls: 0, 12154 hl: wb.lang, 12155 origin: wb.pageUrlParts.host, 12156 rel: 0, 12157 cc_load_policy: 1 12158 }, 12159 events: { 12160 onReady: function( event ) { 12161 if ( data.notifyText ) { 12162 data.notifyText.hide(); 12163 } 12164 onResize(); 12165 youTubeEvents( event ); 12166 if ( !wb.isReady ) { 12167 wb.ready( $this, c
12167omponentName ); 12168 } 12169 }, 12170 onStateChange: youTubeEvents, 12171 onApiChange: function() { 12172 12173 //If captions were enabled before the module was ready, re-enable them 12174 var t = $this.get( 0 ); 12175 t.player( "setCaptionsVisible", t.player( "getCaptionsVisible" ) ); 12176 }, 12177 onError: function() { 12178 console.warn( "There is an issue loading the YouTube player" ); 12179 } 12180 } 12181 } ); 12182 12183 $this.addClass( "youtube" ); 12184 12185 $media = $this.find( "#" + mId ); 12186 12187 data.media = $media; 12188 data.ytPlayer = ytPlayer; 12189 12190 // The fullscreen button is not visible by default because there are no controls when in full screen. 12191 if ( data.fullscreen ) { 12192 $this.attr( "data-fullscreen-btn", true ); 12193 } 12194 12195 // Detect if the YT player reloads, like when magnific Popup show the modal, because it moves the iframe 12196 // and then the iframe gets refreshed and reloaded. So the issue is that the iframe stops emitting the event 12197 // needed to adjust the multimedia player controller, like the "onStateChange" event. 12198 $media.on( "load", function( evt ) { 12199 12200 var elm = evt.currentTarget, 12201 ds = elm.dataset; 12202
12203 // Do nothing on the first load and add a flag to indicate it is loaded a second time 12204 if ( ds.L1 ) { 12205 ds.L2 = true; 12206 } else { 12207 ds.L1 = true; 12208 } 12209 } ); 12210 12211 $this.trigger( renderUIEvent, [ "youtube", data ] ); 12212 } 12213} ); 12214 12215$document.on( renderUIEvent, selector, function( event, type, data ) { 12216 if ( event.namespace === componentName ) { 12217 var $this = $( event.currentTarget ), 12218 captionsUrl = wb.getUrlParts( data.captions ), 12219 currentUrl = wb.getUrlParts( window.location.href ), 12220 $media = data.media, 12221 $eventReceiver; 12222 12223 $media 12224 .after( tmpl( template, data ) ) 12225 .wrap( "<div class=\"display\"></div>" ); 12226 12227 $eventReceiver = $media.is( "object" ) ? $media.children( ":first-child" ) : $media; 12228 12229 // Create an adapter for the event management 12230 $eventReceiver.on( multimediaEvents, function( event ) { 12231 $this.trigger( event ); 12232 } ); 12233 12234 this.object = data.ytPlayer || $media.get( 0 ); 12235 this.player = ( data.ytPlayer ) ? youTubeApi : playerApi; 12236 12237 // Trigger the duration change for cases where the event was called before the event binding 12238 if ( type !== "youtube" && !isNaN( this.player( "getDuration" ) ) ) { 12239 $eventReceiver.trigger( "durationchange" ); 12240 } 12241 12242 // Load the progress polyfill if needed 12243 $this.find( "progress" ).trigger( "wb-init.wb-progress" ); 12244 12245 // Load the slider polyfill if needed 12246 $this.find( "input[type='range']" ).trigger( "wb-init.wb-slider" ); 12247 12248 // Create the share widgets if needed 12249 if ( data.shareUrl !== undef ) { 12250 $( "<div class='wb-share' data-wb-share='{\"type\": \"" + 12251 ( type === "audio" ? type : "video" ) + "\", \"title\": \"" + 12252 data.title.replace( /'/g, "'" ) + "\", \"url\": \"" + data.shareUrl + 12253 "\", \"pnlId\": \"" + data.id + "-shr\"}'></div>" ) 12254 .insertBefore( $media.parent() ) 12255 .trigger( "wb-init.wb-share" ); 12256 } 12257 12258 if ( data.isInitMuted ) { 12259 $this.data( "putMutedOnInit", true ); 12260 } else if ( !data.ytPlayer && this.object.muted ) { 12261 $media.trigger( "volumechange" ); 12262 } 12263 12264 if ( data.captions === undef ) { 12265 return 1; 12266 } 12267 12268 // Load the captions 12269 if ( currentUrl.absolute.replace( currentUrl.hash || "#", "" ) !== captionsUrl.absolute.replace( captionsUrl.hash || "#", "" ) ) { 12270 loadCaptionsExternal( $media, captionsUrl.absolute ); 12271 } else { 12272 loadCaptionsInternal( $media, $( "#" + wb.jqEscape( captionsUrl.hash.substring( 1 ) ) ) ); 12273 } 12274 12275 // The fullscreen button is not visible by default because there are no controls when in full screen. 12276 if ( data.fullscreen ) { 12277 $this.attr( "data-fullscreen-btn", true ); 12278 } 12279 } 12280} ); 12281 12282/* 12283 * UI Bindings 12284 */ 12285 12286$document.on( "click", selector, function( event ) { 12287 var $target = $( event.target ), 12288 className = $target.attr( "class" ) || ""; 12289 12290 // Ignore middle and right mouse buttons 12291 if ( event.which === 2 || event.which === 3 ) { 12292 return true; 12293 } 12294 12295 // Optimized multiple class tests to include child glyphicon because Safari was reporting the click event 12296 // from the child span not the parent button, forcing us to have to check for both elements 12297 // JSPerf for multiple class matching https://jsperf.com/hasclass-vs-is-stackoverflow/7 12298 if ( /playpause|-play|-pause|display/.test( className ) || $target.is( "object" ) || $target.is( "video" ) ) { 12299 this.player( "getPaused" ) || this.player( "getEnded" ) ? this.player( "play" ) : this.player( "pause" ); 12300 } else if ( /(^|\s)cc\b|-subtitles/.test( className ) && !$target.attr( "disabled" ) && !$target.parent().attr( "disabled" ) ) { 12301 this.player( "setCaptionsVisible", !this.player( "getCaptionsVisible" ) ); 12302 } else if ( /\bmute\b|-volume-(up|off)/.test( className ) ) { 12303 this.player( "setMuted", !this.player( "getMuted" ) ); 12304 } else if ( $target.is( "progress" ) || $target.hasClass( "progress" ) || $target.hasClass( "progress-bar" ) ) { 12305 this.player( "setCurrentTime", this.player( "getDuration" ) * ( ( event.pageX - $target.offset().left ) / $target.width() ) ); 12306 } else if ( /\brewind\b|-backward/.test( className ) ) { 12307 this.player( "setCurrentTime", this.player( "getCurrentTime" ) - this.player( "getDuration" ) * 0.05 ); 12308 } else if ( /\bfastforward\b|-forward/.test( className ) ) { 12309 this.player( "setCurrentTime", this.player( "getCurrentTime" ) + this.player( "getDuration" ) * 0.05 ); 12310 } else if ( className.includes( "cuepoint" ) ) { 12311 $( this ).trigger( { type: "cuepoint", cuepoint: $target.data( "cuepoint" ) } ); 12312 } else if ( /fullscreen|fs/.test( className ) ) { 12313 this.player( "fullscreen" ); 12314 } 12315} ); 12316 12317$document.on( "input change", selector, function( event ) { 12318 var target = event.target; 12319 12320 if ( $( target ).hasClass( "volume" ) ) { 12321 event.currentTarget.player( "setMuted", false ); 12322 event.currentTarget.player( "setVolume", target.value / 100 ); 12323 } 12324} ); 12325 12326$document.on( "keydown", dispCtrls, function( event ) { 12327 var playerTarget = event.currentTarget.parentNode, 12328 which = event.which, 12329 volume = 0, 12330 step = 0.05, 12331 $playerTarget = $( playerTarget ); 12332 12333 if ( !( event.ctrlKey || event.altKey || event.metaKey ) ) { 12334 switch ( which ) { 12335 case 32: 12336 12337 // Mute/unmute if focused on the mute/unmute button or volume input. 12338 if ( $( event.target ).hasClass( "mute" ) || event.target.nodeName === "INPUT" ) { 12339 $playerTarget.find( ".mute" ).trigger( "click" ); 12340 } else if ( $( event.target ).hasClass( "fs" ) ) { 12341 12342 // Enter full screen if focused on the full screen button 12343 $playerTarget.find( ".fs" ).trigger( "click" ); 12344 } else if ( $( event.target ).hasClass( "cc" ) ) { 12345 12346 // Show/hide captions if focused on the closed captions button. 12347 $playerTarget.find( ".cc" ).trigger( "click" ); 12348 } else { 12349 12350 // Play/pause if focused on anything else (i.e. the video itself, play/pause button or progress bar). 12351 $playerTarget.find( ".playpause" ).trigger( "click" ); 12352 } 12353 break; 12354 12355 case 37: 12356 playerTarget.player( "setCurrentTime", this.parentNode.player( "getCurrentTime" ) - this.parentNode.player( "getDuration" ) * 0.05 ); 12357 break; 12358 12359 case 39: 12360 playerTarget.player( "setCurrentTime", this.parentNode.player( "getCurrentTime" ) + this.parentNode.player( "getDuration" ) * 0.05 ); 12361 break; 12362 12363 case 38: 12364 volume = Math.round( playerTarget.player( "getVolume" ) * 100 ) / 100 + step; 12365 playerTarget.player( "setVolume", volume < 1 ? volume : 1 ); 12366 break; 12367 12368 case 40: 12369 volume = Math.round( playerTarget.player( "getVolume" ) * 100 ) / 100 - step; 12370 playerTarget.player( "setVolume", volume > 0 ? volume : 0 ); 12371 break; 12372 12373 default: 12374 return true; 12375 } 12376 return false;
12377 } 12378} ); 12379 12380$document.on( "keyup", ctrls, function( event ) { 12381 if ( event.which === 32 && !( event.ctrlKey || event.altKey || event.metaKey ) ) { 12382 12383 // Allows the spacebar to be used for play/pause without double triggering 12384 return false; 12385 } 12386} ); 12387 12388$document.on( "wb-activate", selector, function() { 12389 this.player( "play" ); 12390} ); 12391 12392$document.on( "closed.wb-overlay", ".wb-overlay", function( event ) { 12393 var mltmdPlayer = event.currentTarget.querySelector( selector ); 12394 if ( mltmdPlayer ) { 12395 mltmdPlayer.player( "pause" ); 12396 } 12397} ); 12398 12399$document.on( multimediaEvents, selector, function( event, simulated ) { 12400 var eventTarget = event.currentTarget, 12401 eventType = event.type, 12402 eventNamespace = event.namespace, 12403 $this = $( eventTarget ), 12404 invStart = "<span class='wb-inv'>", 12405 invEnd = "</span>", 12406 currentTime, $button, $slider, buttonData, isPlay, isMuted, isCCVisible, skipTo, volume; 12407 switch ( eventType ) { 12408 case "playing": 12409 case "pause": 12410 case "ended": 12411 isPlay = eventType === "playing"; 12412 $button = $this.find( ".playpause" ); 12413 buttonData = $button.data( "state-" + ( isPlay ? "off" : "on" ) ); 12414 if ( isPlay ) { 12415 $this.addClass( "playing" ); 12416 $this.find( ".progress" ).addClass( "active" ); 12417 } else { 12418 if ( eventType === "ended" ) { 12419 this.loading = clearTimeout( this.loading ); 12420 } 12421 $this.removeClass( "playing" ); 12422 } 12423 $button 12424 .attr( "title", buttonData ) 12425 .children( "span" ) 12426 .toggleClass( "glyphicon-play", !isPlay ) 12427 .toggleClass( "glyphicon-pause", isPlay ) 12428 .html( invStart + buttonData + invEnd ); 12429 break; 12430 12431 case "volumechange": 12432 isMuted = eventTarget.player( "getMuted" ); 12433 $button = $this.find( ".mute" ); 12434 buttonData = $button.data( "state-" + ( isMuted ? "off" : "on" ) ); 12435 volume = eventTarget.player( "getVolume" ) * 100; 12436 $button 12437 .attr( { 12438 title: buttonData, 12439 "aria-pressed": isMuted 12440 } ) 12441 .children( "span" ) 12442 .toggleClass( "glyphicon-volume-up", !isMuted ) 12443 .toggleClass( "glyphicon-volume-off", isMuted ) 12444 .html( invStart + buttonData + invEnd ); 12445 $slider = $this.find( "input[type='range']" ); 12446 $slider[ 0 ].value = isMuted ? 0 : volume; 12447 $slider.trigger( "wb-update.wb-slider" ); 12448 break; 12449 12450 case "timeupdate": 12451 currentTime = eventTarget.player( "getCurrentTime" ); 12452 $this.find( "progress" ) 12453 .attr( 12454 "value", 12455 Math.round( currentTime / eventTarget.player( "getDuration" ) * 1000 ) / 10 12456 ).trigger( "wb-update.wb-progress" ); 12457 12458 $this.find( ".wb-mm-tmln-crrnt span:nth-child(2)" ) 12459 .text( formatTime( currentTime ) ); 12460 12461 if ( $this.hasClass( captionClass ) && $.data( eventTarget, "captions" ) !== undef ) { 12462 updateCaptions( 12463 $this.find( ".wb-mm-cc" ), 12464 currentTime, 12465 $.data( eventTarget, "captions" ) 12466 ); 12467 } 12468 break; 12469 12470 case "durationchange": 12471 $this.find( ".wb-mm-tmln-ttl span:nth-child(2)" ) 12472 .text( formatTime( eventTarget.player( "getDuration" ) ) ); 12473 12474 // Skip to pointer from the querystring 12475 skipTo = wb.pageUrlParts.params[ event.target.id ]; 12476 if ( skipTo ) { 12477 skipTo = parseTime( skipTo ); 12478 eventTarget.player( "setCurrentTime", skipTo ); 12479 } 12480 break; 12481 12482 case "ccloaded": 12483 if ( eventNamespace === componentName ) { 12484 $.data( eventTarget, "captions", event.captions ); 12485 } 12486 break; 12487 12488 case "ccloadfail": 12489 if ( eventNamespace === componentName ) { 12490 if ( !$this.hasClass( "errmsg" ) ) { 12491 $this.addClass( "cc_on errmsg" ) 12492 .find( ".wb-mm-cc" ) 12493 .append( "<div>" + i18nText.cc_error + "</div>" ) 12494 .end() 12495 .find( ".cc" ) 12496 .attr( "disabled", "" ) 12497 .removeAttr( "aria-pressed" ); 12498 } 12499 } 12500 break; 12501 12502 case "ccvischange": 12503 if ( eventNamespace === componentName ) { 12504 isCCVisible = eventTarget.player( "getCaptionsVisible" ); 12505 $button = $this.find( ".cc" ); 12506 buttonData = $button.data( "state-" + ( isCCVisible ? "off" : "on" ) ); 12507 $button.attr( { 12508 title: buttonData, 12509 "aria-pressed": isCCVisible 12510 } ).children( "span" ).html( invStart + buttonData + invEnd ); 12511 } 12512 break; 12513 12514 case "waiting": 12515 if ( !simulated ) { 12516 $document.off( "progress", selector ); 12517 } 12518 this.loading = setTimeout( function() { 12519 $this.addClass( "waiting" ); 12520 }, 500 ); 12521 break; 12522 12523 case "canplay": 12524 case "seeked": 12525 this.loading = clearTimeout( this.loading ); 12526 $this.removeClass( "waiting" ); 12527 break; 12528 case "cuepoint": 12529 eventTarget.player( "setCurrentTime", parseTime( event.cuepoint ) ); 12530 break; 12531 } 12532} ); 12533 12534// Fallback for browsers that don't implement the waiting events 12535$document.on( "progress", selector, function( event ) { 12536 var eventTarget = event.currentTarget,
12537 $this = $( eventTarget ); 12538 12539 // Waiting detected 12540 if ( this.player( "getPaused" ) === false && this.player( "getCurrentTime" ) === this.player( "getPreviousTime" ) ) { 12541 if ( eventTarget.player( "getBuffering" ) === false ) { 12542 eventTarget.player( "setBuffering", true ); 12543 $this.trigger( "waiting", true ); 12544 } 12545 12546 // Waiting has ended 12547 } else if ( eventTarget.player( "getBuffering" ) === true ) { 12548 eventTarget.player( "setBuffering", false ); 12549 $this.trigger( "canplay", true ); 12550 } 12551 eventTarget.player( "setPreviousTime", eventTarget.player( "getCurrentTime" ) ); 12552} ); 12553 12554$document.on( resizeEvent, selector, function( event ) { 12555 if ( event.namespace === componentName ) { 12556 var media = event.target, 12557 $media = $( media ), 12558 figure = event.currentTarget, 12559 ratio, newHeight, 12560 heightDiff; 12561 12562 ratio = $media.attr( "height" ) / $media.attr( "width" ); 12563 12564 // Calculate the new height based on the specified ratio or assume a default 16:9 ratio 12565 newHeight = Math.round( $media.width() * ( !isNaN( ratio ) ? ratio : 0.5625 ) ); 12566 12567 if ( newHeight > window.innerHeight ) { 12568 heightDiff = figure.offsetHeight - window.innerHeight; 12569 newHeight = $media.height() - heightDiff; 12570 } 12571 12572 $media.css( "height", newHeight + "px" ); 12573 } 12574} ); 12575 12576window.onYouTubeIframeAPIReady = youTubeAPIReady; 12577 12578window.youTube = { 12579 ready: false 12580}; 12581 12582wb.add( selector ); 12583 12584} )( jQuery, DOMPurify, window, wb ); 12585 12586/** 12587 * @title WET-BOEW NavCurrent 12588 * @overview Identify URL in a navigation system that matches current page URL or a URL in the breadcrumb trail. Call by applying .trigger( "navcurr.wb", breadcrumb ) where the breadcrumb parameter is an optional object (DOM or jQuery) 12589 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 12590 * @author @pjackson28 12591 */ 12592( function( $, window, wb ) { 12593"use strict"; 12594 12595/* 12596 * Variable and function definitions. 12597 * These are global to the plugin - meaning that they will be initialized once per page, 12598 * not once per instance of plugin on the page. So, this is a good place to define 12599 * variables that are common to all instances of the plugin on a page. 12600 */ 12601var componentName = "wb-navcurr", 12602 selector = "." + componentName, 12603 $document = wb.doc, 12604 breadcrumbLinksArray, breadcrumbLinksUrlArray, 12605 12606 /** 12607 * @method init 12608 * @param {jQuery Event} event Event that triggered the function call 12609 * @param {jQuery DOM element | DOM element} breadcrumb Optional breadcrumb element 12610 * @param {string} classNameOverride Optional class name override (default is wb-navcurr) 12611 */ 12612 init = function( event, breadcrumb, classNameOverride ) { 12613 if ( event.namespace === "wb" ) { 12614 12615 // Start initialization 12616 // returns DOM object = proceed with init 12617 // returns undefined = do not proceed with init (e.g., already initialized) 12618 var menu = wb.init( event.target, componentName, selector ), 12619 menuLinks = menu.getElementsByTagName( "a" ), 12620 menuLinksArray = [], 12621 menuLinksUrlArray = [], 12622 windowLocation = window.location, 12623 pageUrl = windowLocation.hostname + windowLocation.pathname.replace( /^([^/])/, "/$1" ), 12624 pageUrlQuery = windowLocation.search, 12625 match = false, 12626 className = classNameOverride ? classNameOverride : componentName, 12627 child, len, i, j, link, linkHref, linkUrl, linkQuery, linkQueryLen, 12628 localBreadcrumbLinks, localBreadcrumbLinksArray, localBreadcrumbLinksUrlArray, 12629 localBreadcrumbQuery, localBreadcrumbLinkUrl; 12630 12631 if ( menu ) { 12632 12633 // Try to find a match with the page Url and cache link + Url for later if no match found 12634 // Perform the check and caching in reverse to go from more specific links to more general links 12635 for ( i = menuLinks.length - 1; i !== -1; i -= 1 ) { 12636 link = menuLinks[ i ]; 12637 linkHref = link.getAttribute( "href" ); 12638 if ( linkHref !== null ) { 12639 if ( linkHref.length !== 0 && linkHref.charAt( 0 ) !== "#" ) { 12640 linkUrl = link.hostname + link.pathname.replace( /^([^/])/, "/$1" ); 12641 linkQuery = link.search; 12642 linkQueryLen = linkQuery.length; 12643 if ( pageUrl.slice( -linkUrl.length ) === linkUrl && ( linkQueryLen === 0 || pageUrlQuery.slice( -linkQueryLen ) === linkQuery ) ) { 12644 match = true; 12645 break; 12646 } 12647 menuLinksArray.push( link ); 12648 menuLinksUrlArray.push( linkUrl ); 12649 } 12650 } 12651 } 12652 12653 // No page Url match found, try a breadcrumb link match instead 12654 if ( !match && breadcrumb ) { 12655 12656 // Check to see if the data has been cached already 12657 if ( !breadcrumbLinksArray ) { 12658 12659 // Pre-process the breadcrumb links 12660 localBreadcrumbLinksArray = []; 12661 localBreadcrumbLinksUrlArray = []; 12662 localBreadcrumbLinks = ( breadcrumb.jquery ? breadcrumb[ 0 ] : breadcrumb ).getElementsByTagName( "li" ); 12663 len = localBreadcrumbLinks.length; 12664 if ( len ) { 12665 link = localBreadcrumbLinks[ len - 1 ]; 12666 child = link.firstChild; 12667 linkHref = ( child && child.nodeName === "A" ) ? child.getAttribute( "href" ) : ""; 12668 if ( linkHref && linkHref.charAt( 0 ) !== "#" ) { 12669 localBreadcrumbLinksArray.push( child ); 12670 localBreadcrumbLinksUrlArray.push( child.hostname + child.pathname.replace( /^([^/])/, "/$1" ) ); 12671 } 12672 } 12673 12674 // Cache the data in case of more than one execution (e.g., site menu + secondary navigation) 12675 breadcrumbLinksArray = localBreadcrumbLinksArray; 12676 breadcrumbLinksUrlArray = localBreadcrumbLinksUrlArray; 12677 } else { 12678 12679 // Retrieve the cached data 12680 localBreadcrumbLinksArray = breadcrumbLinksArray; 12681 localBreadcrumbLinksUrlArray = breadcrumbLinksUrlArray; 12682 } 12683 12684 // Try to match each breadcrumb link 12685 len = menuLinksArray.length; 12686 for ( j = localBreadcrumbLinksArray.length - 1; j !== -1; j -= 1 ) { 12687 localBreadcrumbLinkUrl = localBreadcrumbLinksUrlArray[ j ]; 12688 localBreadcrumbQuery = localBreadcrumbLinksArray[ j ].search; 12689 12690 for ( i = 0; i !== len; i += 1 ) { 12691 link = menuLinksArray[ i ]; 12692 linkUrl = menuLinksUrlArray[ i ]; 12693 linkQuery = link.search; 12694 linkQueryLen = linkQuery.length; 12695 12696 if ( localBreadcrumbLinkUrl.slice( -linkUrl.length ) === linkUrl && ( linkQueryLen === 0 || localBreadcrumbQuery.slice( -linkQueryLen ) === linkQuery ) ) { 12697 match = true; 12698 break; 12699 } 12700 } 12701 if ( match ) { 12702 break; 12703 } 12704 } 12705 } 12706 12707 if ( match ) { 12708 link.className += " " + className; 12709 if ( menu.className.indexOf( "wb-menu" ) !== -1 && link.className.indexOf( "item" ) === -1 ) { 12710 $( link ).closest( ".sm" ).parent().children( "a" ).addClass( className ); 12711 } 12712 } 12713 12714 // Identify that initialization has completed 12715 wb.ready( $( menu ), componentName ); 12716 } 12717 } 12718 }; 12719 12720// Bind the navcurrent event of the plugin 12721$document.on( "navcurr.wb", init ); 12722 12723} )( jQuery, window, wb ); 12724 12725/** 12726 * @title WET-BOEW Overlay 12727 * @overview Provides multiple styles of overlays such as panels and pop-ups 12728 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 12729 * @author @thomasgohard, @pjackson28 12730 */ 12731( function( $, window, document, wb ) { 12732"use strict"; 12733 12734/* 12735 * Variable and function definitions. 12736 * These are global to the plugin - meaning that they will be initialized once per page, 12737 * not once per instance of plugin on the page. So, this is a good place to define 12738 * variables that are common to all instances of the plugin on a page. 12739 */
12740var componentName = "wb-overlay", 12741 selector = "." + componentName, 12742 initEvent = "wb-init" + selector, 12743 closeClass = "overlay-close", 12744 linkClass = "overlay-lnk", 12745 ignoreOutsideClass = "outside-off", 12746 OverlayOpenFlag = "wb-overlay-dlg", 12747 initialized = false, 12748 sourceLinks = {}, 12749 setFocusEvent = "setfocus.wb", 12750 $document = wb.doc, 12751 i18n, i18nText, 12752 12753 /** 12754 * @method init 12755 * @param {jQuery Event} event Event that triggered the function call 12756 */ 12757 init = function( event ) { 12758 12759 // Start initialization 12760 // returns DOM object = proceed with init 12761 // returns undefined = do not proceed with init (e.g., already initialized) 12762 var elm = wb.init( event, componentName, selector ), 12763 $elm, footer, $header, closeText, overlayClose; 12764 12765 if ( elm ) { 12766 $elm = $( elm ); 12767 12768 // Only initialize the i18nText once 12769 if ( !i18nText ) { 12770 i18n = wb.i18n; 12771 i18nText = { 12772 close: i18n( "close" ), 12773 colon: i18n( "colon" ), 12774 space: i18n( "space" ), 12775 esc: i18n( "esc-key" ), 12776 closeOverlay: i18n( closeClass ) 12777 }; 12778 } 12779 12780 // One left and right panels add close button 12781 var isPanel = ( $elm.attr( "class" ).indexOf( "wb-panel" ) > -1 ) ? true : false, 12782 isPopup = ( $elm.attr( "class" ).indexOf( "wb-popup" ) > -1 ) ? true : false; 12783 if ( isPanel || isPopup ) { 12784 footer = $elm.find( ".modal-footer" )[ 0 ]; 12785 12786 var hasFooter = ( footer && footer.length !== 0 ) ? true : false, 12787 hasButton = hasFooter && $( footer ).find( "." + closeClass ).length !== 0, 12788 closeClassFtr = ( $elm.hasClass( "wb-panel-l" ) ? "pull-right " : "pull-left " ) + closeClass, 12789 closeTextFtr = i18nText.close, 12790 spanTextFtr = i18nText.closeOverlay, 12791 overlayCloseFtr; 12792 12793 if ( !hasButton ) { 12794 if ( !hasFooter ) { 12795 footer = document.createElement( "div" ); 12796 footer.setAttribute( "class", "modal-footer" ); 12797 } 12798 12799 if ( isPopup ) { 12800 footer.style.border = "0"; 12801 } 12802 12803 overlayCloseFtr = "<button type='button' class='btn btn-sm btn-primary " + closeClassFtr + 12804 "' title='" + spanTextFtr + "'>" + 12805 closeTextFtr + 12806 "<span class='wb-inv'>" + spanTextFtr + "</span></button>"; 12807 12808 $( footer ).append( overlayCloseFtr ); 12809 if ( !hasFooter ) { 12810 $elm.append( footer ); 12811 } 12812 } 12813 } 12814 12815 // Add close button 12816 $header = $elm.find( ".modal-title" ); 12817 if ( $header.length !== 0 ) { 12818 closeText = i18nText.close + i18nText.colon + i18nText.space + 12819 $header.text() + i18nText.space + i18nText.esc; 12820 } else { 12821 closeText = i18nText.closeOverlay; 12822 } 12823 closeText = closeText.replace( "'", "'" ); 12824 overlayClose = "<button type='button' class='mfp-close " + closeClass + 12825 "' title='" + closeText + "'>×<span class='wb-inv'> " + 12826 closeText + "</span></button>"; 12827 12828 $elm.append( overlayClose ); 12829 elm.setAttribute( "aria-hidden", "true" ); 12830 12831 // Identify that initialization has completed 12832 initialized = true; 12833 wb.ready( $elm, componentName ); 12834 } 12835 }, 12836 12837 openOverlay = function( overlayId, noFocus ) {
12838 var $overlay = $( "#" + wb.jqEscape( overlayId ) ); 12839 12840 $overlay 12841 .addClass( "open" ) 12842 .attr( "role", "dialog" ) 12843 .attr( "aria-hidden", "false" ); 12844 12845 if ( $overlay.hasClass( "wb-popup-full" ) || $overlay.hasClass( "wb-popup-mid" ) ) { 12846 $overlay.attr( "data-pgtitle", document.getElementsByTagName( "H1" )[ 0 ].textContent ); 12847 $document.find( "body" ).addClass( OverlayOpenFlag ); 12848 } 12849 12850 if ( !noFocus ) { 12851 $overlay 12852 .scrollTop( 0 ) 12853 .trigger( setFocusEvent ); 12854 } 12855 12856 // Register the overlay if it wasn't previously registered 12857 // (only required when opening through an event) 12858 if ( !sourceLinks[ overlayId ] ) { 12859 setTimeout( function() { 12860 sourceLinks[ overlayId ] = null; 12861 }, 1 ); 12862 } 12863 12864 $overlay.trigger( "opened" + selector ); 12865 }, 12866 12867 closeOverlay = function( overlayId, noFocus, userClosed ) { 12868 var $overlay = $( "#" + overlayId ), 12869 sourceLink = sourceLinks[ overlayId ]; 12870 12871 $overlay 12872 .removeClass( "open" ) 12873 .removeAttr( "role" ) 12874 .attr( "aria-hidden", "true" ); 12875 12876 if ( $overlay.hasClass( "wb-popup-full" ) || $overlay.hasClass( "wb-popup-mid" ) ) { 12877 $document.find( "body" ).removeClass( OverlayOpenFlag ); 12878 } 12879 12880 if ( userClosed ) { 12881 $overlay.addClass( "user-closed" ); 12882 } 12883 12884 if ( !noFocus && sourceLink ) { 12885 12886 // Returns focus to the source link for the overlay 12887 $( sourceLink ).trigger( setFocusEvent ); 12888 } 12889 12890 // Delete the source link reference 12891 delete sourceLinks[ overlayId ]; 12892 12893 $overlay.trigger( "closed" + selector ); 12894 }; 12895 12896$document.on( "timerpoke.wb " + initEvent + " keydown open" + selector + 12897" close" + selector, selector, function( event ) { 12898 12899 var eventType = event.type, 12900 which = event.which, 12901 eventTarget = event.target, 12902 eventCurrentTarget = event.currentTarget, 12903 overlayId = eventCurrentTarget.id,
12904 overlay, $focusable, index, length; 12905 12906 switch ( eventType ) { 12907 case "timerpoke": 12908 case "wb-init": 12909 init( event ); 12910 break; 12911 12912 case "open": 12913 if ( eventCurrentTarget === eventTarget ) { 12914 openOverlay( overlayId, event.noFocus ); 12915 } 12916 break; 12917 12918 case "close": 12919 if ( eventCurrentTarget === eventTarget ) { 12920 closeOverlay( overlayId, event.noFocus ); 12921 } 12922 break; 12923 12924 default: 12925 overlay = document.getElementById( overlayId ); 12926 12927 switch ( which ) { 12928 12929 // Tab key 12930 case 9: 12931 12932 // No special tab handling when ignoring outside activity 12933 if ( overlay.className.indexOf( ignoreOutsideClass ) === -1 ) { 12934 $focusable = $( overlay ).find( ":tabbable" ); 12935 length = $focusable.length; 12936 index = $focusable.index( event.target ) + ( event.shiftKey ? -1 : 1 ); 12937 12938 if ( index === -1 || index === length ) { 12939 event.preventDefault(); 12940 $focusable.eq( index === -1 ? length - 1 : 0 ) 12941 .trigger( setFocusEvent ); 12942 } 12943 } 12944 break; 12945 12946 // Escape key 12947 case 27: 12948 if ( !event.isDefaultPrevented() ) { 12949 closeOverlay( overlayId, false, true ); 12950 } 12951 break; 12952 } 12953 } 12954} ); 12955 12956// Handler for clicking on the close button of the overlay 12957$document.on( "click", "." + closeClass, function( event ) { 12958 var which = event.which; 12959 12960 // Ignore if not initialized and middle/right mouse buttons 12961 if ( initialized && ( !which || which === 1 ) ) { 12962 closeOverlay( 12963 $( event.currentTarget ).closest( selector ).attr( "id" ), 12964 false, 12965 true 12966 ); 12967 } 12968} ); 12969 12970// Handler for clicking on a source link for the overlay 12971$document.on( "click keydown", "." + linkClass, function( event ) { 12972 var which = event.which, 12973 sourceLink = event.currentTarget, 12974 overlayId = sourceLink.hash.substring( 1 ); 12975 12976 // Ignore if not initialized and middle/right mouse buttons 12977 if ( initialized && ( !which || which === 1 || which === 32 ) ) { 12978 event.preventDefault(); 12979 12980 // Introduce a delay to prevent outside activity detection 12981 setTimeout( function() { 12982 12983 // Stores the source link for the overlay 12984 sourceLinks[ overlayId ] = sourceLink; 12985 12986 // Opens the overlay 12987 openOverlay( overlayId ); 12988 }, 1 ); 12989 } 12990} ); 12991 12992// Handler for clicking on a same page link within the overlay to outside the overlay 12993$document.on( "click", selector + " a[href^='#']", function( event ) { 12994 var which = event.which, 12995 eventTarget = event.target, 12996 href, overlay, linkTarget; 12997 12998 // Ignore if not initialized and middle/right mouse buttons 12999 if ( initialized && ( !which || which === 1 ) ) { 13000 overlay = $( eventTarget ).closest( selector )[ 0 ]; 13001 href = eventTarget.getAttribute( "href" ); 13002 linkTarget = document.getElementById( href.substring( 1 ) ); 13003 13004 // Ignore same page links to within the overlay 13005 if ( href.length > 1 && !$.contains( overlay, linkTarget ) ) { 13006 13007 // Stop propagation of the click event 13008 if ( event.stopPropagation ) { 13009 event.stopImmediatePropagation(); 13010 } else { 13011 event.cancelBubble = true; 13012 } 13013 13014 // Close the overlay and set focus to the same page link 13015 closeOverlay( overlay.id, true ); 13016 $( linkTarget ).trigger( setFocusEvent ); 13017 } 13018 } 13019} ); 13020 13021// Outside activity detection 13022$document.on( "click focusin", "body", function( event ) { 13023 var eventTarget = event.target, 13024 which = event.which, 13025 overlayId, overlay; 13026 13027 // Ignore if not initialized and middle/right mouse buttons 13028 if ( initialized && ( !which || which === 1 ) ) { 13029 13030 // Close any overlays with outside activity 13031 for ( overlayId in sourceLinks ) { 13032 overlay = document.getElementById( overlayId ); 13033 if ( overlay && overlay.getAttribute( "aria-hidden" ) === "false" && 13034 eventTarget.id !== overlayId && 13035 overlay.className.indexOf( ignoreOutsideClass ) === -1 && 13036 !$.contains( overlay, eventTarget ) ) { 13037 13038 // Close the overlay 13039 closeOverlay( overlayId ); 13040 } 13041 } 13042 } 13043} ); 13044 13045// Ensure any element in focus outside an overlay is visible 13046$document.on( "keyup", function( ) { 13047 var elmInFocus, elmInFocusRect, focusAreaBelow, focusAreaAbove, 13048 overlayId, overlay, overlayRect; 13049 13050 // Ignore if not initialized 13051 if ( initialized ) { 13052 elmInFocus = document.activeElement; 13053 elmInFocusRect = elmInFocus.getBoundingClientRect(); 13054 focusAreaBelow = 0; 13055 focusAreaAbove = window.innerHeight; 13056 13057 // Ensure that at least one overlay is visible, and that the element in focus is not an overlay, 13058 // a child of an overlay, or the body element 13059 if ( $.isEmptyObject( sourceLinks ) || elmInFocus.className.indexOf( componentName ) !== -1 || 13060 $( elmInFocus ).parents( selector ).length !== 0 || elmInFocus === document.body ) { 13061 return; 13062 } 13063 13064 // Determine the vertical portion of the viewport that is not obscured by an overlay 13065 for ( overlayId in sourceLinks ) {
13066 overlay = document.getElementById( overlayId ); 13067 if ( overlay && overlay.getAttribute( "aria-hidden" ) === "false" ) { 13068 overlayRect = overlay.getBoundingClientRect(); 13069 if ( overlay.className.indexOf( "wb-bar-t" ) !== -1 ) { 13070 focusAreaBelow = Math.max( overlayRect.bottom, focusAreaBelow ); 13071 } else if ( overlay.className.indexOf( "wb-bar-b" ) !== -1 ) { 13072 focusAreaAbove = Math.min( overlayRect.top, focusAreaAbove ); 13073 } 13074 } 13075 } 13076 13077 // Ensure the element in focus is visible 13078 // TODO: Find a solution for when there isn't enough page to scroll up or down 13079 if ( elmInFocusRect.top < focusAreaBelow ) { 13080 13081 // Scroll down till the top of the element is visible 13082 window.scrollBy( 0, focusAreaBelow - elmInFocusRect.top ); 13083 } else if ( elmInFocusRect.bottom > focusAreaAbove ) { 13084 13085 // Scroll up till the bottom of the element is visible 13086 window.scrollBy( 0, elmInFocusRect.bottom - focusAreaAbove ); 13087 } 13088 } 13089} ); 13090 13091// Add the timer poke to initialize the plugin 13092wb.add( selector ); 13093 13094} )( jQuery, window, document, wb ); 13095 13096/** 13097 * @title WET-BOEW Tag filter 13098 * @overview Filter based content tagging 13099 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 13100 * @author @duboisp 13101 */ 13102( function( $, window, document, wb ) { 13103"use strict"; 13104 13105let i18n, i18nText; 13106 13107const componentName = "wb-paginate", 13108 selector = ".provisional." + componentName, 13109 initEvent = "wb-init" + selector, 13110 $document = wb.doc, 13111 filterClass = "wb-fltr-out", 13112 pgFilterOutClass = "wb-pgfltr-out", 13113 tagFilterClass = "wb-tgfltr-out", 13114 pagerClass = "wb-paginate-pager", 13115 pageData = "data-pagination-idx", 13116 notFilterClassSel = ":not(." + filterClass + "):not(." + tagFilterClass + ")", 13117 defaults = { 13118 lst: { 13119 selector: "li" 13120 }, 13121 grp: { 13122 selector: "> *" 13123 }, 13124 tbl: { 13125 selector: "tr", 13126 section: ":scope > tbody" 13127 }, 13128 itemsPerPage: 10 13129 }, 13130 13131 init = function( event ) { 13132 const elm = wb.init( event, componentName, selector ); 13133 13134 if ( elm ) { 13135 var $elm = $( elm ), 13136 paginationElm, 13137 elmTagName = elm.nodeName, 13138 setDefault, 13139 uiTargetElm; 13140 13141 if ( !i18nText ) { 13142 i18n = wb.i18n; 13143 i18nText = { 13144 prv: i18n( "prv" ), 13145 nxt: i18n( "nxt" ) 13146 }; 13147 } 13148 13149 // Setup configurations 13150 switch ( elmTagName ) { 13151 case "UL": 13152 setDefault = defaults.lst; 13153 break; 13154 case "TABLE": 13155 setDefault = defaults.tbl; 13156 break; 13157 default: 13158 setDefault = defaults.grp; 13159 break; 13160 } 13161 13162 elm.id = elm.id || wb.getId(); 13163 elm.pgSettings = $.extend( true, {}, setDefault, window[ componentName ], wb.getData( $elm, componentName ) ); 13164 elm.pgSettings.currPage = 1; 13165 elm.pgSettings.itemsPerPage = elm.pgSettings.itemsPerPage || defaults.itemsPerPage; 13166 elm.pgSettings.items = elm.querySelectorAll( ( elm.pgSettings.section || ":scope" ) + " " + elm.pgSettings.selector + notFilterClassSel ); 13167 13168 // Setup pagination container 13169 paginationElm = document.createElement( "div" ); 13170 paginationElm.id = componentName + "-" + elm.id; 13171 paginationElm.classList.add( pagerClass ); 13172 13173 // Add pagination container 13174 if ( elm.pgSettings.uiTarget ) { 13175 uiTargetElm = document.querySelector( elm.pgSettings.uiTarget ); 13176 uiTargetElm.appendChild( paginationElm ); 13177 } else if ( elm.pgSettings.section ) { 13178 if ( elmTagName === "UL" || elmTagName === "TABLE" ) { 13179 elm.after( paginationElm ); 13180 } else { 13181 elm.querySelector( elm.pgSettings.section ).after( paginationElm ); 13182 } 13183 } else { 13184 elm.after( paginationElm ); 13185 } 13186 13187 // Show/hide items and generate pagination 13188 updateItems( elm ); 13189 generateUI( elm ); 13190 13191 wb.ready( $( elm ), componentName ); 13192 } 13193 }, 13194 13195 // Set or reset pagination UI 13196 generateUI = function( elm ) { 13197 var paginationUI = "", 13198 currPage = elm.pgSettings.currPage, 13199 pagesCount = elm.pgSettings.pagesCount, 13200 paginationElm = document.querySelector( "#" + componentName + "-" + elm.id ), 13201 i = 1; 13202 13203 // Make sure the defined current page is not bigger than the total pages 13204 if ( currPage > pagesCount ) { 13205 currPage = pagesCount; 13206 } 13207 13208 // Only add pagination if there is more than one page 13209 if ( pagesCount > 1 ) { 13210 paginationUI = "<ol class=\"pagination\">"; 13211 13212 // Add Previous page button 13213 var prevLI = ""; 13214 prevLI += "<li" + ( i === currPage ? " class=\"disabled\"" : "" ) + ">"; 13215 prevLI += "<button type=\"button\" class=\"paginate-prev\" aria-controls=\"" + elm.id + "\"><span class=\"wb-inv\">Page </span>" + i18nText.prv + "</button>"; 13216 prevLI += "</li>"; 13217 13218 paginationUI += prevLI; 13219 13220 // Add pages buttons 13221 for ( i; i <= pagesCount; i++ ) { 13222 var pageButtonLI = ""; 13223 pageButtonLI += "<li class=\"" + returnItemClass( currPage, pagesCount, i ) + "\"" + ">"; 13224 pageButtonLI += "<button type=\"button\" " + pageData + "=\"" + i + "\" aria-controls=\"" + elm.id + "\"" + ( i === currPage ? " aria-current=\"true\"" : "" ) + "><span class=\"wb-inv\">Page </span>" + i + "</button>"; 13225 pageButtonLI += "</li>"; 13226 paginationUI += pageButtonLI; 13227 } 13228 13229 // Add Next page button 13230 var nextLI = ""; 13231 nextLI += "<li" + ( i === currPage ? " class=\"disabled\"" : "" ) + ">"; 13232 nextLI += "<button type=\"button\" class=\"paginate-next\" aria-controls=\"" + elm.id + "\"><span class=\"wb-inv\">Page </span>" + i18nText.nxt + "</button>"; 13233 nextLI += "</li>"; 13234 paginationUI += nextLI; 13235 paginationUI += "</ol>"; 13236 } 13237 13238 // Insert HTML 13239 paginationElm.innerHTML = paginationUI; 13240 }, 13241 13242 // Show/hide items to reflect current page 13243 updateItems = function( elm ) { 13244 let currPage = elm.pgSettings.currPage, 13245 items = elm.pgSettings.items, 13246 itemsPerPage = elm.pgSettings.itemsPerPage; 13247
13248 items.forEach( function( item, index ) { 13249 if ( ( index < ( itemsPerPage * currPage ) ) && ( index >= ( itemsPerPage * currPage ) - itemsPerPage ) ) { 13250 item.classList.remove( pgFilterOutClass ); 13251 } else { 13252 item.classList.add( pgFilterOutClass ); 13253 } 13254 } ); 13255 13256 elm.pgSettings.pagesCount = Math.ceil( items.length / itemsPerPage ); 13257 }, 13258 13259 // Update pagination to reflect current page 13260 goToPage = function( elm ) { 13261 let paginationElm = document.querySelector( "#" + componentName + "-" + elm.id ), 13262 pageItems = paginationElm.querySelectorAll( "li" ), 13263 itemClass, 13264 pageLink, 13265 currPage = elm.pgSettings.currPage, 13266 pagesCount = elm.pgSettings.pagesCount; 13267 13268 pageItems.forEach( function( pageItem, i ) { 13269 pageLink = pageItem.querySelector( "button" ); 13270 13271 if ( pageLink.classList.contains( "paginate-prev" ) ) { 13272 if ( currPage > 1 ) { 13273 pageItem.classList.remove( "disabled" ); 13274 } else { 13275 pageItem.classList.add( "disabled" ); 13276 } 13277 } else if ( pageLink.classList.contains( "paginate-next" ) ) { 13278 if ( currPage < pagesCount ) { 13279 pageItem.classList.remove( "disabled" ); 13280 } else { 13281 pageItem.classList.add( "disabled" ); 13282 } 13283 } else { 13284 pageItem.className = ""; 13285 pageItem.children[ 0 ].removeAttribute( "aria-current" ); 13286 13287 itemClass = returnItemClass( currPage, pagesCount, i ); 13288 13289 if ( i === currPage ) { 13290 pageItem.children[ 0 ].setAttribute( "aria-current", "true" ); 13291 } 13292 13293 pageItem.className = itemClass; 13294 } 13295 } ); 13296 }, 13297 13298 // Return the list item classname 13299 returnItemClass = function( currPage, pagesCount, i ) { 13300 let itemClass = ""; 13301 13302 if ( currPage > 1 && currPage < pagesCount ) { 13303 if ( Math.abs( currPage - i ) > 1 ) { 13304 itemClass += "hidden-xs hidden-sm"; 13305 13306 if ( Math.abs( currPage - i ) > 2 ) { 13307 itemClass += " hidden-md"; 13308 } 13309 } 13310 } else { 13311 if ( Math.abs( currPage - i ) > 2 ) { 13312 itemClass += "hidden-xs hidden-sm"; 13313 13314 if ( Math.abs( currPage - i ) > 4 ) { 13315 itemClass += " hidden-md"; 13316 } 13317 } 13318 } 13319 13320 if ( pagesCount > 10 ) { 13321 if ( currPage <= 5 ) { 13322 if ( i > 10 ) { 13323 itemClass += " hidden"; 13324 } 13325 } else if ( ( currPage > 5 ) && ( currPage < pagesCount - 5 ) ) { 13326 if ( ( i < currPage - 4 ) || ( i > currPage + 5 ) ) { 13327 itemClass += " hidden"; 13328 } 13329 } else { 13330 if ( i <= pagesCount - 10 ) { 13331 itemClass += " hidden"; 13332 } 13333 } 13334 } 13335 13336 if ( i === currPage ) { 13337 itemClass += " active"; 13338 } 13339 13340 return itemClass; 13341 }; 13342 13343// When a page button is clicked 13344$document.on( "click", "." + pagerClass + " button", function() { 13345 let elm = document.querySelector( "#" + this.getAttribute( "aria-controls" ) ), 13346 pageDest = ( ( this.getAttribute( pageData ) ) * 1 ) || elm.pgSettings.currPage; 13347 13348 if ( this.classList.contains( "paginate-next" ) ) { 13349 pageDest++; 13350 } else if ( this.classList.contains( "paginate-prev" ) ) { 13351 pageDest--; 13352 } 13353 13354 if ( pageDest !== elm.pgSettings.currPage ) { 13355 elm.pgSettings.currPage = pageDest; 13356 13357 updateItems( elm ); 13358 goToPage( elm ); 13359 13360 $( elm ).trigger( "setfocus.wb" ); 13361 if ( elm.getBoundingClientRect().top < 0 ) { 13362 elm.scrollIntoView( { behavior: "smooth" }, true ); 13363 } 13364 } 13365 13366 13367} ); 13368 13369// Resets items and pagination 13370$document.on( "wb-contentupdated", selector, function() { 13371 this.pgSettings.currPage = 1; 13372 this.pgSettings.items = this.pgSettings.items = this.querySelectorAll( ( this.pgSettings.section || ":scope" ) + " " + this.pgSettings.selector + notFilterClassSel ); 13373 13374 updateItems( this ); 13375 generateUI( this ); 13376} ); 13377 13378$document.on( "timerpoke.wb " + initEvent, selector, init ); 13379
13380wb.add( selector ); 13381 13382} )( jQuery, window, document, wb ); 13383 13384/** 13385 * @title WET-BOEW wb-pii-scrub 13386 * @overview This plugin delete Personal Identifiable Information (PII) from the flagged form fields before form submit 13387 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 13388 * @author @polmih, @duboisp, @GormFrank 13389 **/ 13390( function( $, wb ) { 13391"use strict"; 13392 13393var $document = wb.doc, 13394 componentName = "wb-pii-scrub", 13395 selector = "." + componentName, 13396 initEvent = "wb-init" + selector, 13397 attrPIIBlocked = "data-wb-pii-blocked", 13398 attrScrubField = "data-scrub-field", 13399 attrScrubSubmit = "data-scrub-submit", 13400 piiModalID = componentName + "-modal", 13401 defaults = { 13402 scrubChar: "********" 13403 }, 13404 i18n, i18nText, 13405 currSubmitter, 13406 btnAsInput, 13407 13408 init = function( event ) { 13409 var elm = wb.init( event, componentName, selector ), 13410 $elm = $( elm ); 13411 13412 if ( elm ) { 13413 var settings = elm.getAttribute( "data-" + componentName ); 13414 13415 // Initialize i18n strings 13416 if ( !i18nText ) { 13417 i18n = wb.i18n; 13418 i18nText = { 13419 header: i18n( "pii-header" ), 13420 intro: i18n( "pii-intro" ), 13421 viewMore: i18n( "pii-view-more" ), 13422 viewMoreInfo: i18n( "pii-view-more-info" ), 13423 confirmBtn: i18n( "pii-yes-btn" ), 13424 cancelBtn: i18n( "pii-cancel-btn" ), 13425 redacted: i18n( "redacted" ) 13426 }; 13427 } 13428 13429 // Parse settings defined on element 13430 if ( settings ) { 13431 settings = JSON.parse( settings ); 13432 } 13433 13434 // Initialize settings 13435 elm.settings = { ...defaults, ...settings }; 13436 13437 // Ensure the form has an ID 13438 elm.id = elm.id ? elm.id : wb.getId(); 13439 13440 // Block form submission for Postback forms by default 13441 elm.setAttribute( attrPIIBlocked, "true" ); 13442 13443 elm.addEventListener( "submit", function( e ) { 13444 e.preventDefault(); // This is needed because of the setTimeout 13445 13446 // Go through form values 13447 checkFormValues( elm ); 13448 13449 // Check if form has validation errors before showing PII popup or submitting 13450 setTimeout( function() { 13451 let errorElm = elm.querySelector( ".error .label.label-danger" ); 13452 13453 if ( !errorElm ) { 13454 currSubmitter = e.submitter; 13455 13456 // Add submitter data if it is present (only if not a Postback form as it has its own method) 13457 if ( currSubmitter.name && !elm.classList.contains( "wb-postback" ) ) { 13458 btnAsInput = document.createElement( "input" ); 13459 btnAsInput.type = "hidden"; 13460 btnAsInput.name = currSubmitter.name; 13461 btnAsInput.value = currSubmitter.value; 13462 elm.appendChild( btnAsInput ); 13463 } 13464 13465 // Open modal 13466 if ( elm.PIIFields.length > 0 ) { 13467 generateModal( elm ); 13468 13469 $( "#" + piiModalID ).trigger( "open.wb-lbx", [ 13470 [ { 13471 src: "#" + piiModalID, 13472 type: "inline" 13473 } ], 13474 true 13475 ] ); 13476 } else { 13477 if ( elm.classList.contains( "wb-postback" ) ) { 13478 $( elm ).trigger( "wb-postback.submit", currSubmitter ); 13479 } else { 13480 elm.submit(); 13481 } 13482 } 13483 } 13484 }, 50 ); 13485 } ); 13486 13487 wb.ready( $elm, componentName ); 13488 } 13489 }, 13490 13491 /* 13492 * Log all PII positive fields inside the form's "PIIFields" property 13493 * @param form: a reference to the form containing PII fields 13494 */ 13495 checkFormValues = function( form ) { 13496 let fieldsToScrub = form.querySelectorAll( "[" + attrScrubField + "]" ); 13497 13498 form.PIIFields = []; 13499 13500 // identify form elements that were assigned to be scrubbed 13501 fieldsToScrub.forEach( ( field ) => { 13502 13503 // If the field contains PII add field to list 13504 if ( wb.findPotentialPII( field.value, false ) ) { 13505 let fieldLabel = form.querySelector( "[for=" + field.id + "] > span.field-name" ), 13506 fieldLabelText = fieldLabel ? fieldLabel.innerText : form.querySelector( "[for=" + field.id + "]" ).innerText, 13507 scrubbedFieldValue = wb.findPotentialPII( field.value, true, { replaceWith: form.settings.scrubChar } ), 13508 scrubValHTML = wb.findPotentialPII( field.value.replace( /</g, "<" ), true, { replaceWith: "<span role='img' aria-label='" + i18nText.redacted + "'>" + form.settings.scrubChar + "</span>" } ); 13509 13510 form.PIIFields.push( { 13511 elm: field, 13512 scrubVal: scrubbedFieldValue, 13513 scrubValHTML: scrubValHTML, 13514 label: fieldLabelText 13515 } ); 13516 } 13517 } ); 13518 13519 if ( form.PIIFields.length === 1 ) { 13520 document.getElementById( form.PIIFields[ 0 ].elm.id ).focus(); 13521 } 13522 13523 // If PII is found, block Postback form submission 13524 form.PIIFields.length > 0 ? form.setAttribute( attrPIIBlocked, "true" ) : form.setAttribute( attrPIIBlocked, "false" ); 13525 }, 13526 13527 /* 13528 * Scrub all PII positive fields 13529 * @param form: a reference to the form containing PII fields 13530 */ 13531 scrubFormValues = function( form ) { 13532 13533 // Scrub the value of each PII positive fields 13534 form.PIIFields.forEach( ( field ) => { 13535 field.elm.value = field.scrubVal; 13536 } ); 13537 13538 // Clear PII fields as their value has been replaced by the scrubbed value 13539 form.PIIFields = []; 13540 }, 13541 13542 /* 13543 * Generate the modal UI 13544 * @param form: a reference to the form containing PII fields 13545 */ 13546 generateModal = function( form ) { 13547 let piiModalFields = "", 13548 piiModal = document.createElement( "section" ), 13549 moreInfoContent = form.settings.moreInfo ? form.settings.moreInfo : i18nText.viewMoreInfo, 13550 modalTemplate = form.querySelector( "template" + form.settings.modalTemplate ); 13551 13552 // Destroy modal if present 13553 if ( document.getElementById( piiModalID ) ) { 13554 document.getElementById( piiModalID ).remove(); 13555 } 13556 13557 // Generate PII fields list 13558 if ( form.PIIFields.length > 1 ) { 13559 piiModalFields += "<dl>"; 13560 form.PIIFields.forEach( ( field ) => { 13561 piiModalFields += "<dt>" + field.label + "</dt><dd class=\"well well-sm\">" + field.scrubValHTML.replace( /\n/g, "<br>
13561" ) + "</dd>"; 13562 } ); 13563 piiModalFields += "</dl>"; 13564 } else { 13565 piiModalFields += "<div class=\"well well-sm\">" + form.PIIFields[ 0 ].scrubValHTML.replace( /\n/g, "<br>" ) + "</div>"; 13566 } 13567 13568 piiModal.id = piiModalID; 13569 piiModal.className = "modal-dialog modal-content overlay-def"; 13570 piiModal.setAttribute( "data-form", form.id ); 13571 13572 if ( modalTemplate ) { 13573 piiModal.appendChild( modalTemplate.content.cloneNode( true ) ); 13574 } else { 13575 piiModal.innerHTML = `<header class="modal-header"> 13576 <h2 class="modal-title">${ i18nText.header }</h2> 13577 </header> 13578 <div class="modal-body"> 13579 <p>${ i18nText.intro }</p> 13580 ${ piiModalFields } 13581 <details class="mrgn-tp-md"> 13582 <summary>${ i18nText.viewMore }</summary> 13583 ${ moreInfoContent } 13584 </details> 13585 </div> 13586 <div class="modal-footer"> 13587 <div class="row"> 13588 <div class="col-xs-12 col-sm-5 mrgn-tp-sm"><button type="button" class="btn btn-link btn-block popup-modal-dismiss">${ i18nText.cancelBtn }</button></div> 13589 <div class="col-xs-12 col-sm-7 mrgn-tp-sm"><button type="button" class="btn btn-primary btn-block popup-modal-dismiss" ${ attrScrubSubmit }>${ i18nText.confirmBtn }</button></div> 13590 </div> 13591 </div>`; 13592 } 13593 13594 // Using jQuery here to pass the content through DOMpurify 13595 $( "body" ).append( piiModal ); 13596 13597 // Add PII fields HTML if using a custom UI template 13598 if ( modalTemplate ) { 13599 $( "#" + piiModalID + " [data-scrub-modal-fields]" ).html( piiModalFields ); 13600 } 13601 }; 13602 13603// Bind the init event of the plugin 13604$document.on( "timerpoke.wb " + initEvent, selector, init ); 13605 13606// Scrub the form fields on click of the "Confirm" button 13607$document.on( "click", "#" + piiModalID + " [" + attrScrubSubmit + "]", function( ) { 13608 let modal = document.getElementById( piiModalID ), 13609 form = document.getElementById( modal.dataset.form ); 13610 13611 scrubFormValues( form ); 13612 13613 if ( form.classList.contains( "wb-postback" ) ) { 13614 $( form ).trigger( "wb-postback.submit", currSubmitter ); 13615 } else { 13616 form.submit(); 13617 } 13618} ); 13619 13620// Add the timer poke to initialize the plugin 13621wb.add( selector ); 13622 13623} )( jQuery, wb ); 13624 13625/** 13626 * @title WET-BOEW Prettify Plugin 13627 * @overview Wrapper for Google Code Prettify library: https://github.com/google/code-prettify 13628 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 13629 * @author @patheard 13630 */ 13631/* 13632 * Syntax highlighting of source code snippets in an html page using [google-code-prettify](https://github.com/google/code-prettify). 13633 * 13634 * 1. Apply `class="prettyprint"` to a `pre` or `code` element to apply syntax highlighting. Alternatively use `class="all-pre"` to apply syntax highlighting to all `pre` elements on the page. 13635 * 2. Apply `class="linenums"` to a `pre` or `code` element to add line numbers. Alternatively use `class="all-linenums"` to all applicable `pre` elements. Specify the starting number by adding `linenums:#` before `linenums`. 13636 * 3. Add extra language support by applying `class="lang-*"` to each applicable `pre` or `code` element. Supported language syntax CSS classes are as follows: 13637 * - lang-apollo 13638 * - lang-clj 13639 * - lang-css 13640 * - lang-dart 13641 * - lang-go 13642 * - lang-hs 13643 * - lang-lisp 13644 * - lang-lua 13645 * - lang-ml 13646 * - lang-n 13647 * - lang-proto 13648 * - lang-scala 13649 * - lang-sql 13650 * - lang-tex 13651 * - lang-vb 13652 * - lang-vhdl 13653 * - lang-wiki 13654 * - lang-xq 13655 * - lang-yaml 13656 */ 13657( function( $, window, wb ) { 13658"use strict"; 13659 13660/* 13661 * Variable and function definitions. 13662 * These are global to the plugin - meaning that they will be initialized once per page, 13663 * not once per instance of plugin on the page. So, this is a good place to define 13664 * variables that are common to all instances of the plugin on a page. 13665 */ 13666var componentName = "wb-prettify", 13667 selector = "." + componentName, 13668 initEvent = "wb-init" + selector, 13669 prettyPrintEvent = "prettyprint" + selector, 13670 $document = wb.doc, 13671 13672 /* 13673 * Plugin users can override these defaults by setting attributes on the html elements that the 13674 * selector matches. 13675 */ 13676 defaults = { 13677 linenums: false, 13678 allpre: false 13679 }, 13680 13681 /** 13682 * @method init 13683 * @param {jQuery Event} event Event that triggered the function call 13684 */ 13685 init = function( event ) { 13686 13687 // Start initialization 13688 // returns DOM object = proceed with init 13689 // returns undefined = do not proceed with init (e.g., already initialized) 13690 var elm = wb.init( event, componentName, selector ), 13691 modeJS = wb.getMode() + ".js", 13692 deps = [ "site!deps/prettify" + modeJS ], 13693 $elm, classes, settings, i, len, $pre; 13694 13695 if ( elm ) { 13696 $elm = $( elm ); 13697 classes = elm.className.split( " " ); 13698 13699 // Merge default settings with overrides from the selected plugin element. There may be more than one, so don't override defaults globally! 13700 settings = $.extend( {}, defaults, $elm.data() ); 13701 13702 // Check the element for `lang-*` syntax CSS classes 13703 for ( i = 0, len = classes.length; i !== len; i += 1 ) { 13704 if ( classes[ i ].indexOf( "lang-" ) === 0 ) { 13705 deps.push( "site!deps/" + classes[ i ] + modeJS ); 13706 } 13707 } 13708 13709 // CSS class overrides of settings 13710 settings.allpre = settings.allpre || $elm.hasClass( "all-pre" ); 13711 settings.linenums = settings.linenums || $elm.hasClass( "linenums" ); 13712 13713 // Apply global settings 13714 if ( settings.allpre || settings.linenums ) { 13715 $pre = $document.find( "pre" ); 13716 if ( settings.allpre ) { 13717 $pre.addClass( "prettyprint" ); 13718 } 13719 if ( settings.linenums ) { 13720 $pre.filter( ".prettyprint" ).addClass( "linenums" ); 13721 } 13722 } 13723 13724 // Load the required dependencies and prettify the code once finished 13725 Modernizr.load( { 13726 load: deps, 13727 complete: function() { 13728 $document.trigger( prettyPrintEvent ); 13729 } 13730 } ); 13731 } 13732 }, 13733 13734 prettifyDone = function() { 13735 13736 // Identify that initialization has completed 13737 wb.ready( $document, componentName ); 13738 }, 13739 13740 /* 13741 * Invoke the Google pretty print library if it has been initialized 13742 * @method prettyprint 13743 */ 13744 prettyprint = function( event ) { 13745 if ( event.namespace === componentName && 13746 typeof window.prettyPrint === "function" ) { 13747 13748 window.prettyPrint( prettifyDone ); 13749 } 13750 }; 13751 13752// Bind the plugin events 13753$document 13754 .on( "timerpoke.wb " + initEvent, selector, init ) 13755 .on( prettyPrintEvent, prettyprint ); 13756 13757// Add the timer poke to initialize the plugin
13758wb.add( selector ); 13759 13760} )( jQuery, window, wb ); 13761 13762/** 13763 * @title WET-BOEW Resize 13764 * @overview Text and window resizing event handler 13765 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 13766 * @author @pjackson28 13767 */ 13768( function( $, window, document, wb ) { 13769"use strict"; 13770 13771/* 13772 * Variable and function definitions. 13773 * These are global to the plugin - meaning that they will be initialized once per page, 13774 * not once per instance of plugin on the page. So, this is a good place to define 13775 * variables that are common to all instances of the plugin on a page. 13776 */ 13777var componentName = "wb-rsz", 13778 selector = "#" + componentName, 13779 initEvent = "wb-init" + selector, 13780 $document = wb.doc, 13781 sizes = [], 13782 events = [ 13783 "txt-rsz.wb", 13784 "win-rsz-width.wb", 13785 "win-rsz-height.wb" 13786 ], 13787 13788 // Breakpoint names and lower pixel limits 13789 breakpoints = { 13790 xxsmallview: 0, 13791 xsmallview: 480, 13792 smallview: 768, 13793 mediumview: 992, 13794 largeview: 1200, 13795 xlargeview: 1600 13796 }, 13797 resizeTest, currentView, 13798 13799 /** 13800 * @method init 13801 * @param {jQuery Event} event Event that triggered the function call 13802 */ 13803 init = function( event ) { 13804 13805 // Start initialization 13806 // returns DOM object = proceed with init 13807 // returns undefined = do not proceed with init (e.g., already initialized) 13808 var elm = wb.init( event, componentName, selector ), 13809 localResizeTest; 13810 13811 if ( elm ) { 13812 13813 // Set up the DOM element used for resize testing 13814 localResizeTest = document.createElement( "span" ); 13815 localResizeTest.innerHTML = " "; 13816 localResizeTest.setAttribute( "id", componentName ); 13817 document.body.appendChild( localResizeTest ); 13818 resizeTest = localResizeTest; 13819 13820 // Get a snapshot of the current sizes 13821 sizes = [ 13822 localResizeTest.offsetHeight, 13823 window.innerWidth || $document.width(), 13824 window.innerHeight || $document.height() 13825 ]; 13826 13827 // Determine the current view 13828 viewChange( sizes[ 1 ] ); 13829 13830 // Identify that initialization has completed 13831 wb.ready( $document, componentName ); 13832 } 13833 }, 13834 13835 viewChange = function( viewportWidth ) { 13836 var breakpoint, viewName; 13837 13838 // Check for a change between views 13839 for ( breakpoint in breakpoints ) { 13840 13841 // Determine the current view 13842 if ( viewportWidth < breakpoints[ breakpoint ] ) { 13843 break; 13844 } else { 13845 viewName = breakpoint; 13846 } 13847 } 13848 13849 // Determine if the current view is different than the previous view 13850 if ( viewName !== currentView ) { 13851 13852 // Change the breakpoint class on the html element 13853 wb.html 13854 .removeClass( currentView || "" ) 13855 .addClass( viewName ); 13856 13857 // Update the current view 13858 currentView = viewName; 13859 13860 // Trigger the view event 13861 $document.trigger( viewName + ".wb" ); 13862 } 13863 }, 13864 13865 /** 13866 * Tests for text size, window width and window height changes and triggers an event when a change is found 13867 * @method test 13868 */ 13869 test = function() { 13870 var currentSizes = [ 13871 resizeTest.offsetHeight, 13872 window.innerWidth || $document.width(), 13873 window.innerHeight || $document.height() 13874 ], 13875 len = currentSizes.length, 13876 i; 13877 13878 // Check for a viewport or text size change 13879 for ( i = 0; i !== len; i += 1 ) { 13880 if ( currentSizes[ i ] !== sizes[ i ] ) { 13881 13882 // Change detected so trigger related event 13883 $document.trigger( events[ i ], currentSizes ); 13884 13885 // Check for a view change 13886 viewChange( currentSizes[ 1 ] ); 13887 } 13888 } 13889 sizes = currentSizes; 13890 13891 return; 13892 }; 13893 13894// Bind the init event to the plugin 13895$document.on( initEvent, init ); 13896 13897// Re-test on each timerpoke 13898$document.on( "timerpoke.wb", selector, test ); 13899 13900// Initialize the resources 13901$document.trigger( initEvent ); 13902 13903// Add the timer poke to initialize the plugin 13904wb.add( selector ); 13905 13906} )( jQuery, window, document, wb ); 13907 13908/** 13909 * @title WET-BOEW Session Timeout 13910 * @overview Helps Web asset owners to provide session timeout and inactivity timeout functionality. 13911 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 13912 * @author @patheard 13913 */ 13914( function( $, DOMPurify, window, document, wb ) { 13915"use strict"; 13916 13917/* 13918 * Variable and function definitions. 13919 * These are global to the plugin - meaning that they will be initialized once per page, 13920 * not once per instance of plugin on the page. So, this is a good place to define 13921 * variables that are common to all instances of the plugin on a page. 13922 */ 13923var $modal, $modalLink, countdownInterval, i18n, i18nText, 13924 $document = wb.doc, 13925 componentName = "wb-sessto", 13926 selector = "." + componentName, 13927 confirmClass = componentName + "-confirm", 13928 initEvent = "wb-init" + selector, 13929 resetEvent = "reset" + selector, 13930 keepaliveEvent = "keepalive" + selector, 13931 inactivityEvent = "inactivity" + selector, 13932 dataAttr = componentName, 13933 13934 /* 13935 * Plugin users can override these defaults by setting attributes on the html elements that the 13936 * selector matches. 13937 * For example, adding the attribute data-option1="false", will override option1 for that plugin instance. 13938 */ 13939 defaults = { 13940 inactivity: 1200000, /* default inactivity period 20 minutes */ 13941 reactionTime: 180000, /* default confirmation period of 3 minutes */ 13942 sessionalive: 1200000, /* default keepalive period of 20 minutes */ 13943 refreshCallbackUrl: null, /* refresh callback if using AJAX keepalive (no default) */ 13944 logouturl: "./", /* logout URL to end the session */ 13945 signInUrl: null, /* sign-in URL once the session has expired */ 13946 refreshOnClick: true, /* refresh session if user clicks on the page */ 13947 refreshLimit: 120000, /* default period of 2 minutes (ajax calls happen only once during this period) */ 13948 method: "POST", /* the request method to use */ 13949 textOverrides: null, /* text overrides (no default) */ 13950 additionalData: null, /* additional data to send with the request */ 13951 refreshCallback: function( response ) { /* callback function used to check the server response */ 13952 return response.replace( /\s/g, "" ) === "true"; 13953 } 13954 }, 13955 13956 /** 13957 * @function init 13958 * @param {jQuery Event} event Event that triggered the function call 13959 */ 13960 init = function( event ) { 13961 13962 // Start initialization 13963 // returns DOM object = proceed with init 13964 // returns undefined = do not proceed with init (e.g., already initialized) 13965 var elm = wb.init( event, componentName, selector ), 13966 $elm, settings, onReady; 13967 13968 if ( elm ) { 13969 $elm = $( elm ); 13970 13971 // For backwards compatibility where data-wet-boew was used instead of data-wb-sessto 13972 if ( !$elm.attr( "data-" + componentName ) ) { 13973 dataAttr = "wet-boew"; 13974 } 13975 13976 // Merge default settings with overrides from the plugin element 13977 // and save back to the element for future reference 13978 settings = $.extend( {}, defaults, window[ componentName ], $elm.data( dataAttr ) ); 13979 $elm.data( dataAttr, settings ); 13980 13981 // Only initialize the i18nText once 13982 if ( !i18nText ) { 13983 i18n = wb.i18n; 13984 const textOverrides = settings.textOverrides; 13985 if ( textOverrides ) { 13986 i18nText = { 13987 buttonContinue: Object.hasOwn( textOverrides, "buttonContinue" ) ? DOMPurify.sanitize( textOverrides.buttonContinue ) : i18n( "st-btn-cont" ), 13988 buttonEnd: Object.hasOwn( textOverrides, "buttonEnd" ) ? DOMPurify.sanitize( textOverrides.buttonEnd ) : i18n( "st-btn-end" ), 13989 buttonSignin: Object.hasOwn( textOverrides, "buttonSignin" ) ? DOMPurify.sanitize( textOverrides.buttonSignin )
13989: i18n( "tmpl-signin" ), 13990 timeoutBegin: i18n( "st-to-msg-bgn" ), 13991 timeoutEnd: Object.hasOwn( textOverrides, "timeoutEnd" ) ? DOMPurify.sanitize( textOverrides.timeoutEnd ) : i18n( "st-to-msg-end" ), 13992 timeoutTitle: i18n( "st-msgbx-ttl" ), 13993 timeoutAlready: Object.hasOwn( textOverrides, "timeoutAlready" ) ? DOMPurify.sanitize( textOverrides.timeoutAlready ) : i18n( "st-alrdy-to-msg" ) 13994 }; 13995 } else { 13996 i18nText = { 13997 buttonContinue: i18n( "st-btn-cont" ), 13998 buttonEnd: i18n( "st-btn-end" ), 13999 buttonSignin: i18n( "tmpl-signin" ), 14000 timeoutBegin: i18n( "st-to-msg-bgn" ), 14001 timeoutEnd: i18n( "st-to-msg-end" ), 14002 timeoutTitle: i18n( "st-msgbx-ttl" ), 14003 timeoutAlready: i18n( "st-alrdy-to-msg" ) 14004 }; 14005 } 14006 } 14007 14008 onReady = function() { 14009 14010 // Setup the refresh on click behaviour 14011 initRefreshOnClick( $elm, settings ); 14012 14013 // Initialize the keepalive and inactive timeouts of the plugin 14014 $elm.trigger( resetEvent, settings ); 14015 14016 // Identify that initialization has completed 14017 wb.ready( $elm, componentName ); 14018 }; 14019 14020 // Create the modal dialog 14021 initModalDialog( onReady ); 14022 } 14023 }, 14024 14025 /** 14026 * Initializes a timeout that triggers an event 14027 * @function initEventTimeout 14028 * @param {jQuery DOM Element} $elm Element to trigger the event on 14029 * @param {string} eventName Name of the event to trigger on setTimeout 14030 * @param {mixed} time Time to wait before triggering the event 14031 * @param {Object} settings Key-value object 14032 */ 14033 initEventTimeout = function( $elm, eventName, time, settings ) { 14034 var duration = parseTime( time ); 14035 14036 // Clear any existing timeout for the event 14037 clearTimeout( $elm.data( eventName ) ); 14038 14039 // Create the new timeout that will trigger the event 14040 $elm.data( eventName, setTimeout( function() { 14041 $elm.trigger( eventName, settings ); 14042 }, duration ) ); 14043 }, 14044 14045 /** 14046 * Creates the modal dialog element, appends to the <body> and initializes the lightbox plugin 14047 * that is used to create the dialog behaviour. 14048 * @function initModalDialog 14049 */ 14050 initModalDialog = function( callback ) { 14051 var modalID = "#" + componentName + "-modal", 14052 child, modal, temp; 14053 14054 if ( $document.find( modalID ).length === 0 ) { 14055 modal = document.createDocumentFragment(); 14056 temp = document.createElement( "div" ); 14057 14058 // Create the modal dialog. A temp <div> element is used so that its innerHTML can be set as a string. 14059 temp.innerHTML = "<a class='wb-lbx lbx-modal mfp-hide' href='#" + componentName + "-modal'>" + i18nText.timeoutTitle + "</a>" + 14060 "<section id='" + componentName + "-modal' class='mfp-hide modal-dialog modal-content overlay-def'>" + 14061 "<header class='modal-header'><h2 class='modal-title'>" + i18nText.timeoutTitle + "</h2></header>" + 14062 "<div class='modal-body'></div>" + 14063 "<div class='modal-footer'></div>" + 14064 "</section>"; 14065 14066 // Get the temporary <div>'s top level children and append to the fragment 14067 while ( ( child = temp.firstChild ) !== null ) { 14068 modal.appendChild( child ); 14069 } 14070 document.body.appendChild( modal ); 14071 14072 $modal = $document.find( modalID ); 14073 14074 // Get object references to the modal and its triggering link 14075 $modalLink = $modal.prev() 14076 .one( "wb-ready.wb-lbx", callback ) 14077 .trigger( "wb-init.wb-lbx" ); 14078 } else { 14079 callback(); 14080 } 14081 }, 14082 14083 /** 14084 * Initialize the refresh on click keepalive behaviour. This will cause a `keepalive.wb-sessto` 14085 * event to be triggered when the document is clicked, limited by the settings.refreshLimit value. 14086 * @function initRefreshOnClick 14087 * @param {jQuery DOM Element} $elm DOM element to trigger the event on 14088 * @param {Object} settings Key-value object that will be passed when event is triggered. 14089 */ 14090 initRefreshOnClick = function( $elm, settings ) { 14091 if ( settings.refreshOnClick ) { 14092 $document.on( "click", function( event ) { 14093 var className = event.target.className, 14094 lastActivity, currentTime; 14095 14096 // Ignore clicks when the modal dialog is open 14097 if ( ( !className || className.indexOf( confirmClass ) === -1 ) && 14098 $( ".mfp-ready ." + confirmClass ).length === 0 ) { 14099 14100 lastActivity = $elm.data( "lastActivity" ); 14101 currentTime = getCurrentTime(); 14102 if ( !lastActivity || ( currentTime - lastActivity ) > settings.refreshLimit ) { 14103 $elm 14104 .trigger( resetEvent, settings ) 14105 .trigger( keepaliveEvent, settings ); 14106 $elm.data( "lastActivity", currentTime ); 14107 } 14108 } 14109 } ); 14110 } 14111 }, 14112 14113 /** 14114 * Keepalive session event handler. Sends the POST request to determine if the session is still alive. 14115 * @function keepalive 14116 * @param {jQuery Event} event `keepalive.wb-sessto` event that triggered the function call 14117 * @param {Object} settings Key-value object 14118 */ 14119 keepalive = function( event, settings ) { 14120 var $elm = $( event.target ); 14121 if ( settings.refreshCallbackUrl !== null ) { 14122 $.ajax( { 14123 url: settings.refreshCallbackUrl, 14124 data: settings.additionalData, 14125 dataType: "text", 14126 method: settings.method, 14127 success: function( response ) { 14128 14129 // Sanitize the response 14130 response = DOMPurify.sanitize( response ); 14131 14132 // Session is valid 14133 if ( response && settings.refreshCallback( response ) ) { 14134 $elm.trigger( resetEvent, settings ); 14135 14136 // Session has timed out - let the user know they need to sign in again 14137 } else { 14138 14139 // End the inactivity timeouts since the session is already kaput 14140 clearTimeout( $elm.data( inactivityEvent ) ); 14141 clearTimeout( $elm.data( keepaliveEvent ) ); 14142 14143 openModal( { 14144 body: "<p>" + i18nText.timeoutAlready + "</p>", 14145 buttons: $( "<button type='button' class='" + confirmClass + 14146 " btn btn-primary popup-modal-dismiss'>" + i18nText.buttonSignin + "</button>" ) 14147 .data( "logouturl", settings.logouturl ) 14148 } ); 14149 } 14150 } 14151 } ); 14152 } 14153 }, 14154 14155 /** 14156 * Inactivity check event handler. Displays the modal dialog to allow the user to confirm their activity. 14157 * @function inactivity 14158 * @param {jQuery Event} event `inactivity.wb-sessto` event that triggered the function call 14159 * @param {Object} settings Key-value object 14160 */ 14161 inactivity = function( event, settings ) { 14162 var $buttonContinue, $buttonEnd, 14163 time = getTime( settings.reactionTime ), 14164 startTime = getCurrentTime(), 14165 timeoutBegin = i18nText.timeoutBegin 14166 .replace( "#min#", "<span class='min'>" + time.minutes + "</span>" ) 14167 .replace( "#sec#", "<span class='sec'>" + time.seconds + "</span>" ), 14168 buttonStart = "<button type='button' class='", 14169 buttonEnd = "</button>"; 14170 14171 // Clear the keepalive timeout to avoid double firing of requests 14172 clearInterval( $( event.target ).data( keepaliveEvent ) ); 14173 14174 $buttonContinue = $( buttonStart + confirmClass + 14175 " btn btn-primary popup-modal-dismiss'>" + i18nText.buttonContinue + buttonEnd ) 14176 .data( settings ) 14177 .data( "start", startTime ); 14178 $buttonEnd = $( buttonStart + confirmClass + " btn btn-default'>" + 14179 i18nText.buttonEnd + buttonEnd ) 14180 .data( "logouturl", settings.logouturl ); 14181
14182 openModal( { 14183 body: "<p>" + timeoutBegin + "<br />" + i18nText.timeoutEnd + "</p>", 14184 buttons: [ $buttonContinue, $buttonEnd ], 14185 open: function() { 14186 var $minutes = $modal.find( ".min" ), 14187 $seconds = $modal.find( ".sec" ), 14188 endDuration = settings.reactionTime; 14189 14190 countdownInterval = setInterval( function() { 14191 if ( countdown( $minutes, $seconds, startTime, endDuration ) ) { 14192 clearInterval( countdownInterval ); 14193 14194 // Let the user know their session has timed out 14195 $modal.find( "p" ).text( i18nText.timeoutAlready ); 14196 $buttonContinue.text( i18nText.buttonSignin ); 14197 $buttonEnd.hide(); 14198 } 14199 }, 500 ); 14200 } 14201 } ); 14202 }, 14203 14204 /** 14205 * Initialize the inactivity and keepalive timeouts of the plugin 14206 * @function reset 14207 * @param {jQuery Event} event `reset.wb-sessto` event that triggered the function call 14208 * @param {Object} settings Key-value object 14209 */ 14210 reset = function( event, settings ) { 14211 var $elm = $( event.target ); 14212 14213 initEventTimeout( $elm, inactivityEvent, settings.inactivity, settings ); 14214 if ( settings.refreshCallbackUrl !== null ) { 14215 initEventTimeout( $elm, keepaliveEvent, settings.sessionalive, settings ); 14216 } 14217 }, 14218 14219 /** 14220 * Checks if the user wants to keep their session alive. 14221 * @function inactivity 14222 * @param {jQuery Event} event `confirm.wb-sessto` event that triggered the function call 14223 */ 14224 confirm = function( event ) { 14225 var elm = event.target, 14226 $elm = $( elm ), 14227 settings = $elm.data(); 14228 14229 event.preventDefault(); 14230 $.magnificPopup.close(); 14231 clearInterval( countdownInterval ); 14232 14233 // User wants their session maintained 14234 if ( settings.start !== undefined && ( getCurrentTime() - settings.start ) <= settings.reactionTime ) { 14235 $( selector ) 14236 .trigger( resetEvent, settings ) 14237 .trigger( keepaliveEvent, settings ); 14238 14239 // Negative confirmation or the user took too long; logout 14240 } else { 14241 14242 // Use setTimeout() to navigate asynchronously (to support lightbox Escape key presses) 14243 setTimeout( function() { 14244 window.location.href = settings.signInUrl ? settings.signInUrl : settings.logouturl; 14245 } ); 14246 } 14247 }, 14248 14249 /** 14250 * Add the modal dialog's content and display it to the user 14251 * @function openModal 14252 * @param {Object} data Key-value object 14253 */ 14254 openModal = function( data ) { 14255 14256 // Detach the modal to prevent reflows while updating the element 14257 $modal = $modal.detach(); 14258 $modal.find( ".modal-body" ).html( data.body ); 14259 $modal.find( ".modal-footer" ).empty().append( data.buttons ); 14260 14261 // Re-attach the modal and open the dialog 14262 $modal = $modal.insertAfter( $modalLink ); 14263 $modalLink.magnificPopup( "open" ); 14264 14265 // Execute the open callback if it exists 14266 if ( data.open ) { 14267 data.open(); 14268 } 14269 }, 14270 14271 /** 14272 * Returns the current time in milliseconds 14273 * @function getCurrentTime 14274 * @returns {integer} Current time in milliseconds 14275 */ 14276 getCurrentTime = function() { 14277 return ( new Date() ).getTime(); 14278 }, 14279 14280 /** 14281 * Parses a time value into a milliseconds integer value. 14282 * @function parseTime 14283 * @param {Mixed} value The time value to parse (integer or string) 14284 * @returns {integer} Millisecond integer value parsed from the time value 14285 */ 14286 parseTime = function( value ) { 14287 var result, num, mult, 14288 powers = { 14289 ms: 1, 14290 cs: 10, 14291 ds: 100, 14292 s: 1000, 14293 das: 10000, 14294 hs: 100000, 14295 ks: 1000000 14296 }; 14297 14298 if ( value == null ) { //eslint-disable-line no-eq-null 14299 return null; 14300 } 14301 14302 result = /^([0-9]+(?:\.[0-9]*)?)\s*(.*s)?$/.exec( $.trim( value.toString() ) ); 14303 if ( result[ 2 ] ) { 14304 num = parseFloat( result[ 1 ] ); 14305 mult = powers[ result[ 2 ] ] || 1; 14306 return num * mult; 14307 } 14308 return value; 14309 }, 14310 14311 /** 14312 * Converts a millisecond value into minutes and seconds 14313 * @function getTime 14314 * @param {integer} milliseconds The time value in milliseconds 14315 * @returns {Object} An object with a seconds and minutes property 14316 */ 14317 getTime = function( milliseconds ) { 14318 var time = { minutes: "", seconds: "" }; 14319 14320 if ( milliseconds != null ) { //eslint-disable-line no-eq-null 14321 time.minutes = parseInt( ( milliseconds / ( 1000 * 60 ) ) % 60, 10 ); 14322 time.seconds = parseInt( ( milliseconds / 1000 ) % 60, 10 ); 14323 } 14324 return time; 14325 }, 14326 14327 /** 14328 * Given 2 elements representing minutes and seconds, decrement their time value by 1 second 14329 * @function countdown 14330 * @param {jQuery DOM Element} $minutes Element that contains the minute value 14331 * @param {jQuery DOM Element} $seconds Element that contains the second value 14332 * @param { integer } startTime The time value of when the countdown started in milliseconds 14333 * @param { integer } endDuration The time value of the duration of the countdown in milliseconds 14334 * @returns {boolean} Is the countdown finished? 14335 */ 14336 countdown = function( $minutes, $seconds, startTime, endDuration ) { 14337 var newTime = getTime( endDuration - ( getCurrentTime() - startTime ) ); 14338 14339 // Update the DOM elements 14340 $minutes.text( newTime.minutes ); 14341 $seconds.text( newTime.seconds ); 14342 14343 return newTime.minutes <= 0 && newTime.seconds <= 0; 14344 }; 14345 14346// Bind the plugin events 14347$document.on( "timerpoke.wb " + initEvent + " " + keepaliveEvent + " " + 14348inactivityEvent + " " + resetEvent, selector, function( event, settings ) { 14349 14350 var eventType = event.type; 14351 14352 switch ( eventType ) { 14353 case "timerpoke": 14354 case "wb-init": 14355 init( event ); 14356 break; 14357 14358 case "keepalive": 14359 keepalive( event, settings ); 14360 break; 14361 14362 case "inactivity": 14363 inactivity( event, settings ); 14364 break; 14365 14366 case "reset": 14367 reset( event, settings ); 14368 break; 14369 } 14370} ); 14371 14372$document.on( "click", "." + confirmClass, confirm ); 14373 14374// Add the timer poke to initialize the plugin
14375wb.add( selector ); 14376 14377} )( jQuery, DOMPurify, window, document, wb ); 14378 14379/** 14380 * @title WET-BOEW Share widget 14381 * @overview Facilitates sharing Web content on social media platforms. 14382 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 14383 * @author @pjackson28 14384 */ 14385( function( $, window, document, wb ) { 14386"use strict"; 14387 14388/* 14389 * Variable and function definitions. 14390 * These are global to the plugin - meaning that they will be initialized once per page, 14391 * not once per instance of plugin on the page. So, this is a good place to define 14392 * variables that are common to all instances of the plugin on a page. 14393 */ 14394var componentName = "wb-share", 14395 selector = "." + componentName, 14396 initEvent = "wb-init" + selector, 14397 shareLink = "shr-lnk", 14398 panelCount = 0, 14399 $document = wb.doc, 14400 i18n, i18nText, 14401 14402 /* 14403 * Plugin users can override these defaults by setting attributes on the html elements that the 14404 * selector matches. 14405 */ 14406 defaults = { 14407 hdLvl: "h2", 14408 14409 // Supported types are: "page", "video" and "audio" 14410 type: "page", 14411 14412 // For custom types 14413 // custType = " this comment" results in "Share this comment" 14414 custType: "", 14415 14416 url: wb.pageUrlParts.href, 14417 title: document.title || $document.find( "h1:first" ).text(), 14418 14419 pnlId: "", 14420 lnkClass: "", 14421 img: "", 14422 desc: "", 14423 14424 // For filtering the sites that area displayed and controlling the order 14425 // they are displayed. Empty array displays all sites in the default order. 14426 // Otherwise, it displays the sites in the order in the array using the 14427 // keys used by the sites object. 14428 filter: [], 14429 14430 sites: { 14431 14432 // The definitions of the available bookmarking sites, in URL use 14433 // '{u}' for the page URL, '{t}' for the page title, {i} for the image, and '{d}' for the description 14434 blogger: { 14435 name: "Blogger", 14436 url: "https://www.blogger.com/blog_this.pyra?t=&u={u}&n={t}" 14437 }, 14438 bluesky: { 14439 name: "Bluesky", 14440 url: "https://bsky.app/intent/compose?text={t}+{u}" 14441 }, 14442 diigo: { 14443 name: "Diigo", 14444 url: "https://www.diigo.com/post?url={u}&title={t}" 14445 }, 14446 facebook: { 14447 name: "Facebook", 14448 url: "https://www.facebook.com/sharer.php?u={u}&t={t}" 14449 }, 14450 gmail: { 14451 name: "Gmail", 14452 url: "https://mail.google.com/mail/?view=cm&fs=1&tf=1&to=&su={t}&body={u}%0A{d}" 14453 }, 14454 linkedin: { 14455 name: "LinkedIn®", 14456 url: "https://www.linkedin.com/shareArticle?mini=true&url={u}&title={t}&ro=false&summary={d}&source=" 14457 }, 14458 myspace: { 14459 name: "MySpace", 14460 url: "https://www.myspace.com/Modules/PostTo/Pages/?u={u}&t={t}" 14461 }, 14462 pinterest: { 14463 name: "Pinterest", 14464 url: "https://www.pinterest.com/pin/create/button/?url={u}&media={i}&description={t}" 14465 }, 14466 reddit: { 14467 name: "reddit", 14468 url: "https://reddit.com/submit?url={u}&title={t}" 14469 }, 14470 tinyurl: { 14471 name: "TinyURL", 14472 url: "https://tinyurl.com/create.php?url={u}" 14473 }, 14474 tumblr: { 14475 name: "tumblr", 14476 url: "https://www.tumblr.com/share/link?url={u}&name={t}&description={d}" 14477 }, 14478 twitter: { 14479 name: "X", 14480 url: "https://twitter.com/intent/tweet?text={t}&url={u}" 14481 }, 14482 x: { 14483 name: "X", 14484 url: "https://twitter.com/intent/tweet?text={t}&url={u}" 14485 }, 14486 yahoomail: { 14487 name: "Yahoo! Mail", 14488 url: "https://compose.mail.yahoo.com/?to=&subject={t}&body={u}%0A{d}" 14489 }, 14490 whatsapp: { 14491 name: "WhatsApp", 14492 url: "https://api.whatsapp.com/send?text={t}%0A{d}%0A{u}" 14493 } 14494 } 14495 }, 14496 14497 /** 14498 * @method init 14499 * @param {jQuery Event} event Event that triggered the function call 14500 */ 14501 init = function( event ) { 14502 14503 // Start initialization 14504 // returns DOM object = proceed with init 14505 // returns undefined = do not proceed with init (e.g., already initialized) 14506 var elm = wb.init( event, componentName, selector ), 14507 sites, heading, settings, panel, link, $share, $elm, 14508 pageHref, pageTitle, pageImage, pageDescription, 14509 siteProperties, url, shareText, id, pnlId, regex, 14510 filter, i, len, keys, key; 14511 14512 if ( elm ) { 14513 14514 // Only initialize the i18nText once 14515 if ( !i18nText ) { 14516 i18n = wb.i18n; 14517 i18nText = { 14518 shareText: i18n( "shr-txt" ), 14519 page: i18n( "shr-pg" ), 14520 video: i18n( "shr-vid" ), 14521 audio: i18n( "shr-aud" ), 14522 disclaimer: i18n( "shr-disc" ), 14523 email: i18n( "email" ) 14524 }; 14525 14526 // Add an email mailto option 14527 defaults.sites.email = { 14528 name: i18nText.email, 14529 url: "mailto:?subject={t}&body={u}%0A{d}", 14530 isMailto: true 14531 }; 14532 } 14533 14534 $elm = $( elm ); 14535 settings = $.extend( 14536 true, 14537 {}, 14538 defaults, 14539 window[ componentName ], 14540 wb.getData( $elm, componentName ) 14541 ); 14542 sites = settings.sites; 14543 filter = settings.filter; 14544 heading = settings.hdLvl; 14545 14546 shareText = i18nText.shareText + ( settings.custType.length !== 0 ? settings.custType : i18nText[ settings.type ] ); 14547 pnlId = settings.pnlId; 14548 id = "shr-pg" + ( pnlId.length !== 0 ? "-" + pnlId : panelCount ); 14549 pageHref = encodeURIComponent( settings.url ); 14550 14551 regex = /'|'|'/g; 14552 pageTitle = encodeURIComponent( settings.title ) 14553 .replace( regex, "%27" ); 14554 pageImage = encodeURIComponent( settings.img ); 14555 pageDescription = encodeURIComponent( settings.desc ) 14556 .replace( regex, "%27" ); 14557 14558 // Don't create the panel for the second link (class="link-only") 14559 if ( elm.className.indexOf( "link-only" ) === -1 ) { 14560 panel = "<section id='" + id + "' class='shr-pg mfp-hide modal-dialog modal-content overlay-def" + 14561 "'><header class='modal-header'><" + heading + " class='modal-title'>" + 14562 shareText + "</" + heading + "></header><div class='modal-body'>" + 14563 "<ul class='list-unstyled colcount-xs-2'>"; 14564 14565 // If there is no filter array of site keys, then generate an array of site keys 14566 if ( !filter || filter.length === 0 ) { 14567 keys = []; 14568 for ( key in sites ) { 14569 if ( Object.prototype.hasOwnProperty.call( sites, key ) ) { 14570 keys.push( key ); 14571 } 14572 } 14573 } else { 14574 keys = filter; 14575 } 14576 14577 // i18n-friendly sort of the site keys 14578 keys.sort( function( x, y ) { 14579 return wb.normalizeDiacritics( x ).localeCompare( wb.normalizeDiacritics( y ) ); 14580 } ); 14581 14582 // If Twitter and X are both present, only keep X 14583 if ( keys.includes( "twitter" ) && keys.includes( "x" ) ) { 14584 keys = keys.filter( function( item ) { 14585 return item !== "twitter"; 14586 } ); 14587 } 14588 14589 len = keys.length; 14590 14591 // Generate the panel 14592 for ( i = 0; i !== len; i += 1 ) { 14593 key = keys[ i ]; 14594 siteProperties = sites[ key ]; 14595 url = siteProperties.url 14596 .replace( /\{u\}/, pageHref ) 14597 .replace( /\{t\}/, pageTitle ) 14598 .replace( /\{i\}/, pageImage ) 14599 .replace( /\{d\}/, pageDescription ); 14600 panel += "<li><a href='" + url + "' class='" + shareLink + 14601 " " + ( siteProperties.isMailto ? "email" : key ) + 14602 " btn btn-default' target='_blank' rel='noreferrer noopener'>" + 14603 siteProperties.name + "</a></li>"; 14604 } 14605 14606 panel += "</ul><p class='col-sm-12 shr-dscl'>" + i18nText.disclaimer + 14607 "</p><div class='clearfix'></div></div></section>"; 14608 panelCount += 1; 14609 } 14610 link = "<a href='#" + id + "' aria-controls='" + id + 14611 "' class='shr-opn wb-lbx " + settings.lnkClass + 14612 "'><span class='glyphicon glyphicon-share'></span>" + 14613 shareText + "</a>"; 14614 14615 $share = $( ( panel ? panel : "" ) + link ); 14616 14617 $elm.append( $share ); 14618 14619 $share 14620 .trigger( "wb-init.wb-lbx" ); 14621 14622 // Identify that initialization has completed 14623 wb.ready( $elm, componentName ); 14624 } 14625 }; 14626 14627// Bind the init event of the plugin 14628$document.on( "timerpoke.wb " + initEvent, selector, init ); 14629 14630// Add the timer poke to initialize the plugin
14631wb.add( selector ); 14632 14633} )( jQuery, window, document, wb ); 14634 14635/** 14636 * @title WET-BOEW step form 14637 * @overview Provide ability for a form to be broken into steps. 14638 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 14639 * @author @kodecount 14640 */ 14641( function( $, window, document, wb ) { 14642"use strict"; 14643 14644/* 14645 * Variable and function definitions. 14646 * These are global to the plugin - meaning that they will be initialized once per page, 14647 * not once per instance of plugin on the page. So, this is a good place to define 14648 * variables that are common to all instances of the plugin on a page. 14649 */ 14650var componentName = "wb-steps", 14651 selector = ".provisional." + componentName, 14652 initEvent = "wb-init" + selector, 14653 $document = wb.doc, 14654 i18n, i18nText, 14655 btnPrevious, btnNext, btnSubmit, 14656 14657 /** 14658 * @method init 14659 * @param {jQuery Event} evt Event that triggered the function call 14660 */ 14661 init = function( evt ) { 14662 14663 // Start initialization 14664 // returns DOM object = proceed with init 14665 // returns undefined = do not proceed with init (e.g., already initialized) 14666 var elm = wb.init( evt, componentName, selector ); 14667 14668 if ( elm ) { 14669 14670 // Ensure there is a unique id on the element 14671 if ( !elm.id ) { 14672 elm.id = wb.getId(); 14673 } 14674 14675 // Only initialize the i18nText once 14676 if ( !i18nText ) { 14677 i18n = wb.i18n; 14678 i18nText = { 14679 prv: i18n( "prv" ), 14680 nxt: i18n( "nxt" ) 14681 }; 14682 } 14683 14684 /* 14685 * Variable and function definitions 14686 * These will be initialized once per instance of plugin. 14687 */ 14688 var form = elm.getElementsByTagName( "FORM" )[ 0 ], 14689 fieldsets = ( form ) ? $( form ).children( "fieldset" ) : 0, 14690 hasStepsInitialized; 14691 14692 // Initialize navigation buttons 14693 btnPrevious = createStepsButton( "prev", "mrgn-rght-sm mrgn-bttm-md", i18nText.prv ); 14694 btnNext = createStepsButton( "next", "mrgn-bttm-md", i18nText.nxt ); 14695 btnSubmit = form.querySelector( "input[type=submit], button[type=submit]" ); 14696 btnSubmit.classList.add( "mrgn-bttm-md" ); 14697 14698 /* 14699 * Determines if html is correctly formatted and initialize all fieldsets/legend combinations into steps. 14700 */ 14701 for ( var i = 0, len = fieldsets.length; i < len; i++ ) { 14702 14703 /* 14704 * Variable and function definitions 14705 * These well be initialized once per instance of each fieldset. 14706 * Determines the following business rules: 14707 * -Only allow steps if elements are in proper order fieldset -> legend -> div 14708 * -Only allow NEXT button on first step 14709 * -Only allow final SUBMIT button on last step 14710 */ 14711 var fieldset = fieldsets[ i ], 14712 isFirstFieldset = ( i === 0 ) ? true : false, 14713 isLastFieldset = ( i === ( len - 1 ) ) ? true : false, 14714 legend = fieldset.firstElementChild, 14715 div = ( legend && legend.tagName === "LEGEND" ) ? legend.nextElementSibling : false, 14716 buttonGroup = document.createElement( "div" ), 14717 wrapper = document.createElement( "div" ), 14718 buttonGroupClassList = buttonGroup.classList, 14719 divClassList = div.classList; 14720 14721 buttonGroupClassList.add( "buttons" ); 14722 fieldset.parentNode.insertBefore( wrapper, fieldset ); 14723 wrapper.appendChild( fieldset ); 14724 wrapper.classList.add( "steps-wrapper" ); 14725 14726 if ( div && div.tagName === "DIV" ) { 14727 var btnClone; 14728 hasStepsInitialized = true; 14729 14730 if ( !isFirstFieldset ) { 14731 btnClone = btnPrevious.cloneNode( true ); 14732 setStepsBtnEvent( btnClone ); 14733 buttonGroup.appendChild( btnClone ); 14734 wrapper.appendChild( buttonGroup ); 14735 } 14736 14737 if ( !isLastFieldset ) { 14738 btnClone = btnNext.cloneNode( true ); 14739 setStepsBtnEvent( btnClone ); 14740 buttonGroup.appendChild( btnClone ); 14741 } else { 14742 buttonGroup.appendChild( btnSubmit ); 14743 } 14744 14745 wrapper.appendChild( buttonGroup ); 14746 14747 fieldset.classList.add( "wb-tggle-fildst" ); 14748 divClassList.add( "hidden" ); 14749 buttonGroupClassList.add( "hidden" ); 14750 14751 if ( isFirstFieldset ) { 14752 legend.classList.add( "wb-steps-active" ); 14753 btnClone.classList.remove( "hidden" ); 14754 divClassList.remove( "hidden" ); 14755 buttonGroupClassList.remove( "hidden" ); 14756 } 14757 } 14758 } 14759 14760 /* 14761 * if steps has initialized hide any precreated submit or reset buttons 14762 */ 14763 if ( form && hasStepsInitialized ) { 14764 $( form ).children( "input" ).hide(); 14765 wb.ready( $( elm ), componentName ); 14766 } 14767 } 14768 }, 14769 14770 /** 14771 * @method createStepsButton 14772 * @param {string var} tagName, {string var} type, {boolean var} isPrimary, {string var} style, {string var} text 14773 * @returns {Object} A ready-to-use button element 14774 */ 14775 createStepsButton = function( type, style, text ) { 14776 var control = document.createElement( "BUTTON" ); 14777 14778 // set default attributes 14779 control.className = ( type === "prev" ? "btn btn-md btn-default" : "btn btn-md btn-primary" ) + " " + style; 14780 control.setAttribute( "type", "button" ); 14781 control.innerHTML = text; 14782 14783 return control; 14784 }, 14785 14786 /** 14787 * @method setStepsBtnEvent 14788 * @param {JavaScript element} elm 14789 */ 14790 setStepsBtnEvent = function( elm ) { 14791 elm.addEventListener( "click", function( evt ) { 14792 evt.preventDefault(); 14793 var classes = ( this.className ) ? this.className : false, 14794 isNext = ( classes && classes.indexOf( "btn-primary" ) > -1 ), 14795 isFormValid = true, 14796 parentElement = this.parentElement, 14797 parentParentElement = parentElement.parentElement, 14798 parentPreviousClassList = parentElement.previousElementSibling.classList; 14799 14800 // confirm if form is valid 14801 if ( isNext && jQuery.validator && jQuery.validator !== "undefined" ) { 14802 isFormValid = $( "#" + parentParentElement.parentElement.id ).valid(); 14803 } 14804 14805 // continue if valid 14806 if ( isFormValid ) { 14807 showSteps( parentParentElement, isNext ); 14808 if ( isNext ) { 14809 parentPreviousClassList.remove( "wb-steps-error" ); 14810 } 14811 } else if ( isNext && !isFormValid ) { 14812 parentPreviousClassList.add( "wb-steps-error" ); 14813 } 14814 } ); 14815 }, 14816 14817 /** 14818 * @method showSteps 14819 * @param {JavaScript element} elm and {boolean var} isNext 14820 */ 14821 showSteps = function( elm, isNext ) { 14822 var fieldsetElement = elm.getElementsByTagName( "FIELDSET" )[ 0 ], 14823 fields = fieldsetElement.getElementsByTagName( "div" )[ 0 ], 14824 legend = fieldsetElement.getElementsByTagName( "legend" )[ 0 ], 14825 buttonGroup = elm.querySelector( "div.buttons" ), 14826 fieldset; 14827 14828 if ( elm ) { 14829 fields.classList.add( "hidden" ); 14830 buttonGroup.classList.add( "hidden" ); 14831 14832 if ( legend ) { 14833 legend.classList.remove( "wb-steps-active" ); 14834 } 14835 14836 fieldset = ( !isNext ) ? elm.previousElementSibling : elm.nextElementSibling; 14837 if ( fieldset ) { 14838 legend = fieldset.getElementsByTagName( "LEGEND" )[ 0 ]; 14839 elm = fieldset.getElementsByTagName( "DIV" )[ 0 ]; 14840 buttonGroup = fieldset.querySelector( "div.buttons" ); 14841 if ( legend ) { 14842 legend.classList.add( "wb-steps-active" ); 14843 legend.tabIndex = 0; 14844 legend.focus(); 14845 legend.tabIndex = -1; 14846 } 14847 if ( elm ) { 14848 elm.classList.remove( "hidden" ); 14849 } 14850 if ( buttonGroup ) { 14851 buttonGroup.classList.remove( "hidden" ); 14852 } 14853 } 14854 } 14855 }; 14856 14857// Bind the init event of the plugin 14858$document.on( "timerpoke.wb " + initEvent, selector, init ); 14859 14860// Add the timer poke to initialize the plugin
14861wb.add( selector ); 14862 14863} )( jQuery, window, document, wb ); 14864 14865/** 14866 * @title WET-BOEW Tables 14867 * @overview Integrates the DataTables plugin into WET providing searching, sorting, filtering, pagination and other advanced features for tables. 14868 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 14869 * @author @jeresiv 14870 */ 14871/*jshint scripturl:true*/ 14872( function( $, window, wb ) { 14873"use strict"; 14874 14875/* 14876 * Variable and function definitions. 14877 * These are global to the plugin - meaning that they will be initialized once per page, 14878 * not once per instance of plugin on the page. So, this is a good place to define 14879 * variables that are common to all instances of the plugin on a page. 14880 */ 14881var componentName = "wb-tables", 14882 selector = "." + componentName, 14883 initEvent = "wb-init" + selector, 14884 $document = wb.doc, 14885 idCount = 0, 14886 i18n, i18nText, defaults, 14887 14888 /** 14889 * @method init 14890 * @param {jQuery Event} event Event that triggered the function call 14891 */ 14892 init = function( event ) { 14893 14894 // Start initialization 14895 // returns DOM object = proceed with init 14896 // returns undefined = do not proceed with init (e.g., already initialized) 14897 var elm = wb.init( event, componentName, selector ), 14898 elmId; 14899 14900 if ( elm ) { 14901 elmId = elm.id; 14902 14903 // Ensure there is a unique id on the element 14904 if ( !elmId ) { 14905 elmId = componentName + "-id-" + idCount; 14906 idCount += 1; 14907 elm.id = elmId; 14908 } 14909 14910 // Only initialize the i18nText once 14911 if ( !i18nText ) { 14912 i18n = wb.i18n; 14913 i18nText = { 14914 aria: { 14915 sortAscending: i18n( "sortAsc" ), 14916 sortDescending: i18n( "sortDesc" ) 14917 }, 14918 emptyTable: i18n( "emptyTbl" ), 14919 info: i18n( "infoEntr" ), 14920 infoEmpty: i18n( "infoEmpty" ), 14921 infoFiltered: i18n( "infoFilt" ), 14922 lengthMenu: i18n( "lenMenu" ), 14923 loadingRecords: i18n( "load" ), 14924 paginate: { 14925 first: i18n( "first" ), 14926 last: i18n( "last" ), 14927 next: i18n( "nxt" ), 14928 previous: i18n( "prv" ), 14929 page: i18n( "page" ) 14930 }, 14931 processing: i18n( "process" ), 14932 search: i18n( "filter" ), 14933 thousands: i18n( "info1000" ), 14934 zeroRecords: i18n( "infoEmpty" ), 14935 tblFilterInstruction: i18n( "tbFilterInst" ) 14936 }; 14937 } 14938 14939 defaults = { 14940 asStripeClasses: [], 14941 language: i18nText, 14942 dom: "<'top'fil>rt<'bottom'p><'clear'>" 14943 }; 14944 14945 Modernizr.load( { 14946 load: [ "site!deps/jquery.dataTables" + wb.getMode() + ".js" ], 14947 testReady: function() { 14948 return ( $.fn.dataTable && $.fn.dataTable.version ); 14949 }, 14950 complete: function() { 14951 var $elm = $( "#" + elmId ), 14952 dataTableExt = $.fn.dataTableExt, 14953 settings = wb.getData( $elm, componentName ) || {}; 14954 14955 // Explicitly deactivate the paging for the filterEmphasis provisional feature/styling when not configured 14956 if ( $elm.hasClass( "provisional" ) && $elm.hasClass( "filterEmphasis" ) ) { 14957 settings.paging = settings.paging ? settings.paging : false; 14958 } 14959 14960 /* 14961 * Extend sorting support 14962 */ 14963 $.extend( dataTableExt.type.order, { 14964 14965 // Enable internationalization support in the sorting 14966 "html-pre": function( a ) { 14967 return wb.normalizeDiacritics( 14968 !a ? "" : a.replace ? 14969 a.replace( /<.*?>/g, "" ).toLowerCase() : a + "" 14970 ); 14971 }, 14972 "string-case-pre": function( a ) { 14973 return wb.normalizeDiacritics( a ); 14974 }, 14975 "string-pre": function( a ) { 14976 return wb.normalizeDiacritics( a ); 14977 }, 14978 14979 // Formatted number sorting 14980 "formatted-num-asc": function( a, b ) { 14981 return wb.formattedNumCompare( a, b ); 14982 }, 14983 "formatted-num-desc": function( a, b ) { 14984 return wb.formattedNumCompare( b, a ); 14985 } 14986 } ); 14987 14988 // Create the DataTable object 14989 $elm.dataTable( $.extend( true, {}, defaults, window[ componentName ], settings ) ); 14990 } 14991 } ); 14992 } 14993 }, 14994 updatePaginationMarkup = function( $pagination, setFocusOnId ) { 14995 14996 var ol = document.createElement( "OL" ), 14997 li = document.createElement( "LI" ), 14998 paginate_buttons = $pagination.find( ".paginate_button" ), 14999 navFocusOnId; 15000 15001 if ( $pagination.length === 0 ) { 15002 return; 15003 } 15004 15005 // Set the element to get the focus upon navigation 15006 navFocusOnId = setFocusOnId || $pagination.get( 0 ).id; 15007 15008 // Update Pagination List 15009 for ( var i = 0; i < paginate_buttons.length; i++ ) { 15010 var item = li.cloneNode( true ); 15011 item.appendChild( paginate_buttons[ i ] ); 15012 ol.appendChild( item ); 15013 } 15014 15015 ol.className = "pagination mrgn-tp-0 mrgn-bttm-0"; 15016 $pagination.empty(); 15017 $pagination.append( ol ); 15018 15019 // Update the aria-pressed properties on the pagination buttons 15020 // Should be pushed upstream to DataTables 15021 $pagination.find( ".paginate_button" ) 15022 .attr( {
15023 "href": "#" + navFocusOnId 15024 } ) 15025 15026 // This is required to override the datatable.js (v1.10.13) behavior to cancel the event propagation on anchor element. 15027 .on( "keypress", function( evn ) { 15028 if ( evn.keyCode === 13 ) { 15029 window.location = evn.target.href; 15030 } 15031 } ) 15032 15033 .not( ".previous, .next" ) 15034 .attr( "aria-pressed", "false" ) 15035 .html( function( index, oldHtml ) { 15036 return "<span class='wb-inv'>" + i18nText.paginate.page + " </span>" + oldHtml; 15037 } ) 15038 .filter( ".current" ) 15039 .attr( "aria-pressed", "true" ); 15040 }; 15041 15042// Bind the init event of the plugin 15043$document.on( "timerpoke.wb " + initEvent, selector, init ); 15044 15045// Handle the draw.dt event 15046$document.on( "draw.dt", selector, function( event, settings ) { 15047 var $elm = $( event.target ), 15048 pagination = $elm.next( ".bottom" ).find( "div:first-child" ), 15049 pagination_top = $elm.prevAll( ".top" ).find( "div.dataTables_paginate" ), 15050 paginate_buttons = $elm.next( ".bottom" ).find( ".paginate_button" ), 15051 pbLength = paginate_buttons.length, 15052 pHasLF = pagination.find( ".last, .first" ).length === 2, 15053 pHasPN = pagination.find( ".previous, .next" ).length === 2; 15054 15055 // Handle sorting/ordering 15056 var order = $elm.dataTable( { "retrieve": true } ).api().order(); 15057 $elm.find( "th" ).each( function( index ) { 15058 var $th = $( this ), 15059 $btn = $th.find( "button" ); 15060 if ( order && order.length && order[ 0 ][ 0 ] === index ) { 15061 var label = ( order[ 0 ][ 1 ] === "desc" ) ? i18nText.aria.sortAscending : i18nText.aria.sortDescending; 15062 label = $btn.text() + label.replace( / /g, " " ); 15063 $btn.attr( "title", label ); 15064 } 15065 $th.removeAttr( "aria-label" ); 15066 } ); 15067 15068 // Determine if Pagination required 15069 if ( 15070 pbLength === 1 || 15071 ( 15072 pbLength === 3 && 15073 ( 15074 pHasLF || 15075 pHasPN 15076 ) 15077 ) || 15078 ( 15079 pbLength === 5 && 15080 pHasLF && 15081 pHasPN 15082 ) 15083 ) { 15084 pagination.addClass( "hidden" ); 15085 pagination_top.addClass( "hidden" ); 15086 } else { 15087 15088 // Make sure Pagination is visible 15089 pagination.removeClass( "hidden" ); 15090 pagination_top.removeClass( "hidden" ); 15091 15092 updatePaginationMarkup( pagination, $elm.get( 0 ).id ); 15093 updatePaginationMarkup( pagination_top ); 15094 } 15095 15096 // Identify that the table has been updated 15097 $elm.trigger( "wb-updated" + selector, [ settings ] ); 15098} ); 15099 15100// Identify that initialization has completed 15101$document.on( "init.dt", selector, function( event ) { 15102 var $elm = $( event.target ), 15103 settings = $.extend( true, {}, defaults, window[ componentName ], wb.getData( $elm, componentName ) ); 15104 15105 // Handle sorting/ordering 15106 var ordering = ( settings && settings.ordering === false ) ? false : true; 15107 if ( ordering ) { 15108 $elm.find( "thead th" ).each( function() { 15109 var $th = $( this ), 15110 label = ( $th.attr( "aria-sort" ) === "ascending" ) ? i18nText.aria.sortDescending : i18nText.aria.sortAscending; 15111 if ( ( $th.attr( "data-orderable" ) !== "false" ) && !( $th.hasClass( "sorting_disabled" ) ) ) { 15112 $th.html( "<button type='button' aria-controls='" + $th.attr( "aria-controls" ) + "' title='" + $th.text().replace( /'/g, "'" ) + label + "'>" + $th.html() + "<span class='sorting-cnt'><span class='sorting-icons' aria-hidden='true'></span></span></button>" ); 15113 $th.removeAttr( "aria-label tabindex aria-controls" ); 15114 } 15115 } ); 15116 $elm.attr( "aria-label", i18nText.tblFilterInstruction ); 15117 } 15118 15119 // Apply the filter emphasis style 15120 if ( $elm.hasClass( "provisional" ) && $elm.hasClass( "filterEmphasis" ) ) { 15121 $elm.parent().addClass( "provisional filterEmphasis" ); 15122 } 15123 15124 wb.ready( $( event.target ), componentName ); 15125} ); 15126 15127// Handle the draw.dt event 15128$document.on( "submit", ".wb-tables-filter", function( event ) { 15129 15130 event.preventDefault(); 15131 15132 var $form = $( this ), 15133 $datatable = $( "#" + $form.data( "bind-to" ) ).dataTable( { "retrieve": true } ).api(), 15134 $toNumber = function stringToNumber( number ) { 15135 number = number.replace( /[^0-9\-,.]+/g, "" ); 15136 if ( /[,]\d{1,2}$/.test( number ) ) { 15137 number = number.replace( /(\d{2})$/, ".$1" ); 15138 } 15139 number = number.replace( /,/g, "" ); 15140 return parseFloat( number ); 15141 }, 15142 $isDate = function isDate( date ) { 15143 return date instanceof Date && !isNaN( date ); 15144 }; 15145 15146 // Lets reset the search 15147 $datatable.search( "" ).columns().search( "" ); 15148 15149 // Lets loop through all options 15150 var $prevCol = -1, $cachedVal = ""; 15151 $form.find( "[name]" ).each( function() { 15152 var $elm = $( this ), 15153 $val = $elm.val(), 15154 $value = "", 15155 $regex = "", 15156 $column = parseInt( $elm.attr( "data-column" ), 10 ), 15157 $isAopts = $elm.data( "aopts" ), 15158 $aoptsSelector = "[data-aopts*='\"column\": \"" + $column + "\"']:checked", 15159 $aopts = $( $aoptsSelector ), 15160 $aoType = ( $aopts && $aopts.data( "aopts" ) ) ? $aopts.data( "aopts" ).type.toLowerCase() : "", 15161 $fData; 15162 15163 // Ignore the advanced options fields 15164 if ( $isAopts ) { 15165 return; 15166 } 15167 15168 // Verifies if filtering the same column 15169 if ( $column !== $prevCol || $prevCol === -1 ) { 15170 $cachedVal = ""; 15171 } 15172 $prevCol = $column; 15173 15174 // Filters based on input type 15175 if ( $elm.is( "select" ) ) { 15176 $value = $elm.find( "option:selected" ).val(); 15177 } else if ( $elm.is( "input[type='number']" ) ) { 15178 var $minNum, $maxNum = null; 15179 15180 // Retain minimum number (always the first number input) 15181 if ( $cachedVal === "" ) { 15182 $cachedVal = parseFloat( $val ); 15183 $cachedVal = ( $cachedVal ) ? $cachedVal : "-0"; 15184 } else { 15185 $maxNum = parseFloat( $val ); 15186 $maxNum = ( $maxNum ) ? $maxNum : "-0"; 15187 } 15188 $minNum = $cachedVal; 15189 15190 //Number filtering logic needs to be reviewed in order to remove the "-0" value (issue #9235) 15191 // Generates a list of numbers (within the min and max number) 15192 if ( !isNaN( $minNum ) && !isNaN( $maxNum ) ) { 15193 $fData = $datatable.column( $column ).data().filter( function( obj ) { 15194 var $num = $toNumber( obj.toString() ); 15195 15196 if ( !isNaN( $num ) ) { 15197 if ( $aoType === "and" ) {
15198 if ( $cachedVal !== $maxNum && $cachedVal !== "-0" && $maxNum !== "0" && $num >= $minNum && $num <= $maxNum ) { 15199 return true; 15200 } 15201 } else { 15202 if ( $maxNum === null ) { // only one input number 15203 return $minNum === "-0" || $minNum === $num; 15204 } else if ( $maxNum === $cachedVal && $cachedVal === "-0" ) { // both are empty 15205 return true; 15206 } else if ( $maxNum !== "-0" && $minNum === $maxNum && $num === $maxNum ) { // min and max are the same 15207 return true; 15208 } else if ( $maxNum === "-0" && $num >= $minNum ) { // max number is missing 15209 return true; 15210 } else if ( $cachedVal === "-0" && $num <= $maxNum ) { // min number is missing 15211 return true; 15212 } else if ( $cachedVal !== "-0" && $num >= $minNum && $num <= $maxNum ) { // min and max are present 15213 return true; 15214 } 15215 } 15216 } 15217 return false; 15218 } ); 15219 $fData = $fData.join( "|" ); 15220 15221 // If no numbers match set as -0, so no results return 15222 $value = ( $fData ) ? $fData : "-0"; 15223 $regex = "(" + $value.replace( / |\s/g, "\\s" ).replace( /\$/g, "\\$" ) + ")"; 15224 } 15225 } else if ( $elm.is( "input[type='date']" ) && $val ) { 15226 var $minDate, $maxDate; 15227 15228 // Retain minimum date (always the first date input) 15229 if ( $cachedVal === "" ) { 15230 $cachedVal = new Date( $val ); 15231 $cachedVal.setDate( $cachedVal.getDate() + 1 ); 15232 $cachedVal.setHours( 0, 0, 0, 0 ); 15233 } 15234 $minDate = $cachedVal; 15235 15236 // Maximum date is always the current selected date 15237 $maxDate = new Date( $val ); 15238 $maxDate.setDate( $maxDate.getDate() + 1 ); 15239 $maxDate.setHours( 23, 59, 59, 999 ); 15240 15241 // Generates a list of date strings (within the min and max date) 15242 $fData = $datatable.column( $column ).data().filter( function( obj ) { 15243 var $date = obj.replace( /[0-9]{2}\s[0-9]{2}:/g, function( e ) { 15244 return e.replace( /\s/g, "T" ); 15245 } ); 15246 15247 if ( !$date.includes( "T" ) ) { 15248 $date = $date + "T00:00:00"; 15249 } 15250 $date = new Date( $date ); 15251 $date.setHours( 0, 0, 0, 0 ); 15252 15253 if ( !$isDate( $minDate ) || !$isDate( $maxDate ) || !$isDate( $date ) ) { 15254 return; 15255 } 15256 return ( $date >= $minDate && $date <= $maxDate ); 15257 } ); 15258 $fData = $fData.join( "|" ); 15259 15260 // If no dates match set as element value, so no results return 15261 $value = ( $fData ? $fData : $val ); 15262 } else if ( $elm.is( ":checkbox" ) ) { 15263 15264 // Verifies if checkbox is checked before setting value 15265 if ( $elm.is( ":checked" ) ) { 15266 if ( $aoType === "both" ) { 15267 $cachedVal += "(?=.*\\b" + $val + "\\b)"; 15268 } else { 15269 $cachedVal += ( $cachedVal.length > 0 ) ? "|" : ""; 15270 $cachedVal += $val; 15271 } 15272 15273 $value = $cachedVal; 15274 $value = $value.replace( /\s/g, "\\s*" ); 15275 15276 // Adjust regex based on advanced options 15277 switch ( $aoType ) { 15278 case "both": 15279 $regex = "(" + $value + ").*"; 15280 break; 15281 case "either": 15282 $regex = "^(" + $value + ")$"; 15283 break; 15284 case "and": 15285 $regex = ( $value.indexOf( "|" ) > -1 ) ? "^(" + $value + "|[,\\s])(" + $value + "|[,\\s])+$" : "(" + $value + ")"; 15286 break; 15287 case "any": 15288 default: 15289 $regex = "(" + $value + ")"; 15290 break; 15291 } 15292 } 15293 } else { 15294 $value = $val; 15295 } 15296 15297 if ( $value ) { 15298 15299 // Verifies if regex was preset, if not preset use 'contains value' as default 15300 if ( !$regex ) { 15301 if ( $elm[ 0 ].getAttribute( "data-exact" ) ) { 15302 $regex = "^" + $value + "$"; 15303 } else { 15304 $value = $value.replace( /\s/g, "\\s*" ); 15305 $regex = "(" + $value + ")"; 15306 } 15307 } 15308 15309 $datatable.column( $column ).search( $regex, true ); 15310 } 15311 } ); 15312 $datatable.draw(); 15313 return false; 15314} ); 15315 15316$document.on( "click", ".wb-tables-filter [type='reset']", function( event ) { 15317 event.preventDefault(); 15318 15319 var $form = $( this ).closest( ".wb-tables-filter" ), 15320 $datatable = $( "#" + $form.data( "bind-to" ) ).dataTable( { "retrieve": true } ).api(); 15321 15322 $datatable.search( "" ).columns().search( "" ).draw(); 15323 15324 $form.find( "select" ).prop( "selectedIndex", 0 ); 15325 $form.find( "input:checkbox, input:radio" ).prop( "checked", false ); 15326 $form.find( ":input" ).not( ":button, :submit, :reset, :hidden, :checkbox, :radio" ).val( "" ); 15327 return false;
15328} ); 15329 15330// Add the timer poke to initialize the plugin 15331wb.add( selector ); 15332 15333} )( jQuery, window, wb ); 15334 15335/** 15336 * @title WET-BOEW Tabbed interface 15337 * @overview Dynamically stacks multiple sections of content, transforming them into a tabbed interface. 15338 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 15339 * @author WET Community 15340 */ 15341( function( $, window, wb ) { 15342"use strict"; 15343 15344/* 15345 * Variable and function definitions. 15346 * These are global to the plugin - meaning that they will be initialized once per page, 15347 * not once per instance of plugin on the page. So, this is a good place to define 15348 * variables that are common to all instances of the plugin on a page. 15349 */ 15350var componentName = "wb-tabs", 15351 selector = "." + componentName, 15352 initEvent = "wb-init" + selector, 15353 shiftEvent = "wb-shift" + selector, 15354 selectEvent = "wb-select" + selector, 15355 updatedEvent = "wb-updated" + selector, 15356 setFocusEvent = "setfocus.wb", 15357 controls = selector + " ul[role=tablist] a, " + selector + " ul[role=tablist] .tab-count", 15358 initialized = false, 15359 tabsAccordionClass = "tabs-acc", 15360 nestedTglPanelSelector = "> .tabpanels > details > .tgl-panel", 15361 activePanel = "-activePanel", 15362 activateEvent = "click keydown", 15363 pagePath = wb.pageUrlParts.pathname + "#", 15364 $document = wb.doc, 15365 i18n, i18nText, 15366 15367 // Includes "smallview", "xsmallview" and "xxsmallview" 15368 smallViewPattern = "smallview", 15369 isSmallView, oldIsSmallView, 15370 15371 defaults = { 15372 excludePlay: false, 15373 interval: 6, 15374 updateHash: false, 15375 ignoreSession: false 15376 }, 15377 15378 /** 15379 * @method init 15380 * @param {jQuery Event} event Event that triggered the function call 15381 */ 15382 init = function( event ) { 15383 15384 // Start initialization 15385 // returns DOM object = proceed with init 15386 // returns undefined = do not proceed with init (e.g., already initialized) 15387 var elm = wb.init( event, componentName, selector ), 15388 isCarousel = true, 15389 open = "open", 15390 hashTargetLen = 0, 15391 $panels, $tablist, activeId, $openPanel, $elm, elmId, $hashTarget, 15392 settings, $panel, i, len, tablist, isOpen, hashFocus, 15393 newId, positionY, groupClass, $tabPanels, openByHash; 15394 15395 if ( elm ) { 15396 $elm = $( elm ); 15397 15398 // For backwards compatibility. Should be removed in WET v4.1 15399 if ( $elm.children( ".tabpanels" ).length === 0 ) { 15400 $elm.children( "[role=tabpanel], details" ).wrapAll( "<div class='tabpanels'></div>" ); 15401 } 15402 15403 $panels = $elm.find( "> .tabpanels > [role=tabpanel], > .tabpanels > details" ); 15404 $tablist = $elm.children( "[role=tablist]" ); 15405 isCarousel = $tablist.length !== 0; 15406 15407 // If a carousel contains only 1 panel, remove its controls, visually-hide its thumbnails and prevent it from attempting to play 15408 if ( isCarousel && $panels.length === 1 ) { 15409 15410 $elm.removeClass( "show-thumbs playing" ); 15411 $elm.addClass( "exclude-controls" ); 15412 } 15413 15414 activeId = wb.jqEscape( wb.pageUrlParts.hash.substring( 1 ) ); 15415 hashFocus = activeId.length !== 0; 15416 $openPanel = hashFocus ? $panels.filter( "#" + activeId ) : undefined; 15417 openByHash = $openPanel && $openPanel.length !== 0; 15418 elmId = elm.id; 15419 15420 settings = $.extend( 15421 true, 15422 {}, 15423 defaults, 15424 { 15425 interval: $elm.hasClass( "slow" ) ? 15426 9 : $elm.hasClass( "fast" ) ? 15427 3 : defaults.interval, 15428 excludeControls: $elm.hasClass( "exclude-controls" ), 15429 excludePlay: $elm.hasClass( "exclude-play" ), 15430 updateHash: $elm.hasClass( "update-hash" ), 15431 playing: $elm.hasClass( "playing" ), 15432 ignoreSession: $elm.hasClass( "ignore-session" ) 15433 }, 15434 window[ componentName ], 15435 wb.getData( $elm, componentName ) 15436 ); 15437 15438 try { 15439 15440 // If the panel was not set by URL hash 15441 if ( !openByHash ) { 15442 if ( hashFocus ) { 15443 $hashTarget = $panels.find( "#" + activeId ); 15444 hashTargetLen = $hashTarget.length; 15445 } 15446 15447 // If the anchor target is within a panel, then open that panel 15448 if ( hashTargetLen !== 0 ) { 15449 activeId = $hashTarget.closest( "[role=tabpanel]" ).attr( "id" ); 15450 15451 // Attempt to retrieve active panel from sessionStorage 15452 } else { 15453 if ( !settings.ignoreSession ) { 15454 activeId = sessionStorage.getItem( pagePath + elmId + activePanel ); 15455 } 15456 } 15457 15458 if ( activeId ) { 15459 $openPanel = $panels.filter( "#" + activeId ); 15460 } 15461 15462 // If the panel was set by URL hash, then store in sessionStorage 15463 } else { 15464 if ( !settings.ignoreSession ) { 15465 try { 15466 sessionStorage.setItem( pagePath + elmId + activePanel, activeId ); 15467 } catch ( error ) { 15468 15469 /* swallow error */ 15470 } 15471 } 15472 } 15473 } catch ( error ) { 15474 15475 /* swallow error */ 15476 } 15477 15478 // Determine the current view 15479 isSmallView = document.documentElement.className.indexOf( smallViewPattern ) !== -1; 15480 15481 // Only initialize the i18nText once 15482 if ( !i18nText ) { 15483 i18n = wb.i18n; 15484 i18nText = { 15485 prev: i18n( "prv" ), 15486 next: i18n( "nxt" ), 15487 play: i18n( "tab-play" ), 15488 rotStart: i18n( "tab-rot" ).on, 15489 rotStop: i18n( "tab-rot" ).off, 15490 space: i18n( "space" ), 15491 hyphen: i18n( "hyphen" ), 15492 pause: i18n( "pause" ), 15493 tabCount: i18n( "lb-curr" ) 15494 }; 15495 } 15496 15497 // Build the tablist and enhance the panels as needed for details/summary 15498 if ( !isCarousel ) { 15499 $elm.addClass( tabsAccordionClass ); 15500 groupClass = elmId + "-grp"; 15501 $tabPanels = $elm.children( ".tabpanels" ); 15502 $panels = $tabPanels.children( "details" ); 15503 len = $panels.length; 15504 15505 $tabPanels.detach(); 15506 15507 // Ensure there is only one panel open 15508 // Order of priority is hash, open property, first details 15509 if ( !$openPanel || $openPanel.length === 0 ) { 15510 $openPanel = $panels.filter( "[open]" ).first(); 15511 if ( $openPanel.length === 0 ) { 15512 $openPanel = $panels.eq( 0 ); 15513 } 15514 } 15515 $panels.removeAttr( open ); 15516 $openPanel.attr( open, open ); 15517 15518 // Hide the tablist in small view and the summary elements in large view 15519 tablist = "<ul role='tablist' aria-live='off' class='generated'>"; 15520 15521 for ( i = 0; i !== len; i += 1 ) { 15522 $panel = $panels.eq( i ); 15523 $panel 15524 .addClass( groupClass ) 15525 .html( 15526 $panel.html() 15527 .replace( /(<\/summary>)/i, "$1<div class='tgl-panel'>" ) + 15528 "</div>" 15529 ); 15530 15531 newId = $panel.attr( "id" ); 15532 if ( !newId ) { 15533 newId = wb.getId(); 15534 $panel.attr( "id", newId ); 15535 } 15536 isOpen = !!$panel.attr( open ); 15537 15538 if ( isSmallView ) { 15539 if ( !wb.supportsDetails ) { 15540 $panel.toggleClass( "open", isOpen ); 15541 } 15542 } else { 15543 $panel.attr( { 15544 role: "tabpanel", 15545 open: open 15546 } );
15547 $panel.addClass( ( wb.supportsDetails ? "" : open + " " ) + "fade " + ( isOpen ? "in" : "noheight out wb-inv" ) ); 15548 } 15549 15550 tablist += "<li" + ( isOpen ? " class='active'" : "" ) + 15551 "><a id='" + newId + "-lnk' href='#" + newId + "'>" + 15552 $panel.children( "summary" ).html() + "</a></li>"; 15553 } 15554 15555 $tablist = $( tablist + "</ul>" ); 15556 $tabPanels.find( "> details > summary" ) 15557 .addClass( "wb-toggle tgl-tab" ) 15558 .attr( "data-toggle", "{\"parent\": \"#" + elmId + 15559 "\", \"group\": \"." + groupClass + "\"}" ); 15560 15561 $elm 15562 .prepend( $tablist ) 15563 .append( $tabPanels ) 15564 .trigger( "wb-init.wb-toggle" ); 15565 } else if ( $openPanel && $openPanel.length !== 0 ) { 15566 $panels.filter( ".in" ) 15567 .addClass( "out noheight" ) 15568 .removeClass( "in" ); 15569 $openPanel 15570 .addClass( "in" ) 15571 .removeClass( "out noheight" ); 15572 $tablist.find( ".active" ) 15573 .removeClass( "active" ); 15574 $tablist.find( "a" ) 15575 .filter( "[href$='" + activeId + "']" ) 15576 .parent() 15577 .addClass( "active" ); 15578 } 15579 15580 drizzleAria( $panels, $tablist ); 15581 15582 if ( isCarousel ) { 15583 15584 // Returns true if the tabs should be rotating automatically 15585 if ( createControls( $tablist, settings ) ) { 15586 15587 // Register this specific tabs instance for timerpoke.wb events 15588 wb.add( "#" + elmId + selector ); 15589 } 15590 } 15591 15592 // If focus is being set by the URL hash, then ensure the tabs are 15593 // not above the top of the viewport (if the panel was the target), 15594 // or the anchor is not above the top of viewport (if the anchor was 15595 // the target) 15596 if ( hashFocus ) { 15597 15598 // Need a slight delay to allow for the reflow 15599 setTimeout( function() { 15600 if ( openByHash ) { 15601 positionY = $tablist.offset().top; 15602 } else if ( hashTargetLen !== 0 ) { 15603 positionY = $hashTarget.offset().top; 15604 } else { 15605 positionY = -1; 15606 } 15607 15608 if ( positionY !== -1 && positionY < document.body.scrollTop ) { 15609 document.body.scrollTop = positionY; 15610 } 15611 }, 1 ); 15612 } 15613 15614 $elm.data( { 15615 "wb-tabs": { 15616 panels: $panels, 15617 tablist: $tablist, 15618 settings: settings, 15619 ctime: 0 15620 } 15621 } ); 15622 15623 initialized = true; 15624 onResize( $elm ); 15625 15626 // Update the URL hash if needed 15627 if ( settings.updateHash ) { 15628 updateHash( $openPanel[ 0 ] ); 15629 } 15630 15631 // Identify that initialization has completed 15632 wb.ready( $elm, componentName ); 15633 } 15634 }, 15635 15636 /** 15637 * @method onTimerPoke 15638 * @param {jQuery DOM element} $elm The plugin element 15639 */ 15640 onTimerPoke = function( $elm ) { 15641 var data = $elm.data( componentName ),
15642 delayCurrent = parseFloat( data.ctime ) + 0.5; 15643 15644 // Check if we need to rotate panels 15645 if ( parseFloat( data.settings.interval ) <= delayCurrent ) { 15646 $elm.trigger( shiftEvent ); 15647 delayCurrent = 0; 15648 } 15649 15650 data.ctime = delayCurrent; 15651 $elm.data( componentName, data ); 15652 }, 15653 15654 /** 15655 * @method createControls 15656 * @param {jQuery DOM element} $tablist The plugin element 15657 * @param {object} settings Settings for the tabs instance 15658 * @returns {boolean} Whether or not the tabs should be rotating initially 15659 */ 15660 createControls = function( $tablist, settings ) { 15661 var prevText = i18nText.prev, 15662 nextText = i18nText.next, 15663 spaceText = i18nText.space, 15664 excludeControls = settings.excludeControls, 15665 excludePlay = settings.excludePlay, 15666 isPlaying = !excludeControls && !excludePlay && settings.playing, 15667 state = isPlaying ? i18nText.pause : i18nText.play, 15668 hidden = isPlaying ? i18nText.rotStop : i18nText.rotStart, 15669 glyphiconStart = "<span class='glyphicon glyphicon-", 15670 wbInvStart = "<span class='wb-inv'>", 15671 tabsToggleStart = "<li class='control ", 15672 btnMiddle = "' href='javascript:;' role='button' title='", 15673 btnEnd = "</span></a></li> ", 15674 iconState = glyphiconStart + ( isPlaying ? "pause" : "play" ) + "'></span>", 15675 $tabs = $tablist.find( "[role=tab]" ), 15676 currentIndex = $tabs.index( $tabs.filter( "[aria-selected=true]" ) ) + 1, 15677 i18nTabCount = i18nText.tabCount, 15678 firstReplaceIndex = i18nTabCount.indexOf( "%" ), 15679 lastReplaceIndex = i18nTabCount.lastIndexOf( "%" ) + 1, 15680 prevControl = tabsToggleStart + "prv'><a class='prv" + btnMiddle + 15681 prevText + "'>" + glyphiconStart + "chevron-left'></span>" + 15682 wbInvStart + prevText + btnEnd, 15683 tabCount = tabsToggleStart + " tab-count' tabindex='0'><div>" + 15684 i18nTabCount.substring( 0, firstReplaceIndex ) + 15685 "<div class='curr-count'>" + 15686 i18nTabCount.substring( firstReplaceIndex, lastReplaceIndex ) 15687 .replace( "%curr%", "<span class='curr-index'>" + currentIndex + "</span>" ) 15688 .replace( "%total%", $tabs.length ) + 15689 "</div>" + i18nTabCount.substring( lastReplaceIndex ) + 15690 "</div></li>", 15691 nextControl = tabsToggleStart + "nxt'><a class='nxt" + btnMiddle + 15692 nextText + "'>" + glyphiconStart + "chevron-right'></span>" + 15693 wbInvStart + nextText + btnEnd, 15694 playControl = tabsToggleStart + "plypause'><a class='plypause" + 15695 btnMiddle + state + "'>" + iconState + " <span>" + state + 15696 "</span>" + wbInvStart + spaceText + i18nText.hyphen + spaceText + 15697 hidden + btnEnd; 15698 15699 if ( !excludeControls ) { 15700 $tablist.prepend( prevControl + tabCount + nextControl ); 15701 } 15702 15703 if ( !excludeControls && !excludePlay ) { 15704 $tablist.append( playControl ); 15705 } 15706 $tablist.find( "a[role=button]" ).attr( "href", "javascript:;" ); 15707 15708 return isPlaying; 15709 }, 15710 15711 /** 15712 * @method drizzleAria 15713 * @param {jQuery DOM element} $panels Tabpanel groupings 15714 * @param {jQuery DOM element} $tablist Pointers to the groupings 15715 */ 15716 drizzleAria = function( $panels, $tabList ) { 15717 15718 // Let's process the elements for aria 15719 var panels = $panels.get(), 15720 tabCounter = panels.length - 1, 15721 listItems = $tabList.children().get(), 15722 listCounter = listItems.length - 1, 15723 isDetails = $panels[ 0 ].nodeName.toLowerCase() === "details", 15724 isActive, item, link, panelId, activeFound; 15725 15726 for ( ; tabCounter !== -1; tabCounter -= 1 ) { 15727 item = panels[ tabCounter ]; 15728 isActive = item.className.indexOf( "out" ) === -1; 15729 15730 if ( !isDetails || !isSmallView ) { 15731 item.setAttribute( "aria-hidden", isActive ? "false" : "true" ); 15732 item.setAttribute( "aria-expanded", isActive ? "true" : "false" ); 15733 } 15734 item.setAttribute( "aria-labelledby", item.id + "-lnk" ); 15735 } 15736 15737 activeFound = false; 15738 for ( ; listCounter !== -1; listCounter -= 1 ) { 15739 item = listItems[ listCounter ]; 15740 item.setAttribute( "role", "presentation" ); 15741 15742 isActive = item.className.indexOf( "active" ) !== -1; 15743 if ( isActive ) { 15744 activeFound = true; 15745 } else if ( listCounter === 0 && !activeFound ) { 15746 isActive = true; 15747 item.className += " active"; 15748 } 15749 15750 link = item.getElementsByTagName( "a" )[ 0 ]; 15751 panelId = link.getAttribute( "href" ).substring( 1 ); 15752 15753 link.tabIndex = isActive ? "0" : "-1"; 15754 link.setAttribute( "role", "tab" ); 15755 link.setAttribute( "aria-selected", isActive ? "true" : "false" ); 15756 link.setAttribute( "aria-controls", panelId ); 15757 link.id = panelId + "-lnk"; 15758 } 15759 $tabList.attr( "aria-live", "off" ); 15760 }, 15761 15762 /** 15763 * @method updateHash 15764 * @param {DOM element} elm Tabpanel to be referenced in the URL hash 15765 */ 15766 updateHash = function( elm ) { 15767 var elmId = elm.id; 15768 15769 wb.ignoreHashChange = true;
15770 elm.id += "-off"; 15771 window.location.hash = elmId; 15772 elm.id = elmId; 15773 wb.ignoreHashChange = false; 15774 }, 15775 15776 updateNodes = function( $panels, $controls, $next, $control ) { 15777 var $tabs = $controls.find( "[role=tab]" ), 15778 newIndex = $tabs.index( $control ) + 1, 15779 $currPanel = $panels.filter( ".in" ), 15780 $container = $next.closest( selector ), 15781 tabSettings = $container.data( componentName ).settings, 15782 mPlayers = $currPanel.find( ".wb-mltmd-inited" ).get(), 15783 mPlayersLen = mPlayers.length, 15784 mPlayer, i, j, last; 15785 15786 // Handle the direction of the slide transitions 15787 if ( $currPanel[ 0 ].className.indexOf( "slide" ) !== -1 ) { 15788 i = $panels.index( $currPanel ); 15789 j = $panels.index( $next ); 15790 last = $panels.length - 1; 15791 15792 $panels.toggleClass( 15793 "reverse", 15794 ( i > j && ( i !== last || j !== 0 ) ) || ( i === 0 && j === last ) 15795 ); 15796 } 15797 15798 $currPanel 15799 .removeClass( "in" ) 15800 .addClass( "out noheight" ) 15801 .attr( { 15802 "aria-hidden": "true", 15803 "aria-expanded": "false" 15804 } ); 15805 15806 // Pause all multimedia players in the current panel 15807 for ( i = 0; i !== mPlayersLen; i += 1 ) { 15808 mPlayer = mPlayers[ i ]; 15809 if ( mPlayer.player ) { 15810 mPlayer.player( "pause" ); 15811 } 15812 } 15813 15814 $next 15815 .removeClass( "out noheight" ) 15816 .addClass( "in" ) 15817 .attr( { 15818 "aria-hidden": "false", 15819 "aria-expanded": "true" 15820 } ); 15821 15822 $controls 15823 .find( ".active" ) 15824 .removeClass( "active" ) 15825 .children( "a" ) 15826 .attr( { 15827 "aria-selected": "false", 15828 tabindex: "-1" 15829 } ); 15830 15831 // Update the Item x of n 15832 $controls 15833 .find( ".curr-index" ) 15834 .html( newIndex ); 15835 15836 $control 15837 .attr( { 15838 "aria-selected": "true", 15839 tabindex: "0" 15840 } ) 15841 .parent() 15842 .addClass( "active" ); 15843 15844 // Update sessionStorage with the current active panel 15845 if ( !tabSettings.ignoreSession ) { 15846 try { 15847 sessionStorage.setItem( 15848 pagePath + $container.attr( "id" ) + activePanel, 15849 $next.attr( "id" ) 15850 ); 15851 } catch ( error ) { 15852 15853 /* swallow error */ 15854 } 15855 } 15856 15857 // Update the URL hash if needed 15858 if ( tabSettings.updateHash ) { 15859 updateHash( $next[ 0 ] ); 15860 } 15861 15862 // Identify that the tabbed interface/carousel was updated 15863 $container.trigger( updatedEvent, [ $next ] ); 15864 }, 15865 15866 /** 15867 * @method onPick 15868 * @param {jQuery DOM element} $sldr The plugin element 15869 * @param {jQuery DOM element} $elm The selected link from the tablist 15870 */ 15871 onPick = function( $sldr, $elm ) { 15872 var data = $sldr.data( componentName ),
15873 $panels = data.panels, 15874 $controls = data.tablist, 15875 $next = $panels.filter( "#" + $elm.attr( "aria-controls" ) ); 15876 15877 updateNodes( $panels, $controls, $next, $elm ); 15878 }, 15879 15880 /** 15881 * @method onShift 15882 * @param (jQuery event} event Current event 15883 * @param {jQuery DOM element} $elm The plugin element 15884 */ 15885 onShift = function( event, $elm ) { 15886 var data = $elm.data( componentName ), 15887 $panels = data.panels, 15888 len = $panels.length, 15889 current = $elm.find( "> .tabpanels > .in" ).prevAll( "[role=tabpanel]" ).length, 15890 autoCycle = !event.shiftto, 15891 next = current > len ? 0 : current + ( autoCycle ? 1 : event.shiftto ); 15892 15893 onSelect( $panels[ ( next > len - 1 ) ? 0 : ( next < 0 ) ? len - 1 : next ].id, autoCycle ); 15894 }, 15895 15896 /** 15897 * @method onSelect 15898 * @param (string) id Id attribute of the panel 15899 * @param (boolean) autoCycle Whether change is caused by an auto cycle 15900 */ 15901 onSelect = function( id, autoCycle ) { 15902 var panelSelector = "#" + id, 15903 $panel = $( panelSelector ), 15904 $panelSelectorLink; 15905 15906 if ( isSmallView && $panel[ 0 ].nodeName.toLowerCase() === "details" ) { 15907 $panel.children( "summary" ).trigger( $panel.attr( "open" ) ? setFocusEvent : "click" ); 15908 } else { 15909 $panelSelectorLink = $( panelSelector + "-lnk" ); 15910 $panelSelectorLink.trigger( { 15911 type: "click", 15912 which: autoCycle ? undefined : 1 15913 } ); 15914 15915 // Don't change the focus if change is cause by an auto cycle 15916 if ( !autoCycle ) { 15917 $panelSelectorLink.trigger( setFocusEvent ); 15918 } 15919 } 15920 }, 15921 15922 /** 15923 * @method onCycle 15924 * @param {jQuery DOM element} $elm The plugin element 15925 * @param {integer} shiftto The item to shift to 15926 */ 15927 onCycle = function( $elm, shiftto ) { 15928 $elm.trigger( { 15929 type: shiftEvent, 15930 shiftto: shiftto 15931 } ); 15932 }, 15933 15934 /** 15935 * @method onResize 15936 * @param {jQuery Object} $currentElm Element being initialized (only during initialization process). 15937 */ 15938 onResize = function( $currentElm ) { 15939 var $elms, $elm, $tabPanels, $details, $detailsElm, $tablist, 15940 $openDetails, openDetailsId, activeId, $summary, $panelElm, 15941 i, len, j, len2, viewChange, isInit, isActive; 15942 15943 if ( initialized ) { 15944 isSmallView = document.documentElement.className.indexOf( smallViewPattern ) !== -1; 15945 viewChange = isSmallView !== oldIsSmallView; 15946 isInit = $currentElm.length ? true : false; 15947 15948 if ( viewChange || isInit ) { 15949 $elms = isInit ? $currentElm : $( selector ); 15950 len = $elms.length; 15951 15952 for ( i = 0; i !== len; i += 1 ) { 15953 $elm = $elms.eq( i ); 15954 $tabPanels = $elm.children( ".tabpanels" ); 15955 $details = $tabPanels.children( "details" ); 15956 len2 = $details.length; 15957 15958 if ( $details.length !== 0 ) { 15959 $tabPanels.detach(); 15960 $summary = $details.children( "summary" ); 15961 $tablist = $elm.children( "ul" ); 15962 15963 if ( isSmallView ) { 15964 15965 // Switch to small view 15966 activeId = $tablist.find( ".active a" ).attr( "href" ).substring( 1 ); 15967 for ( j = 0; j !== len2; j += 1 ) { 15968 $detailsElm = $details.eq( j ); 15969 $panelElm = $detailsElm.children( ".tgl-panel" ); 15970 isActive = $detailsElm.attr( "id" ) === activeId; 15971 15972 $detailsElm 15973 .removeAttr( "role aria-expanded aria-hidden" ) 15974 .removeClass( "fade out noheight in" ) 15975 .toggleClass( "open", isActive ); 15976 15977 $panelElm 15978 .attr( "role", "tabpanel" ) 15979 .removeAttr( "aria-expanded" ) 15980 .removeAttr( "aria-hidden" ); 15981 15982 if ( isActive ) { 15983 $detailsElm.attr( "open", "open" ); 15984 } else { 15985 $detailsElm.removeAttr( "open" ); 15986 } 15987 15988 if ( !isInit ) { 15989 $detailsElm 15990 .children( "summary" ) 15991 .attr( { 15992 "aria-expanded": isActive, 15993 "aria-selected": isActive 15994 } ); 15995 } 15996 } 15997 } else if ( oldIsSmallView ) { 15998 15999 // Switch to large view 16000 $openDetails = $details.filter( "[open]" ); 16001 openDetailsId = $openDetails.attr( "id" ); 16002 16003 $openDetails = ( $openDetails.length === 0 ? $details : $openDetails ).eq( 0 ); 16004 16005 $details 16006 .attr( { 16007 role: "tabpanel", 16008 open: "open" 16009 } ) 16010 .not( $openDetails ) 16011 .addClass( "fade out noheight wb-inv" ) 16012 .attr( { 16013 "aria-hidden": "true", 16014 "aria-expanded": "false" 16015 } ); 16016 16017 $details.children( ".tgl-panel" ).removeAttr( "role" ); 16018 16019 $openDetails 16020 .addClass( "fade in" ) 16021 .attr( { 16022 "aria-hidden": "false", 16023 "aria-expanded": "true" 16024 } ); 16025 } 16026 16027 $summary.attr( "aria-hidden", !isSmallView ); 16028 $tablist.attr( "aria-hidden", isSmallView ); 16029 16030 $elm.append( $tabPanels ); 16031 16032 // Update the tablist role 16033 if ( isSmallView && !isInit ) { 16034 $elm.attr( "role", "tablist" ); 16035 } else if ( oldIsSmallView ) { 16036 $elm 16037 .removeAttr( "role" ) 16038 .find( nestedTglPanelSelector ).removeAttr( "role" ); 16039 16040 $elm.find( "> ul [href$='" + openDetailsId + "']" ).trigger( "click" ); 16041 } 16042 } 16043 } 16044 16045 // Need timeout to account for Toggle changes 16046 if ( isInit && !isSmallView && $elms.hasClass( tabsAccordionClass ) ) { 16047 setTimeout( function() { 16048 $elms 16049 .removeAttr( "role" ) 16050 .find( nestedTglPanelSelector ).removeAttr( "role" ); 16051 }, 1 ); 16052 } 16053 } 16054 16055 oldIsSmallView = isSmallView; 16056 } 16057 16058 if ( viewChange || isInit ) { 16059 16060 // Remove wb-inv from regular tabs that were used to prevent FOUC (after 300ms delay) 16061 setTimeout( function() { 16062 $( selector + " .tabpanels > details.wb-inv" ).removeClass( "wb-inv" ); 16063 }, 300 ); 16064 } 16065 }; 16066 16067// Bind the init event of the plugin 16068$document.on( "timerpoke.wb " + initEvent + " " + shiftEvent + " " + selectEvent, selector, function( event ) { 16069 var eventTarget = event.target, 16070 eventCurrentTarget = event.currentTarget, 16071 $elm; 16072 16073 // Filter out any events triggered by descendants 16074 if ( eventCurrentTarget === eventTarget ) { 16075 switch ( event.type ) { 16076 case "timerpoke": 16077 $elm = $( eventTarget ); 16078 if ( !$elm.hasClass( componentName + "-inited" ) ) { 16079 init( event ); 16080 } else if ( $elm.hasClass( "playing" ) ) { 16081 onTimerPoke( $elm ); 16082 } 16083 break; 16084 16085 /* 16086 * Init 16087 */ 16088 case "wb-init": 16089 init( event ); 16090 break; 16091 16092 /* 16093 * Change tab panels by a delta 16094 */ 16095 case "wb-shift": 16096 onShift( event, $( eventTarget ) ); 16097 break; 16098 16099 /* 16100 * Select a specific tab panel 16101 */ 16102 case "wb-select":
16103 onSelect( event.id ); 16104 break; 16105 } 16106 } 16107 16108 /* 16109 * Since we are working with events we want to ensure that we are being passive about our control, 16110 * so returning true allows for events to always continue 16111 */ 16112 return true; 16113} ); 16114 16115/* 16116 * Tabs, next, previous and play/pause 16117 */ 16118$document.on( activateEvent, controls, function( event ) { 16119 var which = event.which, 16120 elm = event.currentTarget, 16121 className = elm.className, 16122 spaceText = i18nText.space, 16123 $elm, $sldr, sldrId, plypause, buttonText, data, isPlaying, isPlayPause; 16124 16125 // No control, alt or meta keys and only left mouse button, enter key, 16126 // space bar, escape key and arrow keys 16127 if ( !( event.ctrlKey || event.altKey || event.metaKey ) && 16128 ( !which || which === 1 || which === 13 || which === 27 || 16129 which === 32 || ( which > 36 && which < 41 ) ) ) { 16130 16131 // Stop propagation of the activate event 16132 event.preventDefault(); 16133 if ( event.stopPropagation ) { 16134 event.stopImmediatePropagation(); 16135 } else { 16136 event.cancelBubble = true; 16137 } 16138 16139 $elm = $( elm ); 16140 $sldr = $elm.closest( selector ); 16141 sldrId = $sldr[ 0 ].id; 16142 isPlaying = $sldr.hasClass( "playing" ); 16143 isPlayPause = className.indexOf( "plypause" ) !== -1; 16144 16145 // Reset ctime to 0 16146 data = $sldr.data( componentName ); 16147 data.ctime = 0; 16148 $sldr.data( componentName, data ); 16149 16150 // Stop the slider from playing unless it is already stopped 16151 // and the play button is activated 16152 if ( ( isPlaying && which ) || ( isPlayPause && !( which > 36 && which < 41 ) ) ) { 16153 if ( isPlaying ) { 16154 wb.remove( "#" + sldrId + selector ); 16155 } else { 16156 wb.add( "#" + sldrId + selector ); 16157 } 16158 16159 $sldr.toggleClass( "playing" ); 16160 isPlaying = !isPlaying; 16161 buttonText = isPlaying ? i18nText.pause : i18nText.play; 16162 16163 plypause = $sldr.find( "a.plypause" )[ 0 ]; 16164 plypause.setAttribute( "title", buttonText ); 16165 plypause.innerHTML = "<span class='glyphicon glyphicon-" + 16166 ( isPlaying ? "pause" : "play" ) + "'></span> " + 16167 "<span>" + buttonText + "</span><span class='wb-inv'>" + 16168 spaceText + i18nText.hyphen + spaceText + 16169 ( isPlaying ? i18nText.rotStop : i18nText.rotStart ) + "</span>"; 16170 } 16171 16172 // Arrow keys 16173 if ( which > 36 ) { 16174 onCycle( $sldr, which < 39 ? -1 : 1 ); 16175 $sldr.find( "> [role=tablist] .active a" ).trigger( setFocusEvent ); 16176 16177 // Not the escape key 16178 } else if ( which !== 27 ) { 16179 16180 // If the target is a tab 16181 if ( elm.getAttribute( "role" ) === "tab" ) { 16182 16183 // Only change the tabpanel if the tab is not currently selected 16184 if ( elm.getAttribute( "aria-selected" ) !== "true" ) { 16185 onPick( $sldr, $elm ); 16186 } 16187 16188 // Put focus on the tab panel if the enter key or space bar are used 16189 if ( which === 13 || which === 32 ) { 16190 $sldr.find( elm.getAttribute( "href" ) ) 16191 .trigger( setFocusEvent ); 16192 } 16193 16194 // If the target is next, previous or tab count 16195 } else if ( !isPlayPause ) { 16196 onCycle( $sldr, className.indexOf( "prv" ) !== -1 ? -1 : 1 ); 16197 } 16198 } 16199 } 16200 16201 /* 16202 * Since we are working with events we want to ensure that we are being passive about our control, 16203 * so returning true allows for events to always continue 16204 */ 16205 return true; 16206} ); 16207 16208$document.on( activateEvent, selector + " [role=tabpanel]", function( event ) { 16209 var currentTarget = event.currentTarget, 16210 which = event.which, 16211 $container; 16212 16213 // Stop propagation of the click/keydown event 16214 if ( event.stopPropagation ) { 16215 event.stopImmediatePropagation(); 16216 } else { 16217 event.cancelBubble = true; 16218 } 16219 16220 // Ctrl + Up arrow 16221 if ( event.ctrlKey && event.which === 38 ) { 16222 16223 // Move focus to the tab or summary element 16224 if ( isSmallView ) { 16225 $( currentTarget ).prev().trigger( setFocusEvent ); 16226 } else { 16227 $( currentTarget ) 16228 .closest( selector ) 16229 .find( "[href$='#" + currentTarget.id + "']" ) 16230 .trigger( setFocusEvent ); 16231 } 16232 16233 // Left mouse button click or escape key 16234 } else if ( !which || which === 1 || which === 27 ) { 16235 $container = $( event.currentTarget ).closest( selector ); 16236 16237 // Stop the carousel 16238 if ( $container.hasClass( "playing" ) ) { 16239 $container.find( ".plypause" ).trigger( "click" ); 16240 } 16241 } 16242} ); 16243 16244// Handling for links to tabs from within a panel 16245$document.on( "click", selector + " [role=tabpanel] a", function( event ) { 16246 var currentTarget = event.currentTarget, 16247 href = currentTarget.getAttribute( "href" ), 16248 which = event.which, 16249 $tabpanels, $panel, $summary; 16250 16251 // Ignore middle and right mouse buttons 16252 if ( ( !which || which === 1 ) && href.charAt( 0 ) === "#" ) { 16253 $tabpanels = $( currentTarget ).closest( ".tabpanels" ); 16254 $panel = $tabpanels.children( "#" + wb.jqEscape( href.substring( 1 ) ) ); 16255 if ( $panel.length !== 0 ) { 16256 event.preventDefault(); 16257 $summary = $panel.children( "summary" ); 16258 if ( $summary.length !== 0 && $summary.attr( "aria-hidden" ) !== "true" ) { 16259 $summary.trigger( "click" ); 16260 } else { 16261 $tabpanels.parent().find( href + "-lnk" ).trigger( "click" ); 16262 } 16263 } 16264 } 16265} ); 16266 16267// These events only fire at the document level 16268$document.on( wb.resizeEvents, onResize ); 16269 16270$document.on( activateEvent, selector + " > .tabpanels > details > summary", function( event ) { 16271 var which = event.which, 16272 details = event.currentTarget.parentNode, 16273 $details, $container, tabSettings; 16274 16275 if ( !( event.ctrlKey || event.altKey || event.metaKey ) && 16276 ( !which || which === 1 || which === 13 || which === 32 ) ) { 16277 16278 $container = $( details.parentNode.parentNode ); 16279 $details = $( details ); 16280 tabSettings = $container.data( componentName ).settings; 16281 16282 // Update sessionStorage with the current active panel 16283 if ( !tabSettings.ignoreSession ) { 16284 try { 16285 sessionStorage.setItem( 16286 pagePath + $container.attr( "id" ) + activePanel, 16287 details.id 16288 ); 16289 } catch ( error ) { 16290 16291 /* swallow error */ 16292 } 16293 } 16294 16295 // Update the URL hash if needed 16296 if ( tabSettings.updateHash ) { 16297 updateHash( details ); 16298 } 16299 16300 // Identify that the tabbed interface accordion was updated 16301 // if the panel was not already open 16302 if ( !$details.attr( "open" ) ) { 16303 $container.trigger( updatedEvent, [ $details ] ); 16304 } 16305 } 16306} ); 16307 16308// Change the panel based upon an external link click 16309$document.on( "click", ".wb-tabs-ext", function( event ) { 16310 var which = event.which; 16311 16312 // Ignore middle and right mouse buttons 16313 if ( !which || which === 1 ) { 16314 event.preventDefault(); 16315 onSelect( event.currentTarget.getAttribute( "href" ).substring( 1 ) ); 16316 } 16317} ); 16318 16319// Add the timer poke to initialize the plugin
16320wb.add( selector ); 16321 16322} )( jQuery, window, wb ); 16323 16324/** 16325 * @title WET-BOEW Tag filter 16326 * @overview Filter based content tagging 16327 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 16328 * @author @duboisp 16329 */ 16330( function( $, window, document, wb ) { 16331"use strict"; 16332 16333let wait; 16334 16335const componentName = "wb-tagfilter", 16336 selector = ".provisional." + componentName, 16337 selectorCtrl = "." + componentName + "-ctrl", 16338 initEvent = "wb-init" + selector, 16339 $document = wb.doc, 16340 filterOutClass = "wb-fltr-out", 16341 tgFilterOutClass = "wb-tgfltr-out", 16342 itemsWrapperClass = "wb-tagfilter-items", 16343 noResultWrapperClass = "wb-tagfilter-noresult", 16344 16345 init = function( event ) { 16346 const elm = wb.init( event, componentName, selector ); 16347 16348 if ( elm ) { 16349 const filterControls = elm.querySelectorAll( selectorCtrl ), 16350 taggedItems = elm.querySelectorAll( "[data-wb-tags]" ), 16351 taggedItemsWrapper = elm.querySelector( "." + itemsWrapperClass ), 16352 noResultWrapper = elm.querySelector( "." + noResultWrapperClass ); 16353 16354 elm.items = []; 16355 elm.filters = {}; 16356 elm.activeFilters = []; 16357 16358 if ( taggedItemsWrapper ) { 16359 taggedItemsWrapper.id = taggedItemsWrapper.id || wb.getId(); // Ensure the element has an ID 16360 taggedItemsWrapper.setAttribute( "aria-live", "polite" ); 16361 } 16362 16363 // Handle filters 16364 if ( filterControls.length ) { 16365 elm.filters = buildFiltersObj( filterControls ); 16366 16367 filterControls.forEach( function( item ) { 16368 item.setAttribute( "aria-controls", taggedItemsWrapper.id ); 16369 } ); 16370 } 16371 16372 // Handle tagged items 16373 if ( taggedItems.length ) { 16374 elm.items = buildTaggedItemsArr( taggedItems ); 16375 } 16376 16377 // Add accessibility to no result element 16378 if ( noResultWrapper ) { 16379 noResultWrapper.setAttribute( "role", "status" ); 16380 } 16381 16382 // Update list of visible items (in case of predefined filters) 16383 update( elm ); 16384 16385 wb.ready( $( elm ), componentName ); 16386 } 16387 }, 16388 16389 // Add every tagged item to an array of objects with their DOM ID, list of associated tags, and default isMatched attribute 16390 buildTaggedItemsArr = function( taggedItems ) { 16391 let taggedItemsArr = []; 16392 16393 taggedItems.forEach( function( taggedItem ) { 16394 let tagsList = taggedItem.dataset.wbTags.split( " " ); 16395 16396 if ( !taggedItem.id ) { 16397 taggedItem.setAttribute( "id", wb.getId() ); 16398 } 16399 16400 taggedItemsArr.push( { 16401 id: taggedItem.id, 16402 tags: tagsList, 16403 isMatched: true, 16404 itemText: taggedItem.innerText.toLowerCase() 16405 } ); 16406 } ); 16407 16408 return taggedItemsArr; 16409 }, 16410 16411 // Build list of available filters using all filters grouped by filter name 16412 buildFiltersObj = function( filterControls ) { 16413 let filtersObj = {}; 16414 16415 filterControls.forEach( function( control ) { 16416 if ( !control.name ) { 16417 console.error( componentName + ": Filter controls require an attribute 'name'." ); 16418 } 16419 16420 switch ( control.type ) { 16421 case "checkbox": 16422 case "radio": 16423 if ( !( control.name in filtersObj ) ) { 16424 filtersObj[ control.name ] = [ ]; 16425 } 16426 16427 filtersObj[ control.name ].push( { 16428 isChecked: control.checked, 16429 type: control.type, 16430 value: control.value 16431 } ); 16432 16433 break; 16434 case "select-one": 16435 filtersObj[ control.name ] = [ { 16436 type: control.type, 16437 value: control.value 16438 } ]; 16439 break; 16440 } 16441 } ); 16442 16443 return filtersObj; 16444 }, 16445 16446 // Update array of active filters according to UI selected controls 16447 refineFilters = function( instance ) { 16448 instance.activeFilters = [ ]; // Clear active filters 16449 16450 for ( let filterGroupName in instance.filters ) { 16451 let filterGroup = instance.filters[ filterGroupName ], 16452 filterGroupChkCnt = filterGroup.filter( function( o ) { 16453 return o.isChecked === true; 16454 } ).length, 16455 filterGroupActiveFilters = [ ]; 16456 16457 switch ( filterGroup[ 0 ].type ) { 16458 case "checkbox": 16459 if ( filterGroupChkCnt > 0 ) { 16460 filterGroup.forEach( function( filterItem ) { 16461 if ( filterItem.isChecked ) { 16462 filterGroupActiveFilters.push( filterItem.value ); 16463 } 16464 } ); 16465 } 16466 break; 16467 16468 case "radio": 16469 if ( filterGroupChkCnt > 0 ) { 16470 for ( let filterItem of filterGroup ) { 16471 if ( filterItem.isChecked === true ) { 16472 if ( filterItem.value !== "" ) { 16473 filterGroupActiveFilters.push( filterItem.value ); 16474 } 16475 break; 16476 } 16477 } 16478 } else { 16479 console.warn( componentName + ": Radio button groups must have a default selected value. If you want to display all items, add an option called \"All\" with an empty value." ); 16480 } 16481 break; 16482
16483 case "select-one": 16484 if ( filterGroup[ 0 ].value !== "" ) { 16485 filterGroupActiveFilters.push( filterGroup[ 0 ].value ); 16486 } 16487 break; 16488 } 16489 16490 instance.activeFilters.push( filterGroupActiveFilters ); 16491 } 16492 }, 16493 16494 // Match tagged items to active filters and only return items that have an active filter in every filter group 16495 matchItemsToFilters = function( instance ) { 16496 let filtersGroups = instance.activeFilters.length; 16497 16498 instance.items.forEach( function( item ) { 16499 let matchCount = 0; 16500 16501 instance.activeFilters.forEach( function( filterGroup ) { 16502 if ( filterGroup.length === 0 ) { 16503 matchCount++; 16504 } else { 16505 let itemIncludesFilter = filterGroup.filter( function( f ) { 16506 return item.tags.includes( f ); 16507 } ).length; 16508 16509 if ( itemIncludesFilter ) { 16510 matchCount++; 16511 } 16512 } 16513 } ); 16514 16515 matchCount === filtersGroups ? item.isMatched = true : item.isMatched = false; 16516 } ); 16517 }, 16518 16519 // Update list of visible items according to their "isMatched" property 16520 updateDOMItems = function( instance ) { 16521 const updatedItemsList = instance.items.forEach( function( item ) { 16522 let domItem = instance.querySelector( "#" + item.id ), 16523 matched = item.isMatched; 16524 16525 if ( matched ) { 16526 if ( domItem.classList.contains( tgFilterOutClass ) ) { 16527 domItem.classList.remove( tgFilterOutClass ); 16528 } 16529 } else { 16530 if ( !domItem.classList.contains( tgFilterOutClass ) ) { 16531 domItem.classList.add( tgFilterOutClass ); 16532 } 16533 } 16534 } ); 16535 16536 return updatedItemsList; 16537 }, 16538 16539 // Utility method to update stored active filters, update stored items and update visibility of tagged items 16540 update = function( instance ) { 16541 refineFilters( instance ); 16542 matchItemsToFilters( instance ); 16543 updateDOMItems( instance ); 16544 16545 $( instance ).trigger( "wb-contentupdated", [ { source: componentName } ] ); 16546 }; 16547 16548// When a filter is updated 16549$document.on( "change", selectorCtrl, function( event ) { 16550 let control = event.currentTarget, 16551 filterType = control.type, 16552 filterName = control.name, 16553 filterValue = control.value, 16554 elm = control.closest( selector ), 16555 filterGroup = elm.filters[ filterName ]; 16556 16557 switch ( filterType ) { 16558 case "checkbox": 16559 16560 // Update virtual filter to the new state 16561 filterGroup.find( function( filter ) { 16562 return filter.value === filterValue; 16563 } ).isChecked = !!control.checked; 16564 break; 16565 16566 case "radio": 16567 16568 // Set all virtual radio items to unchecked 16569 filterGroup.forEach( function( filterItem ) { 16570 filterItem.isChecked = false; 16571 } ); 16572 16573 // Set selected radio button's associated virtual filter to checked 16574 filterGroup.find( function( filter ) { 16575 return filter.value === filterValue; 16576 } ).isChecked = true; 16577 break; 16578 16579 case "select-one": 16580 16581 // Update virtual filter to the new value 16582 filterGroup[ 0 ].value = filterValue; 16583 break; 16584 } 16585 16586 // Update list of visible items 16587 update( elm ); 16588} ); 16589 16590$document.on( "wb-contentupdated", selector, function( event, data ) { 16591 let that = this, 16592 supportsHas = window.getComputedStyle( document.documentElement ).getPropertyValue( "--supports-has" ); // Get "--supports-has" CSS property 16593 16594 // Reinitialize tagfilter if content on the page has been updated by another plugin 16595 if ( data && data.source !== componentName ) { 16596 if ( wait ) { 16597 clearTimeout( wait ); 16598 } 16599 16600 wait = setTimeout( function() { 16601 that.classList.remove( "wb-init", componentName + "-inited" ); 16602 $( that ).trigger( "wb-init." + componentName ); 16603 }, 100 ); 16604 } 16605 16606 // Show no result message if on Firefox -- Remove once Firefox supports ":has()" 16607 if ( supportsHas === "false" ) { 16608 let noResultItem = this.querySelector( "." + noResultWrapperClass ); 16609 16610 if ( noResultItem && this.items.length > 0 ) { 16611 let visibleItems = this.querySelectorAll( "." + itemsWrapperClass + " " + "[data-wb-tags]:not(." + tgFilterOutClass + ", ." + filterOutClass + ")" ); 16612 16613 if ( visibleItems.length < 1 ) { 16614 noResultItem.style.display = "block"; 16615 } else { 16616 noResultItem.style.display = "none"; 16617 } 16618 } 16619 } 16620} ); 16621 16622$document.on( "timerpoke.wb " + initEvent, selector, init ); 16623
16624wb.add( selector ); 16625 16626} )( jQuery, window, document, wb ); 16627 16628/** 16629 * @title WET-BOEW Text highlighting 16630 * @overview Automatically highlights certain words on a Web page. The highlighted words can be selected via the query string. 16631 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 16632 * @author @pjackson28 16633 */ 16634( function( $, window, document, wb ) { 16635"use strict"; 16636 16637/* 16638 * Variable and function definitions. 16639 * These are global to the plugin - meaning that they will be initialized once per page, 16640 * not once per instance of plugin on the page. So, this is a good place to define 16641 * variables that are common to all instances of the plugin on a page. 16642 */ 16643var componentName = "wb-txthl", 16644 selector = "." + componentName, 16645 initEvent = "wb-init" + selector, 16646 $document = wb.doc, 16647 16648 /** 16649 * @method init 16650 * @param {jQuery Event} event Event that triggered the function call 16651 */ 16652 init = function( event ) { 16653 16654 // Start initialization 16655 // returns DOM object = proceed with init 16656 // returns undefined = do not proceed with init (e.g., already initialized) 16657 var elm = wb.init( event, componentName, selector ), 16658 params = wb.pageUrlParts.params, 16659 searchCriteria, newText; 16660 16661 if ( elm ) { 16662 if ( event.txthl ) { 16663 searchCriteria = Array.isArray( event.txthl ) ? event.txthl.join( "|" ) : event.txthl; 16664 } else if ( params && params.txthl ) { 16665 searchCriteria = decodeURIComponent( 16666 wb.pageUrlParts.params.txthl 16667 .replace( /^\s+|\s+$|\|+|"|\(|\)/g, "" ).replace( /\++/g, "|" ) 16668 ); 16669 } 16670 16671 if ( searchCriteria ) { 16672 16673 // Strip html 16674 var map = { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }; 16675 searchCriteria = searchCriteria.replace( /[&<>"']/g, function( m ) { 16676 return map[ m ]; 16677 } ); 16678 16679 // Make sure that we're not checking for text within a tag; only the text outside of tags. 16680 searchCriteria = "(?=([^>]*<))([\\s'])?(" + searchCriteria + ")(?!>)"; 16681 16682 newText = elm.innerHTML.replace( new RegExp( searchCriteria, "gi" ), function( match, group1, group2, group3 ) { 16683 return ( !group2 ? "" : group2 ) + "<mark>" + group3 + "</mark>"; 16684 } ); 16685 elm.innerHTML = newText; 16686 } 16687 16688 // Identify that initialization has completed 16689 wb.ready( $( elm ), componentName ); 16690 } 16691 }; 16692 16693// Bind the init event of the plugin 16694$document.on( "timerpoke.wb " + initEvent, selector, init ); 16695 16696// Add the timer poke to initialize the plugin 16697wb.add( selector ); 16698 16699} )( jQuery, window, document, wb ); 16700 16701/** 16702 * @title WET-BOEW Toggle 16703 * @overview Plugin that allows a link to toggle elements between on and off states. 16704 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 16705 * @author @patheard 16706 */ 16707( function( $, window, wb ) { 16708"use strict"; 16709 16710/* 16711 * Variable and function definitions. 16712 * These are global to the plugin - meaning that they will be initialized once per page, 16713 * not once per instance of plugin on the page. So, this is a good place to define 16714 * variables that are common to all instances of the plugin on a page. 16715 */ 16716var componentName = "wb-toggle", 16717 selector = "." + componentName, 16718 selectorPanel = ".tgl-panel", 16719 selectorTab = ".tgl-tab", 16720 initEvent = "wb-init" + selector, 16721 toggleEvent = "toggle" + selector, 16722 toggledEvent = "toggled" + selector, 16723 setFocusEvent = "setfocus.wb", 16724 states = {}, 16725 $document = wb.doc, 16726 $window = wb.win, 16727 16728 defaults = { 16729 stateOn: "on", 16730 stateOff: "off" 16731 }, 16732 16733 /** 16734 * @method init 16735 * @param {jQuery Event} event Event that triggered the function call 16736 */ 16737 init = function( event ) { 16738 16739 // Start initialization 16740 // returns DOM object = proceed with init 16741 // returns undefined = do not proceed with init (e.g., already initialized) 16742 var link = wb.init( event, componentName, selector, true ), 16743 $link, data, persistState; 16744 16745 if ( link ) { 16746 16747 // Merge the elements settings with the defaults 16748 $link = $( link ); 16749 data = $.extend( {}, defaults, $link.data( "toggle" ) ); 16750 $link.data( "toggle", data ); 16751 16752 // Add aria attributes of the toggle element 16753 initAria( link, data ); 16754 16755 // Persist toggle state across page loads 16756 if ( data.persist ) { 16757 persistState = initPersist( $link, data ); 16758 } 16759 16760 // Toggle behaviour when the page is printed 16761 if ( data.print ) { 16762 initPrint( $link, data ); 16763 } 16764 16765 // Set the initial state if the state has been specified and 16766 // the persistent state has not been set 16767 if ( !persistState && data.state ) { 16768 setState( $link, data, data.state ); 16769 } 16770 16771 // Identify that initialization has completed 16772 wb.ready( $link, componentName ); 16773 } 16774 }, 16775 16776 /** 16777 * Initialize the aria attributes for a given toggle element 16778 * @param {DOM element} link The toggle element to initialize 16779 * @param {Object} data Simple key/value data object passed when the event was triggered 16780 */ 16781 initAria = function( link, data ) { 16782 var i, len, elm, elms, parent, tabs, tab, panel, isOpen, wrapper,
16783 ariaControls = "", 16784 hasOpen = false; 16785 16786 // Group toggle elements with a parent are assumed to be a tablist 16787 if ( data.group != null && data.parent != null ) { //eslint-disable-line no-eq-null 16788 parent = document.querySelector( data.parent ); 16789 16790 // Check that the tablist widget hasn't already been initialized 16791 if ( parent.getAttribute( "role" ) !== "tablist" ) { 16792 16793 // Only apply the tablist role if the parent is not the tabbed interface container 16794 // or the page is currently in "smallview", "xsmallview" or "xxsmallview" 16795 if ( parent.className.indexOf( "wb-tabs" ) === -1 || 16796 document.documentElement.className.indexOf( "smallview" ) !== -1 ) { 16797 parent.setAttribute( "role", "tablist" ); 16798 } 16799 16800 elms = parent.querySelectorAll( data.group ); 16801 tabs = parent.querySelectorAll( data.group + " " + selectorTab ); 16802 16803 // Set the tab and panel aria attributes 16804 for ( i = 0, len = elms.length; i !== len; i += 1 ) { 16805 elm = elms[ i ]; 16806 tab = tabs[ i ]; 16807 panel = elm.querySelector( selectorPanel ); 16808 16809 // Check if the element is toggled on based on the 16810 // open attribute or "on" CSS class 16811 isOpen = elm.nodeName.toLowerCase() === "details" ? 16812 !!elm.hasAttribute( "open" ) : 16813 ( " " + tab.className + " " ).indexOf( " " + data.stateOn + " " ); 16814 if ( isOpen ) { 16815 hasOpen = true; 16816 } 16817 16818 if ( !tab.getAttribute( "id" ) ) { 16819 tab.setAttribute( "id", wb.getId() ); 16820 } 16821 16822 //Details and summary don't support aria roles and some aria attribute that is why they are wrapped in a div 16823 if ( elm.nodeName.toLowerCase() === "details" && elm.parentNode.className.toLowerCase().indexOf( "accordion" ) > -1 ) { 16824 wrapper = document.createElement( "div" ); 16825 wrapper.classList.add( "tgl-tab" ); 16826 wrapper.setAttribute( "role", "tab" ); 16827 wrapper.setAttribute( "aria-selected", isOpen ); 16828 wrapper.setAttribute( "tabindex", isOpen ? "0" : "-1" ); 16829 wrapper.setAttribute( "aria-posinset", i + 1 ); 16830 wrapper.setAttribute( "aria-setsize", len ); 16831 parent.replaceChild( wrapper, elm ); 16832 wrapper.appendChild( elm ); 16833 } else { 16834 tab.setAttribute( "role", "tab" ); 16835 tab.setAttribute( "aria-selected", isOpen ); 16836 tab.setAttribute( "tabindex", isOpen ? "0" : "-1" ); 16837 tab.setAttribute( "aria-posinset", i + 1 ); 16838 tab.setAttribute( "aria-setsize", len ); 16839 } 16840 panel.setAttribute( "role", "tabpanel" ); 16841 panel.setAttribute( "aria-labelledby", tab.getAttribute( "id" ) ); 16842 panel.setAttribute( "aria-expanded", isOpen ); 16843 panel.setAttribute( "aria-hidden", !isOpen ); 16844 } 16845 16846 // No open panels so put the first summary in the tab order 16847 if ( !hasOpen ) { 16848 tabs[ 0 ].setAttribute( "tabindex", "0" ); 16849 } 16850 } 16851 16852 // Set the elements this link controls 16853 } else { 16854 elms = getElements( link, data ); 16855 for ( i = 0, len = elms.length; i !== len; i += 1 ) { 16856 elm = elms[ i ]; 16857 if ( !elm.id ) { 16858 elm.id = wb.getId(); 16859 } 16860 ariaControls += elm.id + " "; 16861 } 16862 link.setAttribute( "aria-controls", ariaControls.slice( 0, -1 ) ); 16863 } 16864 }, 16865 16866 /** 16867 * Initializes persistent behaviour of the toggle element 16868 * @param {jQuery Object} $link The toggle element to initialize 16869 * @param {Object} data Simple key/value data object passed when the event was triggered 16870 * @returns {string} Persistent state 16871 */ 16872 initPersist = function( $link, data ) { 16873 var state, 16874 link = $link[ 0 ]; 16875 16876 // Store the persistence type and key for later use 16877 data.persist = data.persist === "session" ? sessionStorage : localStorage; 16878 data.persistKey = componentName + ( data.group ? data.group : "" ) + link.id; 16879 16880 // If there's a saved toggle state, trigger the change to that state 16881 state = data.persist.getItem( data.persistKey ); 16882 if ( state ) { 16883 $link.trigger( toggleEvent, $.extend( {}, data, { type: state } ) ); 16884 } 16885 16886 return state; 16887 }, 16888 16889 /** 16890 * Initialize open on print behaviour of the toggle element 16891 * @param {jQuery Object} $link The toggle element to initialize 16892 * @param {Object} data Simple key/value data object passed when the event was triggered 16893 */ 16894 initPrint = function( $link, data ) { 16895 var mediaQuery, 16896 printEvent = "beforeprint"; 16897 16898 $window.on( printEvent, function() { 16899 $link.trigger( toggleEvent, $.extend( {}, data, { type: data.print } ) ); 16900 } ); 16901 16902 // Fallback for browsers that don't support print events 16903 if ( window.matchMedia ) { 16904 mediaQuery = window.matchMedia( "print" ); 16905 if ( mediaQuery.addListener ) { 16906 mediaQuery.addListener( function( query ) { 16907 if ( query.matches ) { 16908 $window.trigger( printEvent ); 16909 } 16910 } ); 16911 } 16912 } 16913 }, 16914 16915 /** 16916 * Click handler for the toggle links 16917 * @param {jQuery Event} event The event that triggered this invocation 16918 */ 16919 click = function( event ) { 16920 var $link = $( event.currentTarget ); 16921 16922 $link.trigger( toggleEvent, $link.data( "toggle" ) ); 16923 event.preventDefault(); 16924 16925 // Assign focus to eventTarget 16926 $link.trigger( setFocusEvent ); 16927 }, 16928 16929 /** 16930 * Toggles the elements a link controls between the on and off states. 16931 * @param {jQuery Event} event The event that triggered this invocation 16932 * @param {Object} data Simple key/value data object passed when the event was triggered 16933 */ 16934 toggle = function( event, data ) { 16935 if ( event.namespace === componentName ) { 16936 var dataGroup, key, $elmsGroup, 16937 isGroup = !!data.group, 16938 isPersist = !!data.persist, 16939 isTablist = isGroup && !!data.parent, 16940 link = event.currentTarget, 16941 $link = $( link ), 16942 stateFrom = getState( $link, data ), 16943 isToggleOn = stateFrom === data.stateOff, 16944 stateTo = isToggleOn ? data.stateOn : data.stateOff, 16945 $elms = isTablist ? $link.parent( data.group ) : getElements( link, data ); 16946 16947 // Group toggle behaviour: only one element in the group open at a time. 16948 if ( isGroup ) { 16949 16950 // Get the grouped elements using data.group as the CSS selector 16951 dataGroup = $.extend( {}, data, { selector: data.group } ); 16952 $elmsGroup = getElements( link, dataGroup ); 16953 16954 // Set the toggle state to "off". For tab lists, this is stored on the tab element 16955 setState( isTablist ? $( data.parent ).find( selectorTab ) : $elmsGroup, 16956 dataGroup, data.stateOff ); 16957 16958 // Toggle all grouped elements to "off" 16959 $elmsGroup.wb( "toggle", data.stateOff, data.stateOn ); 16960 $elmsGroup.trigger( toggledEvent, { 16961 isOn: false, 16962 isTablist: isTablist, 16963 elms: $elmsGroup 16964 } ); 16965 16966 // Remove all grouped persistence keys 16967 if ( isPersist ) { 16968 for ( key in data.persist ) { 16969 if ( key.indexOf( componentName + data.group ) === 0 ) { 16970 data.persist.removeItem( key ); 16971 } 16972 } 16973 } 16974 } 16975 16976 // Set the toggle state. For tab lists, this is set on the tab element 16977 setState( isTablist ? $link : $elms, data, stateTo ); 16978 16979 // Toggle all elements to the requested state 16980 $elms.wb( "toggle", stateTo, stateFrom ); 16981 $elms.trigger( toggledEvent, { 16982 isOn: isToggleOn, 16983 isTablist: isTablist, 16984 elms: $elms 16985 } ); 16986 16987 // Ensure that last focused element in the accordion remains keyboard focusable 16988 // whether it is collapsed or not 16989 if ( isGroup ) { 16990 $elms.find( "summary" ).attr( { "tabindex": "0" } ); 16991 } 16992 16993 // Store the toggle link's current state if persistence is turned on. 16994 // Try/catch is required to address exceptions thrown when using BB10 or 16995 // private browsing in iOS. 16996 if ( isPersist ) { 16997 try { 16998 data.persist.setItem( data.persistKey, stateTo ); 16999 } catch ( error ) { 17000 17001 /* swallow error */ 17002 } 17003 } 17004 } 17005 }, 17006 17007 /** 17008 * Sets the required property and attribute for toggling open/closed a details element 17009 * @param {jQuery Event} event The event that triggered this invocation 17010 * @param {Object} data Simple key/value data object passed when the event was triggered 17011 */ 17012 toggleDetails = function( event, data ) { 17013 if ( event.namespace === componentName && event.target === event.currentTarget ) { 17014 var top, 17015 isOn = data.isOn, 17016 $elms = data.elms, 17017 $this = $( this ), 17018 $detail = $this.is( "summary" ) ? $this.parent() : $this; 17019 17020 // Stop propagation of the toggleDetails event 17021 if ( event.stopPropagation ) { 17022 event.stopImmediatePropagation(); 17023 } else { 17024 event.cancelBubble = true; 17025 } 17026 17027 $detail.prop( "open", isOn ); 17028 17029 if ( data.isTablist ) { 17030 17031 // Set the required aria attributes 17032 $elms.find( selectorTab ).parents( selectorTab ).attr( { 17033 "aria-selected": isOn, 17034 tabindex: isOn ? "0" : "-1" 17035 } ); 17036 $elms.find( selectorPanel ).attr( { 17037 "aria-hidden": !isOn, 17038 "aria-expanded": isOn 17039 } ); 17040 17041 // Check that the top of the open element is in view. 17042 if ( isOn && $elms.length === 1 ) { 17043 top = $elms.offset().top; 17044 if ( top < $window.scrollTop() ) { 17045 $window.scrollTop( top ); 17046 } 17047 } 17048 } 17049 } 17050 }, 17051 17052 /** 17053 * Returns the elements a given toggle element controls. 17054 * @param {DOM element} link Toggle element that was clicked 17055 * @param {Object} data Simple key/value data object passed when the event was triggered 17056 * @returns {jQuery Object} DOM elements the toggle link controls 17057 */ 17058 getElements = function( link, data ) { 17059 var selector = data.selector || link, 17060 parent = data.parent || null; 17061 17062 return parent !== null ? $( parent ).find( selector ) : $( selector ); 17063 }, 17064 17065 /** 17066 * Gets the current toggle state of elements controlled by the given link. 17067 * @param {jQuery Object} $link Toggle link that was clicked 17068 * @param {Object} data Simple key/value data object passed when the event was triggered 17069 */ 17070 getState = function( $link, data ) { 17071 var parent = data.parent, 17072 selector = data.selector, 17073 type = data.type; 17074 17075 // Get opposite state of the type. Toggle reverses this 17076 // to the requested state. 17077 if ( type ) { 17078 return type === "on" ? data.stateOff : data.stateOn; 17079 17080 // <details> elements use the open attribute to determine state 17081 } else if ( $link[ 0 ].nodeName.toLowerCase() === "summary" ) { 17082 return $link.parent().attr( "open" ) ? data.stateOn : data.stateOff; 17083 17084 // When no selector, use the data attribute of the link 17085 } else if ( !selector ) { 17086 return $link.data( componentName + "-state" ) || data.stateOff; 17087 17088 // When toggling multiple <details> elements, state is "off" if any are collapsed 17089 } else if ( selector === "details" && !type ) { 17090 var anyCollapsed = false;
17091 getElements( $link, data ).each( function() { 17092 if ( !$( this ).attr( "open" ) ) { 17093 anyCollapsed = true; 17094 } 17095 } ); 17096 return anyCollapsed ? data.stateOff : data.stateOn; 17097 17098 // Get the current on/off state of the elements specified by the selector and parent 17099 } else if ( Object.prototype.hasOwnProperty.call( states, selector ) ) { 17100 return Object.prototype.hasOwnProperty.call( states[ selector ], parent ) ? 17101 states[ selector ][ parent ] : 17102 states[ selector ].all; 17103 } 17104 return data.stateOff; 17105 }, 17106 17107 /* 17108 * Sets the current toggle state of elements controlled by the given link. 17109 * @param {DOM element} link Toggle link that was clicked 17110 * @param {Object} data Simple key/value data object passed when the event was triggered 17111 * @param {String} state The current state of the elements: "on" or "off" 17112 */ 17113 setState = function( $elms, data, state ) { 17114 var prop, 17115 parent = data.parent, 17116 selector = data.selector, 17117 elmsState = states[ selector ]; 17118 17119 if ( selector ) { 17120 17121 // Check the selector object has been created 17122 if ( !elmsState ) { 17123 elmsState = { all: data.stateOff }; 17124 states[ selector ] = elmsState; 17125 } 17126 17127 // If there's a parent, set its state 17128 if ( parent ) { 17129 elmsState[ parent ] = state; 17130 17131 // No parent means set all states for the given selector. This is 17132 // because toggle links that apply to the entire DOM also affect 17133 // links that are restricted by parent. 17134 } else { 17135 for ( prop in elmsState ) { 17136 if ( Object.prototype.hasOwnProperty.call( elmsState, prop ) ) { 17137 elmsState[ prop ] = state; 17138 } 17139 } 17140 } 17141 } 17142 17143 // Store the state on the elements as well. This allows a link to toggle itself. 17144 $elms.data( componentName + "-state", state ); 17145 }; 17146 17147// Bind the plugin's events 17148$document.on( "timerpoke.wb " + initEvent + " " + toggleEvent + 17149" click", selector, function( event, data ) { 17150 17151 var eventType = event.type; 17152 17153 switch ( eventType ) { 17154 case "click": 17155 click( event ); 17156 break; 17157 17158 case "toggle": 17159 toggle( event, data ); 17160 break; 17161 17162 case "timerpoke": 17163 case "wb-init": 17164 init( event ); 17165 break; 17166 } 17167} ); 17168 17169$document.on( toggledEvent, "summary, details", toggleDetails ); 17170 17171// Keyboard handling for the accordion 17172$document.on( "keydown", selectorTab, function( event ) { 17173 var which = event.which, 17174 data, $elm, $parent, $group, $newPanel, index; 17175 17176 if ( !event.ctrlKey && which > 34 && which < 41 ) { 17177 event.preventDefault(); 17178 $elm = $( event.currentTarget ); 17179 data = $elm.data( "toggle" ); 17180 $parent = $document.find( data.parent ); 17181 $group = $parent.find( data.group ); 17182 index = $group.index( $elm.parent() ); 17183 17184 switch ( which ) { 17185 17186 // End 17187 case 35: 17188 $newPanel = $group.last(); 17189 break; 17190 17191 // Home 17192 case 36: 17193 $newPanel = $group.first(); 17194 break; 17195 17196 // Left / up arrow 17197 case 37: 17198 case 38: 17199 if ( index === 0 ) { 17200 $newPanel = $group.last(); 17201 } else { 17202 $newPanel = $group.eq( index - 1 ); 17203 } 17204 break; 17205 17206 // Right / down arrow 17207 case 39: 17208 case 40: 17209 if ( index === $group.length - 1 ) { 17210 $newPanel = $group.first(); 17211 } else { 17212 $newPanel = $group.eq( index + 1 ); 17213 } 17214 break; 17215 } 17216 17217 $newPanel 17218 .children( "summary" ) 17219 .trigger( setFocusEvent ); 17220 } 17221} ); 17222 17223$document.on( "keydown", selectorPanel, function( event ) { 17224 17225 // Ctrl + Up arrow 17226 if ( event.ctrlKey && event.which === 38 ) { 17227 17228 // Move focus to the summary element 17229 $( event.currentTarget ) 17230 .prev() 17231 .trigger( setFocusEvent ); 17232 } 17233} ); 17234 17235// Add the timer poke to initialize the plugin 17236wb.add( selector ); 17237 17238} )( jQuery, window, wb ); 17239 17240/** 17241 * @title WET-BOEW Twitter embedded timeline 17242 * @overview Helps with implementing Twitter embedded timelines. 17243 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 17244 * @author @pjackson28 17245 */ 17246( function( $, window, wb ) { 17247"use strict"; 17248 17249/* 17250 * Variable and function definitions. 17251 * These are global to the plugin - meaning that they will be initialized once per page, 17252 * not once per instance of plugin on the page. So, this is a good place to define 17253 * variables that are common to all instances of the plugin on a page. 17254 */
17255var componentName = "wb-twitter", 17256 selector = "." + componentName, 17257 initEvent = "wb-init" + selector, 17258 $document = wb.doc, 17259 i18n, i18nText, 17260 17261 /** 17262 * @method init 17263 * @param {jQuery Event} event Event that triggered the function call 17264 */ 17265 init = function( event ) { 17266 17267 // Start initialization 17268 // returns DOM object = proceed with init 17269 // returns undefined = do not proceed with init (e.g., already initialized) 17270 var eventTarget = wb.init( event, componentName, selector ), 17271 protocol = wb.pageUrlParts.protocol; 17272 17273 if ( eventTarget ) { 17274 const twitterLink = eventTarget.querySelector( "a.twitter-timeline" ); 17275 17276 // Ignore IE11 17277 // Note: Twitter's widget no longer supports it... 17278 if ( wb.ie11 ) { 17279 wb.ready( $( eventTarget ), componentName ); 17280 return; 17281 } 17282 17283 // Process the Twitter link 17284 if ( twitterLink ) { 17285 const loadingDiv = document.createElement( "div" ); 17286 let observer; 17287 17288 // Only initialize the i18nText once 17289 if ( !i18nText ) { 17290 i18n = wb.i18n; 17291 i18nText = { 17292 startNotice: i18n( "twitter-start-notice" ), 17293 endNotice: i18n( "twitter-end-notice" ), 17294 skipEnd: i18n( "twitter-skip-end" ), 17295 skipStart: i18n( "twitter-skip-start" ), 17296 timelineTitle: i18n( "twitter-timeline-title" ) 17297 }; 17298 } 17299 17300 // Show a warning if the timelineTitle variable isn't a string 17301 if ( typeof i18nText.startNotice !== "string" ) { 17302 console.warn( componentName + ": i18n text is missing. Iframe title override and skip links will be disabled." ); 17303 } 17304 17305 // Set Chinese (Simplified)'s language code to "zh-cn" 17306 // If the link doesn't specify a widget language and its "in-page" language code is "zh-Hans"... 17307 // Notes: 17308 // -WET uses "zh-Hans", Twitter uses "zh-ch" and falls back to English if the former is used 17309 // -Language code sourced from https://developer.twitter.com/en/docs/twitter-for-websites/supported-languages 17310 if ( !twitterLink.dataset.lang && twitterLink.closest( "[lang='zh-Hans']" ) ) { 17311 twitterLink.dataset.lang = "zh-cn"; 17312 } 17313 17314 // Match the Facebook page plugin's default height 17315 // If data-height is set to "fb-page" OR the widget has a tweet limit and lacks a custom height... 17316 // Notes: 17317 // -Counteracts enormous default widget heights that can reach tens of thousands of pixels without a scrollbar 17318 // -Timeline widgets stopped honouring tweet limits on July 21, 2023 and began showing up to 100 tweets at a time ("verified" accounts only) 17319 // -Facebook page plugin's default height is documented in https://developers.facebook.com/docs/plugins/page-plugin#settings 17320 if ( twitterLink.dataset.height === "fb-page" || ( twitterLink.dataset.tweetLimit && !twitterLink.dataset.height ) ) { 17321 twitterLink.dataset.height = "500"; 17322 } 17323 17324 // Add a "do not track" parameter (i.e. data-dnt="true" attribute) unless it's already been set 17325 // Note: Covered in https://developer.twitter.com/en/docs/twitter-for-websites/webpage-properties 17326 if ( !twitterLink.dataset.dnt ) { 17327 twitterLink.dataset.dnt = "true"; 17328 } 17329 17330 // Add a loading icon below the link 17331 loadingDiv.className = "twitter-timeline-loading"; 17332 twitterLink.after( loadingDiv ); 17333 17334 // Observe DOM mutations 17335 observer = new MutationObserver( function( mutations ) { 17336 mutations.forEach( function( mutation ) { 17337 switch ( mutation.type ) { 17338 17339 // Check for attribute changes 17340 case "attributes": { 17341 const mutationTarget = mutation.target; 17342 17343 // Override the timeline iframe's title right after Twitter's widget script adds it 17344 // Notes: 17345 // -The timeline's iframe title is English-only, uses "Twitter" and is written in title case ("Twitter Timeline")... This replaces it with an i18n version that uses "X" and is written in sentence case. 17346 // -Only proceed if the i18n variable is a string... otherwise this'll trigger an infinite loop of attribute mutations 17347 if ( mutationTarget.nodeName === "IFRAME" && mutationTarget.title !== i18nText.timelineTitle && typeof i18nText.timelineTitle === "string" ) { 17348 mutationTarget.title = i18nText.timelineTitle; 17349 } 17350 break; 17351 } 17352 17353 // Check for node removals 17354 case "childList": {
17355 mutation.removedNodes.forEach( function( removedNode ) { 17356 17357 // If the removed node was a Twitter link, remove its adjacent loading icon, add skip links and stop observing 17358 // Note: Twitter's widget script removes "a.twitter-timeline" upon displaying the timeline iframe's content... at which point the loading icon is no longer useful 17359 if ( removedNode === twitterLink && mutation.nextSibling === loadingDiv ) { 17360 const iframeContainer = loadingDiv.previousElementSibling; 17361 17362 loadingDiv.remove(); 17363 addSkipLinks( iframeContainer ); 17364 17365 // The following 2 lines were added as a workaround in Safari where the iFrame is not displayed 17366 eventTarget.style.opacity = 1; 17367 eventTarget.style.opacity = ""; 17368 17369 observer.disconnect(); 17370 } 17371 } ); 17372 } 17373 } 17374 } ); 17375 } ); 17376 17377 // Observe changes to the plugin container's child elements and title attributes 17378 observer.observe( eventTarget, { 17379 attributeFilter: [ "title" ], 17380 childList: true, 17381 subtree: true 17382 } ); 17383 } 17384 17385 Modernizr.load( { 17386 load: ( protocol.indexOf( "http" ) === -1 ? "http:" : protocol ) + "//platform.twitter.com/widgets.js", 17387 complete: function() { 17388 17389 // Identify that initialization has completed 17390 wb.ready( $( eventTarget ), componentName ); 17391 } 17392 } ); 17393 } 17394 }, 17395 17396 // Add skip links immediately before and after the timeline widget 17397 // Note: Verified account timelines may contain several hundred interactive elements... this provides a mechanism to spare keyboard-only users from needing to tab through everything to move past the widget. 17398 addSkipLinks = function( iframeContainer ) { 17399 const timelineIframe = iframeContainer.getElementsByTagName( "iframe" )[ 0 ]; 17400 const username = getTwitterUsername( timelineIframe.src ); 17401 const noticeClass = componentName + "-" + "notice"; 17402 const skipClass = componentName + "-" + "skip"; 17403 const startText = "start"; 17404 const endText = "end"; 17405 let startNotice; 17406 let endNotice; 17407 let skipToEndLink; 17408 let skipToStartLink; 17409 17410 // Abort if Twitter username is falsy or i18n variables aren't strings 17411 if ( !username || typeof i18nText.timelineTitle !== "string" ) { 17412 return; 17413 } 17414 17415 // Add a start of timeline notice 17416 startNotice = createNotice( i18nText.startNotice, username, timelineIframe.id, noticeClass, startText ); 17417 iframeContainer.before( startNotice ); 17418 17419 // Add an end of timeline notice 17420 endNotice = createNotice( i18nText.endNotice, username, timelineIframe.id, noticeClass, endText ); 17421 iframeContainer.after( endNotice ); 17422 17423 // Add a skip to end link 17424 skipToEndLink = createSkipLink( i18nText.skipEnd, username, endNotice.id, skipClass, endText ); 17425 startNotice.after( skipToEndLink ); 17426 17427 // Add a skip to start link 17428 skipToStartLink = createSkipLink( i18nText.skipStart, username, startNotice.id, skipClass, startText ); 17429 endNotice.before( skipToStartLink ); 17430 }, 17431 17432 // Extract a Twitter username from the iframe's timeline URL 17433 getTwitterUsername = function( iframeSrc ) { 17434 let username = iframeSrc.match( /\/screen-name\/([^?]+)/ ); 17435 username = username ? username[ 1 ] : null; 17436 17437 return username; 17438 }, 17439 17440 // Create a timeline notice 17441 createNotice = function( textTemplate, username, iframeId, noticeClass, position ) { 17442 const spanElm = document.createElement( "span" ); 17443 const pElm = document.createElement( "p" ); 17444 17445 spanElm.innerHTML = textTemplate.replace( "%username%", username ); 17446 17447 pElm.id = iframeId + "-" + position; 17448 pElm.className = noticeClass + "-" + position; 17449 pElm.prepend( spanElm ); 17450 17451 // Hide the notice upon losing focus 17452 // Removes its tabindex attribute to make its CSS hide it from screen readers 17453 pElm.addEventListener( "blur", function( e ) { 17454 e.target.removeAttribute( "tabindex" ); 17455 } ); 17456 17457 return pElm; 17458 }, 17459 17460 // Create a skip link 17461 createSkipLink = function( textTemplate, username, linkDestId, skipClass, linkDir ) { 17462 const spanElm = document.createElement( "span" ); 17463 const aElm = document.createElement( "a" ); 17464 const pElm = document.createElement( "p" ); 17465 17466 spanElm.innerHTML = textTemplate.replace( "%username%", username ); 17467 17468 aElm.href = "#" + linkDestId;
17469 aElm.prepend( spanElm ); 17470 17471 // Focus onto the destination of a clicked link 17472 $( aElm ).on( "click", function( event ) { 17473 const currentTarget = event.currentTarget; 17474 const linkDestId = "#" + wb.jqEscape( currentTarget.getAttribute( "href" ).substring( 1 ) ); 17475 const $linkDest = $document.find( linkDestId ); 17476 17477 // Assign focus to the link's destination 17478 // Note: The focus event's scrolling behaviour is more graceful than "jumping" to an anchor link's destination 17479 $linkDest.trigger( "setfocus.wb" ); 17480 17481 // Don't engage normal link navigation behaviour (i.e. "jumping" to the link destination, changing address/navigation history) 17482 return false; 17483 } ); 17484 17485 pElm.className = skipClass + " " + skipClass + "-" + linkDir; 17486 pElm.prepend( aElm ); 17487 17488 return pElm; 17489 }; 17490 17491$document.on( "timerpoke.wb " + initEvent, selector, init ); 17492 17493// Add the timer poke to initialize the plugin 17494wb.add( selector ); 17495 17496} )( jQuery, window, wb ); 17497 17498/** 17499 * @title WET-BOEW Data Json [data-json-after], [data-json-append], 17500 * [data-json-before], [data-json-prepend], [data-json-replace], [data-json-replacewith] and [data-wb-json] 17501 * @overview Insert content extracted from JSON file. 17502 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 17503 * @author @duboisp 17504 */ 17505/*global jsonpointer */ 17506( function( $, window, wb ) { 17507"use strict"; 17508 17509/* 17510 * Variable and function definitions. 17511 * These are global to the plugin - meaning that they will be initialized once per page, 17512 * not once per instance of plugin on the page. So, this is a good place to define 17513 * variables that are common to all instances of the plugin on a page. 17514 */ 17515var componentName = "wb-data-json", 17516 shortName = "wb-json", 17517 selectors = [ 17518 "[data-json-after]", 17519 "[data-json-append]", 17520 "[data-json-before]", 17521 "[data-json-prepend]", 17522 "[data-json-replace]", 17523 "[data-json-replacewith]", 17524 "[data-" + shortName + "]" 17525 ], 17526 allowJsonTypes = [ "after", "append", "before", "prepend", "val" ], 17527 allowAttrNames = /(href|src|data-*|aria-*|role|pattern|min|max|step|low|high|lang|hreflang|action)/, 17528 allowPropNames = /(checked|selected|disabled|required|readonly|multiple|hidden)/, 17529 selectorsLength = selectors.length, 17530 selector = selectors.join( "," ), 17531 initEvent = "wb-init." + componentName, 17532 updateEvent = "wb-update." + componentName, 17533 contentUpdatedEvent = "wb-contentupdated", 17534 dataQueue = componentName + "-queue", 17535 $document = wb.doc, 17536 isExtensionRegistered, 17537 s, 17538 17539 /** 17540 * @method init 17541 * @param {jQuery Event} event Event that triggered this handler 17542 * @param {string} ajaxType The type of JSON operation, either after, append, before or replace 17543 */ 17544 init = function( event ) { 17545 17546 // Start initialization 17547 // returns DOM object = proceed with init 17548 // returns undefined = do not proceed with init (e.g., already initialized) 17549 var elm = wb.init( event, componentName, selector ), 17550 jsSettings = window[ componentName ] || { }, 17551 prop, 17552 $elm; 17553 17554 if ( elm ) { 17555 17556 var jsonCoreTypes = [ 17557 "before", 17558 "replace", 17559 "replacewith", 17560 "after", 17561 "append", 17562 "prepend" 17563 ], 17564 jsonType, jsondata, 17565 i, i_len = jsonCoreTypes.length, i_cache, 17566 lstCall = [], 17567 url; 17568 17569 $elm = $( elm ); 17570 17571 for ( i = 0; i !== i_len; i += 1 ) { 17572 jsonType = jsonCoreTypes[ i ]; 17573 url = elm.getAttribute( "data-json-" + jsonType ); 17574 if ( url !== null ) { 17575 lstCall.push( { 17576 type: jsonType, 17577 url: url 17578 } ); 17579 } 17580 } 17581 17582 // Extend but not overwrite the functionTest and the functionForOperand if some was added 17583 if ( !isExtensionRegistered ) { 17584 if ( jsSettings.functionForTest ) { 17585 for ( prop in jsSettings.functionForTest ) { 17586 if ( !functionForTest[ prop ] ) { 17587 functionForTest[ prop ] = jsSettings.functionForTest[ prop ]; 17588 } 17589 } 17590 } 17591 if ( jsSettings.functionForOperand ) { 17592 for ( prop in jsSettings.functionForOperand ) { 17593 if ( !functionForOperand[ prop ] ) { 17594 functionForOperand[ prop ] = jsSettings.functionForOperand[ prop ]; 17595 } 17596 } 17597 } 17598 isExtensionRegistered = true; 17599 } 17600 17601 // Identify that initialization has completed 17602 wb.ready( $elm, componentName ); 17603
17604 jsondata = wb.getData( $elm, shortName ); 17605 17606 if ( jsondata && jsondata.url ) { 17607 lstCall.push( jsondata ); 17608 } else if ( jsondata && Array.isArray( jsondata ) ) { 17609 i_len = jsondata.length; 17610 for ( i = 0; i !== i_len; i += 1 ) { 17611 lstCall.push( jsondata[ i ] ); 17612 } 17613 } 17614 17615 // Save it to the dataJSON object. 17616 $elm.data( dataQueue, lstCall ); 17617 17618 i_len = lstCall.length; 17619 for ( i = 0; i !== i_len; i += 1 ) { 17620 i_cache = lstCall[ i ]; 17621 loadJSON( elm, i_cache.url, i, i_cache.nocache, i_cache.nocachekey, i_cache.data, i_cache.contenttype, i_cache.method ); 17622 } 17623 17624 } 17625 }, 17626 17627 loadJSON = function( elm, url, refId, nocache, nocachekey, data, contentType, method ) { 17628 var $elm = $( elm ), 17629 fetchObj = { 17630 url: url, 17631 refId: refId, 17632 nocache: nocache, 17633 nocachekey: nocachekey, 17634 data: data, 17635 contentType: contentType, 17636 method: method 17637 }; 17638 17639 $elm.trigger( { 17640 type: "json-fetch.wb", 17641 fetch: fetchObj 17642 } ); 17643 }, 17644 17645 17646 // Manage JSON value After the json data has been fetched. This function can deal with array. 17647 jsonFetched = function( event ) { 17648 17649 var elm = event.target, 17650 $elm = $( elm ), 17651 lstCall = $elm.data( dataQueue ), 17652 fetchObj = event.fetch, 17653 itmSettings = lstCall[ fetchObj.refId ], 17654 jsonType = itmSettings.type, 17655 attrname = itmSettings.prop || itmSettings.attr, 17656 showEmpty = itmSettings.showempty, 17657 content = fetchObj.response, 17658 typeOfContent = typeof content, 17659 jQueryCaching; 17660 17661 if ( showEmpty || typeOfContent !== "undefined" ) { 17662 17663 if ( showEmpty && typeOfContent === "undefined" ) { 17664 content = ""; 17665 } 17666 17667 //Prevents the force caching of nested resources 17668 jQueryCaching = jQuery.ajaxSettings.cache; 17669 jQuery.ajaxSettings.cache = true; 17670 17671 // "replace" and "replaceWith" doesn't map to a jQuery function 17672 if ( !jsonType ) { 17673 jsonType = "template"; 17674 applyTemplate( elm, itmSettings, content ); 17675 17676 // Trigger wet 17677 if ( itmSettings.trigger && !wb.isDisabled ) { 17678 $elm 17679 .find( wb.allSelectors ) 17680 .addClass( "wb-init" ) 17681 .filter( ":not(#" + elm.id + " .wb-init .wb-init)" ) 17682 .trigger( "timerpoke.wb" ); 17683 } 17684 } else if ( jsonType === "replace" ) { 17685 $elm.html( content ); 17686 } else if ( jsonType === "replacewith" ) { 17687 $elm.replaceWith( content ); 17688 } else if ( jsonType === "addclass" ) { 17689 $elm.addClass( content ); 17690 } else if ( jsonType === "removeclass" ) { 17691 $elm.removeClass( content ); 17692 } else if ( jsonType === "prop" && attrname && allowPropNames.test( attrname ) ) { 17693 $elm.prop( attrname, content ); 17694 } else if ( jsonType === "attr" && attrname && allowAttrNames.test( attrname ) ) { 17695 $elm.attr( attrname, content ); 17696 } else if ( typeof $elm[ jsonType ] === "function" && allowJsonTypes.indexOf( jsonType ) !== -1 ) { 17697 $elm[ jsonType ]( content ); 17698 } else { 17699 throw componentName + " do not support type: " + jsonType; 17700 } 17701 17702 //Resets the initial jQuery caching setting 17703 jQuery.ajaxSettings.cache = jQueryCaching; 17704 17705 $elm.trigger( contentUpdatedEvent, { "json-type": jsonType, "content": content } ); 17706 } 17707 }, 17708 17709 // Apply the template as per the configuration 17710 applyTemplate = function( elm, settings, content ) { 17711 17712 var elmClass = elm.className, 17713 dataTable, 17714 dataTableAddRow, 17715 template = settings.source ? document.querySelector( settings.source ) : elm.querySelector( "template" ); 17716 17717 // If combined with wb-tables plugin 17718 if ( elm.tagName === "TABLE" && elmClass.indexOf( "wb-tables" ) !== -1 ) { 17719 17720 // Wait for its initialization before to applyTemplate 17721 if ( elmClass.indexOf( "wb-tables-inited" ) === -1 || !$.fn.dataTable || !$.fn.dataTable.isDataTable( elm ) ) { 17722 $( elm ).one( "wb-ready.wb-tables", function( ) { 17723 applyTemplate( elm, settings, content ); 17724 } ); 17725 return; 17726 } 17727 17728 dataTable = $( elm ).dataTable( { "retrieve": true } ).api(); 17729 dataTableAddRow = dataTable.row.add; 17730 settings.tobeclone = "tr"; // Only table row can be added 17731 } 17732 17733 if ( !template ) { 17734 return; 17735 } 17736 17737 // Needed when executing sub-template that wasn't polyfill, like in IE11 17738 if ( !template.content ) { 17739 wb.tmplPolyfill( template ); 17740 } 17741 17742 if ( !settings.streamline ) { 17743 dataIterator( elm, content, settings ); 17744 } else { 17745 processMapping( elm, elm, content, settings ); 17746 } 17747 17748 // Refresh the dataTable display 17749 if ( dataTableAddRow ) { 17750 dataTable.draw(); 17751 } 17752 }, 17753 17754 // Iterate over the dataset 17755 dataIterator = function( elm, content, mappingConfig, useClone ) { 17756 17757 var i, i_len, i_cache, 17758 elmAppendTo = elm, 17759 clone, template, 17760 dataTable, dataTableAddRow; 17761 17762 if ( mappingConfig.appendto ) { 17763 elmAppendTo = $( mappingConfig.appendto ).get( 0 ); 17764 } 17765 17766 // Connection with data table plugin 17767 if ( elm.tagName === "TABLE" && elm.className.indexOf( "wb-tables" ) !== -1 ) { 17768 dataTable = $( elm ).dataTable( { "retrieve": true } ).api(); 17769 dataTableAddRow = dataTable.row.add; 17770 mappingConfig.tobeclone = "tr"; 17771 } 17772 17773
17774 // if content is object, transform into array @id and @value 17775 if ( !Array.isArray( content ) ) { 17776 if ( typeof content !== "object" ) { 17777 content = [ content ]; 17778 } else { 17779 content = $.map( content, function( val, index ) { 17780 if ( val && typeof val === "object" && !Array.isArray( val ) ) { 17781 if ( !val[ "@id" ] ) { 17782 val[ "@id" ] = index; 17783 } 17784 } else { 17785 val = { 17786 "@id": index, 17787 "@value": val 17788 }; 17789 } 17790 return [ val ]; 17791 } ); 17792 } 17793 } 17794 i_len = content.length; 17795 17796 // Get the template to be iterated. 17797 if ( !useClone && mappingConfig.source ) { 17798 template = document.querySelector( mappingConfig.source ); 17799 } else if ( !useClone && mappingConfig.template ) { 17800 template = elm.querySelector( mappingConfig.template ); 17801 } else if ( !useClone ) { 17802 template = elm.querySelectorAll( ":scope > template" ); 17803 if ( template.length === 1 || template[ 0 ].attributes.length === 0 ) { 17804 17805 // Only when there is one choice or take the first one only if only there is no attribute set on the element 17806 template = template[ 0 ]; 17807 } else { 17808 17809 // let the mapping instructions to define which template to use 17810 template = false; 17811 } 17812 } 17813 17814 // Iterate the data array 17815 for ( i = 0; i < i_len; i += 1 ) { 17816 i_cache = content[ i ]; 17817 17818 17819 // If the data are filtered. This is deprecated and are only for backward compatible purpose 17820 if ( !filterPassJSON( i_cache, mappingConfig.filter, mappingConfig.filternot ) ) { 17821 continue; 17822 } 17823 17824 // Get the template (if applicable) 17825 if ( !clone && useClone ) { 17826 clone = useClone; 17827 } 17828 17829 // Create a clone if one unique template is found 17830 if ( !useClone && template && !mappingConfig.tobeclone ) { 17831 clone = template.content.cloneNode( true ); 17832 } else if ( !useClone && template ) { 17833 clone = template.content.querySelector( mappingConfig.tobeclone ).cloneNode( true ); 17834 } 17835 17836 // process the mapping, return value is the new clone object if applicable 17837 var tmpClone; 17838 tmpClone = processMapping( elm, clone, i_cache, mappingConfig ); 17839 17840 // Remove the template flag, to ensure we do reuse it for the subsequent iteration 17841 if ( tmpClone ) { 17842 delete mappingConfig.template; 17843 clone = tmpClone; 17844 } 17845 17846 // Add the clone object 17847 if ( dataTableAddRow ) { 17848 dataTableAddRow( $( clone ) ); // If wb-tables, use its API to add rows 17849 } else if ( !useClone && template ) { 17850 elmAppendTo.appendChild( clone ); 17851 } 17852 } 17853 17854 // Refresh the dataTable display (if applicable) 17855 if ( dataTableAddRow ) { 17856 dataTable.draw(); 17857 } 17858 17859 }, 17860 17861 // Check if the mapping are met or not 17862 canProcessMapping = function( content, mappingConfig ) { 17863 17864 var rawValue, value, 17865 testableData, 17866 operand = mappingConfig.operand || "softEq", 17867 operandOutcome; 17868 17869 if ( !mappingConfig.test ) { 17870 return; 17871 } 17872 17873 // Get the value to be tested 17874 try { 17875 rawValue = getRawValue( content, mappingConfig.assess || mappingConfig.value ); 17876 value = getValue( rawValue ); 17877 } catch ( ex ) { 17878 17879 // If this is an error, the path probably don't exist 17880 rawValue = undefined; 17881 value = undefined; 17882 } 17883 17884 // Get the function to use 17885 if ( !functionForTest[ mappingConfig.test ] ) { 17886 console.error( "The test function '" + mappingConfig.test + "' don't exist. Default to false test result" ); 17887 console.error( mappingConfig ); 17888 return false; 17889 } 17890 testableData = functionForTest[ mappingConfig.test ].call( mappingConfig, value, rawValue ); 17891 17892 // Run the operand 17893 if ( !functionForOperand[ operand ] ) { 17894 console.error( "The operand '" + operand + "' don't exist" ); 17895 console.error( mappingConfig ); 17896 operand = "softEq"; 17897 } 17898 operandOutcome = functionForOperand[ operand ].call( mappingConfig, testableData, mappingConfig.expect ); 17899 17900 // If not true, go next 17901 if ( !operandOutcome ) { 17902 return false; 17903 } 17904 17905 // Run mapping if satisfied 17906 return true; 17907 }, 17908 17909 // Special mapping typed function 17910 functionForTypedMapping = { 17911 17912 "rdf:Alt": function( elm, clone, content, mappingConfig ) { 17913 17914 var mapping = mappingConfig.mapping, 17915 i, i_cache, 17916 i_len = mapping.length, 17917 value = content; 17918 17919 for ( i = 0; i < i_len || i === 0; i += 1 ) { 17920 i_cache = mapping[ i ]; 17921 17922 if ( canProcessMapping( content, i_cache ) ) { 17923 17924 // Clone the object to avoid conflict when it is reused for other data 17925 i_cache = $.extend( true, {}, i_cache ); 17926 17927 // Remove the test, because it was checked 17928 delete i_cache.test; 17929 17930 // Navigate the content if specified 17931 if ( i_cache.value ) { 17932 value = getValue( content, i_cache.value ); 17933 } 17934 17935 // Process the mapping 17936 processMapping( elm, clone, value, i_cache ); 17937 17938 // End 17939 return; 17940 } 17941 } 17942 } 17943 }, 17944 17945 // Function called for testing the mapping condition, which can be extend via the js configuration 17946 functionForTest = { 17947 17948 "fn:isArray": function( value ) { 17949 return Array.isArray( value ); 17950 }, 17951 17952 "fn:isLiteral": function( value ) { 17953 17954 // Check if the value are set under the JSON-LD parameter @value 17955 if ( value && value[ "@value" ] ) { 17956 value = value[ "@value" ]; 17957 } 17958 17959 if ( value && typeof value !== "object" ) { 17960 return true; 17961 } 17962 17963 return false;
17964 }, 17965 17966 "fn:getType": function( value, rawValue ) { 17967 17968 var tp = value[ "@type" ] || rawValue[ "@type" ]; 17969 17970 if ( tp === "@json" ) { 17971 return "rdf:JSON"; 17972 } else if ( Array.isArray( tp ) && tp.indexOf( "@json" ) !== -1 ) { 17973 tp[ tp.indexOf( "@json" ) ] = "rdf:JSON"; 17974 } 17975 17976 if ( tp ) { 17977 return tp; 17978 } else { 17979 return typeof value; 17980 } 17981 }, 17982 17983 "fn:getValue": function( value ) { 17984 17985 return value; 17986 17987 }, 17988 17989 "fn:guessType": function( value, rawValue ) { 17990 17991 var guessType; 17992 17993 if ( !value ) { 17994 guessType = "undefined"; 17995 } else if ( value[ "@type" ] ) { 17996 guessType = value[ "@type" ]; 17997 } else if ( rawValue[ "@type" ] ) { 17998 guessType = rawValue[ "@type" ]; 17999 } else if ( value[ "@value" ] ) { 18000 18001 // Only if we are in JSON ld mode 18002 // Check if the value are set under the JSON-LD parameter @value 18003 value = value[ "@value" ]; 18004 } 18005 18006 // Edge case to convert the @json data type into its RDF form 18007 if ( guessType && guessType !== "undefined" ) { 18008 if ( guessType === "@json" ) { 18009 guessType = "rdf:JSON"; 18010 } else if ( Array.isArray( guessType ) && guessType.indexOf( "@json" ) !== -1 ) { 18011 guessType[ guessType.indexOf( "@json" ) ] = "rdf:JSON"; 18012 } 18013 } 18014 18015 if ( !guessType ) { 18016 if ( typeof value === "string" && value.match( /^([a-z][a-z0-9+\-.]*):/i ) ) { 18017 guessType = [ "xsd:anyURI", "rdfs:Literal" ]; 18018 } else if ( typeof value === "string" ) { 18019 guessType = [ "xsd:string", "rdfs:Literal" ]; 18020 } else if ( typeof value === "boolean" ) { 18021 guessType = [ "xsd:boolean", "rdfs:Literal" ]; 18022 } else if ( typeof value === "number" ) { 18023 guessType = [ "xsd:double", "rdfs:Literal" ]; 18024 } else if ( typeof value === "undefined" ) { 18025 guessType = "undefined"; 18026 } else if ( Array.isArray( value ) ) { 18027 guessType = "rdfs:Container"; 18028 } else { 18029 18030 // The type is a generic Object 18031 guessType = "rdfs:Resource"; 18032 } 18033 } 18034 18035 return guessType; 18036 } 18037 18038 }, 18039 18040 // Operand used to evaluate the testable output from functionForTest to determine if the mapping condition is met or not, which can be extend via the js configuration 18041 functionForOperand = { 18042 18043 "softEq": function( value, expect ) { 18044 var i, i_len; 18045 18046 if ( Array.isArray( value ) && !Array.isArray( expect ) && value.indexOf( expect ) !== -1 ) { 18047 return true; 18048 } else if ( Array.isArray( value ) && Array.isArray( expect ) ) { 18049 i_len = expect.length; 18050 for ( i = 0; i !== i_len; i++ ) { 18051 if ( value.indexOf( expect[ i ] ) ) { 18052 return true; 18053 } 18054 } 18055 } else if ( expect && value === expect ) { 18056 return true; 18057 } else if ( !expect && value ) { 18058 return true; 18059 } 18060 18061 return false; 18062 18063 }, 18064 18065 "eq": function( value, expect ) { 18066 18067 if ( _equalsJSON( value, expect ) ) { 18068 return true; 18069 } 18070 18071 return false; 18072 }, 18073 18074 "neq": function( value, expect ) { 18075 if ( !_equalsJSON( value, expect ) ) { 18076 return true; 18077 } 18078 18079 return false; 18080 }, 18081 18082 "in": function( value, expect ) { 18083 18084 var i; 18085 18086 if ( !expect ) { 18087 console.error( "Expected value is missing. Defaulting to false." ); 18088 console.error( this ); 18089 return false; 18090 } 18091 18092 if ( Array.isArray( value ) && !Array.isArray( expect ) && value.indexOf( expect ) !== -1 ) { 18093 return true; 18094 } else if ( Array.isArray( value ) && Array.isArray( expect ) ) { 18095 for ( i = 0; i !== expect.length; i++ ) { 18096 if ( value.indexOf( expect[ i ] ) ) { 18097 return true; 18098 } 18099 } 18100 } else if ( !Array.isArray( value ) && Array.isArray( expect ) && expect.indexOf( value ) !== -1 ) { 18101 return true; 18102 } else if ( value === expect ) { 18103 return true; 18104 } 18105 18106 return false; 18107 }, 18108 18109 "nin": function( value, expect ) { 18110 return !functionForOperand.in.call( this, value, expect ); 18111 } 18112 }, 18113 18114 // Mapping the data into a template or into a node 18115 processMapping = function( elm, clone, content, mappingConfig ) { 18116 18117 var j, j_cache, 18118 cached_node, cached_value, 18119 cached_value_is_HTML, cached_value_is_JSON, cached_value_is_IRI, 18120 queryAll = mappingConfig.queryall, 18121 selElements, 18122 mapping = mappingConfig.mapping, 18123 mapping_len, 18124 upstreamClone, template; 18125 18126 18127 // Is this mapping a special mapping type? 18128 if ( mappingConfig[ "@type" ] ) { 18129 functionForTypedMapping[ mappingConfig[ "@type" ] ].call( content, elm, clone, content, mappingConfig ); 18130 return; 18131 } 18132 18133 // Can we proceed? 18134 if ( mappingConfig.test && !canProcessMapping( content, mappingConfig ) ) { 18135 return; 18136 } 18137 18138 // Check if there is some mapping configuration 18139 if ( !mapping && !queryAll && !mappingConfig.template && typeof mapping !== "object" ) { 18140 return; 18141 } 18142 18143 // Clone mappingConfig to ensure it don't interfere with subsequent data iteration 18144 mappingConfig = $.extend( true, {}, mappingConfig ); 18145 18146 // If there is no clone, let use the element (parent) 18147 clone = clone || elm; 18148 18149 // If there is a "template" property, get the inner template 18150 if ( mappingConfig.template ) { 18151 template = clone.querySelector( mappingConfig.template ); 18152 18153 upstreamClone = clone; // Keep reference of the top clone 18154 18155 clone = template.content.cloneNode( true ); 18156 18157 // Ensure we don't recreated it if during a subsequent iteration 18158 delete mappingConfig.template; 18159 } 18160 18161 18162 // Is content an array? then iterate the content 18163 if ( Array.isArray( content ) ) { 18164 18165 18166 dataIterator( clone, content, mappingConfig, clone ); 18167 18168 // Case of where a template is associated with this mapping action 18169 if ( template ) { 18170 if ( template.parentNode ) { 18171 18172 if ( !mappingConfig.append ) { 18173 template.parentNode.insertBefore( clone, template ); 18174 } else { 18175 template.parentNode.appendChild( clone ); 18176 } 18177 } else { 18178 upstreamClone.appendChild( clone ); 18179 } 18180 18181 return elm; 18182 18183 } 18184 return; 18185 } 18186 18187 // Prepare the mapping object to be iterated 18188 if ( !mapping ) { 18189 mapping = [ {} ]; 18190 } 18191 if ( !Array.isArray( mapping ) ) { 18192 mapping = [ mapping ]; 18193 } 18194 mapping_len = mapping.length; 18195 18196 // Ensure the mapping is an array of Mapping Object 18197 for ( j = 0; j < mapping_len || j === 0; j += 1 ) { 18198 if ( typeof mapping[ j ] === "string" ) { 18199 mapping[ j ] = { 18200 value: mapping[ j ] 18201 }; 18202 } 18203 } 18204 18205 if ( queryAll ) { 18206 selElements = clone.querySelectorAll( queryAll ); 18207 18208 // Replicate this setting the in the mapping 18209 for ( j = 0; j < selElements.length || j === 0; j += 1 ) { 18210 if ( !mapping[ j ].selector && queryAll.indexOf( "nth-child" ) === -1 ) { 18211 mapping[ j ].selector = queryAll + ":nth-child(" + ( j + 1 ) + ")"; 18212 } else if ( !mapping[ j ].selector ) { 18213 mapping[ j ].selector = queryAll; 18214 } 18215 } 18216 } 18217 18218 18219 // 18220 // Process the mapping 18221 // 18222 for ( j = 0; j < mapping_len || j === 0; j += 1 ) { 18223 j_cache = mapping[ j ]; 18224 18225 // Reset the cache value special type flag 18226 cached_value_is_IRI = false;
18227 cached_value_is_HTML = false; 18228 cached_value_is_JSON = false; 18229 18230 // Get the element to be updated 18231 if ( j_cache.selector ) { 18232 cached_node = clone.querySelector( j_cache.selector ); 18233 } else { 18234 cached_node = clone; 18235 } 18236 18237 // Get the value to be set 18238 try { 18239 cached_value = getRawValue( content, j_cache ); 18240 } catch ( ex ) { 18241 18242 // The path don't exist, let continue to the next mapping item 18243 console.info( "JSON selector path for mapping don't exist in content" ); 18244 console.info( j_cache ); 18245 console.info( content ); 18246 continue; 18247 } 18248 18249 // Go to the next mapping if the value of JSON node don't exist to ensure we keep the default text set in the template, but move ahead if empty or null 18250 if ( typeof cached_value === "undefined" ) { 18251 continue; 18252 } 18253 18254 // Do the cache value contain special @type 18255 if ( cached_value && cached_value[ "@value" ] && cached_value[ "@type" ] ) { 18256 if ( !Array.isArray( cached_value[ "@type" ] ) ) { 18257 cached_value[ "@type" ] = [ cached_value[ "@type" ] ]; 18258 } 18259 cached_value_is_IRI = cached_value[ "@type" ].indexOf( "@id" ) !== -1; 18260 cached_value_is_HTML = cached_value[ "@type" ].indexOf( "rdf:HTML" ) !== -1; 18261 cached_value_is_JSON = cached_value[ "@type" ].indexOf( "rdf:JSON" ) !== -1 || cached_value[ "@type" ].indexOf( "@json" ) !== -1; 18262 } 18263 18264 // Action the value 18265 if ( Array.isArray( cached_value ) && ( j_cache.mapping || j_cache.queryall ) ) { 18266 18267 // Deep dive into the content if a mapping exist 18268 dataIterator( cached_node, cached_value, j_cache ); 18269 18270 } else if ( j_cache.mapping || j_cache.queryall || !j_cache.mapping && typeof j_cache.mapping === "object" ) { 18271 try { 18272 18273 // Map the inner mapping 18274 processMapping( template || elm, cached_node, cached_value, j_cache ); 18275 } catch ( ex ) { 18276 18277 if ( ex === "cached_node: null" && typeof cached_value === "object" ) { 18278 18279 // If it fail, let iterate the cached_value object 18280 dataIterator( cached_node, cached_value, j_cache ); 18281 } else { 18282 throw ex; 18283 } 18284 } 18285 } else if ( cached_value_is_IRI && cached_value_is_HTML ) { 18286 18287 // The import file type are expected to be HTML 18288 // Add the data-ajax instruction so the content would be added once the JSON mapping is completed and added on the page. 18289 cached_node.dataset.wbAjax = JSON.stringify( { 18290 url: cached_value[ "@value" ], 18291 type: "replace", 18292 dataType: cached_value_is_JSON ? "json" : null, 18293 encode: j_cache.encode 18294 } ); 18295 } else if ( cached_value_is_HTML && cached_value_is_JSON && !cached_value_is_IRI ) { 18296 18297 // Get content from the "@value" property which contain JSON value and use it as a string value 18298 cached_value = JSON.stringify( cached_value[ "@value" ] ); 18299 18300 // Map the value in the element 18301 mapValue( cached_node, cached_value, j_cache ); 18302 18303 } else if ( !cached_node && typeof cached_value === "object" ) { 18304 throw "cached_node: null"; 18305 } else if ( mappingConfig.mapping !== null ) { 18306 18307 cached_value = getValue( cached_value ); 18308 18309 // Serialize the value if it is an JS object and its not a null object 18310 if ( typeof cached_value === "object" && cached_value !== null ) { 18311 cached_value = JSON.stringify( cached_value ); 18312 } 18313 18314 // Map the value in the element 18315 mapValue( cached_node, cached_value, j_cache ); 18316 } 18317 18318 } 18319 18320 // Add the template, if applicable 18321 if ( template ) { 18322 if ( template.parentNode ) { 18323 18324 if ( !mappingConfig.append ) { 18325 template.parentNode.insertBefore( clone, template ); 18326 } else { 18327 template.parentNode.appendChild( clone ); 18328 } 18329 } else { 18330 upstreamClone.appendChild( clone ); 18331 } 18332 18333 return elm; 18334 } 18335 18336 }, 18337 18338 // Extract the value of an JS object 18339 getValue = function( source, pointer ) { 18340 18341 var value = getRawValue( source, pointer ); 18342 18343 // for JSON-LD @value support 18344 if ( typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call( value, "@value" ) ) { 18345 value = value[ "@value" ]; 18346 } 18347 18348 return value; 18349 }, 18350 18351 // Extract the value without considering it possible JSON-LD value 18352 getRawValue = function( source, pointer ) { 18353 18354 var value; 18355 pointer = pointer || false; // Ensure pointer is defined 18356 18357 // Get the value if source is string or pointer is pointing to root 18358 if ( typeof source === "string" || pointer === "/" || pointer === "/@value" || pointer.value === "/" || pointer.value === "/@value" ) { 18359 value = source; 18360 } else if ( typeof pointer === "string" ) { 18361 value = jsonpointer.get( source, pointer ); 18362 } else if ( pointer.value ) { 18363 value = jsonpointer.get( source, pointer.value ); 18364 } else { 18365 value = source; 18366 } 18367 18368 return value; 18369 }, 18370 18371 // Map a value into an HTML element or attribute 18372 mapValue = function( element, value, mappingConfig ) { 18373 18374 var attributeName, placeholderText; 18375 18376 attributeName = mappingConfig.attr; 18377 if ( attributeName ) { 18378 if ( !element.hasAttribute( attributeName ) ) { 18379 element.setAttribute( attributeName, "" ); 18380 } 18381 element = element.getAttributeNode( attributeName ); 18382 } 18383 18384 // Placeholder text replacement if any 18385 if ( mappingConfig.placeholder ) { 18386 placeholderText = element.textContent || ""; 18387 value = placeholderText.replace( mappingConfig.placeholder, value ); 18388 } 18389 18390 // Exclude null values and replace with default text 18391 if ( value !== null ) { 18392 if ( mappingConfig.isHTML ) { 18393 element.innerHTML = value; 18394 } else { 18395 element.textContent = value; 18396 } 18397 } 18398 }, 18399 18400 18401 // Filtering a JSON 18402 // Return true if trueness && falseness 18403 // Return false if !( trueness && falseness ) 18404 // trueness and falseness is an array of { "path": "", "value": "" } object 18405 filterPassJSON = function( obj, trueness, falseness ) { 18406 var i, i_cache, 18407 trueness_len = trueness ? trueness.length : 0, 18408 falseness_len = falseness ? falseness.length : 0, 18409 compareResult = false, 18410 isEqual; 18411 18412 if ( trueness_len || falseness_len ) { 18413 18414 for ( i = 0; i < trueness_len; i += 1 ) { 18415 i_cache = trueness[ i ]; 18416 isEqual = _equalsJSON( jsonpointer.get( obj, i_cache.path ), i_cache.value ); 18417 18418 if ( i_cache.optional ) { 18419 compareResult = compareResult || isEqual; 18420 } else if ( !isEqual ) { 18421 return false;
18422 } else { 18423 compareResult = true; 18424 } 18425 } 18426 if ( trueness_len && !compareResult ) { 18427 return false; 18428 } 18429 18430 for ( i = 0; i < falseness_len; i += 1 ) { 18431 i_cache = falseness[ i ]; 18432 isEqual = _equalsJSON( jsonpointer.get( obj, i_cache.path ), i_cache.value ); 18433 18434 if ( isEqual && !i_cache.optional || isEqual && i_cache.optional ) { 18435 return false; 18436 } 18437 } 18438 18439 } 18440 return true; 18441 }, 18442 18443 // 18444 _equalsJSON = function( a, b ) { 18445 switch ( typeof a ) { 18446 case "undefined": 18447 return false; 18448 case "boolean": 18449 case "string": 18450 case "number": 18451 return a === b; 18452 case "object": 18453 if ( a === null ) { 18454 return b === null; 18455 } 18456 var i, l; 18457 if ( Array.isArray( a ) ) { 18458 if ( !Array.isArray( b ) || a.length !== b.length ) { 18459 return false; 18460 } 18461 for ( i = 0, l = a.length; i < l; i++ ) { 18462 if ( !_equalsJSON( a[ i ], b[ i ] ) ) { 18463 return false; 18464 } 18465 } 18466 return true; 18467 } 18468 var bKeys = _objectKeys( b ), 18469 bLength = bKeys.length; 18470 if ( _objectKeys( a ).length !== bLength ) { 18471 return false; 18472 } 18473 for ( i in a ) { 18474 if ( !_equalsJSON( a[ i ], b[ i ] ) ) { 18475 return false; 18476 } 18477 } 18478 return true; 18479 default: 18480 return false; 18481 } 18482 }, 18483 _objectKeys = function( obj ) { 18484 var keys; 18485 if ( Array.isArray( obj ) ) { 18486 keys = new Array( obj.length ); 18487 for ( var k = 0; k < keys.length; k++ ) { 18488 keys[ k ] = "" + k; 18489 } 18490 return keys; 18491 } 18492 if ( Object.keys ) { 18493 return Object.keys( obj ); 18494 } 18495 keys = []; 18496 for ( var i in obj ) { 18497 if ( Object.prototype.hasOwnProperty.call( obj, i ) ) { 18498 keys.push( i ); 18499 } 18500 } 18501 return keys; 18502 }, 18503 18504 // Manage JSON value After the json data has been fetched 18505 jsonUpdate = function( event ) { 18506 var elm = event.target, 18507 $elm = $( elm ), 18508 lstCall = $elm.data( dataQueue ), 18509 refId = lstCall.length, 18510 wbJsonConfig = event[ "wb-json" ]; 18511 18512 if ( !( wbJsonConfig.url && ( wbJsonConfig.type || wbJsonConfig.source ) ) ) { 18513 throw "Data JSON update not configured properly"; 18514 } 18515 18516 lstCall.push( wbJsonConfig ); 18517 $elm.data( dataQueue, lstCall ); 18518 18519 loadJSON( elm, wbJsonConfig.url, refId ); 18520 }; 18521 18522$document.on( "json-failed.wb", selector, function( event ) { 18523 18524 var elm = event.currentTarget, 18525 $elm = $( elm ), 18526 lstCall = $elm.data( dataQueue ), 18527 fetchObj = event.fetch, 18528 xhrResponse = fetchObj.xhr, 18529 itmSettings = lstCall[ fetchObj.refId ], 18530 failSettings = itmSettings.fail; 18531 18532 if ( failSettings ) { 18533 18534 // Mapping is always streamline because the data structure is a static object not an array 18535 failSettings.streamline = true; 18536 18537 // apply the template to display an error message 18538 applyTemplate( elm, failSettings, { 18539 error: fetchObj.error.message || xhrResponse.statusText, 18540 status: fetchObj.status, 18541 url: fetchObj.fetchOpts.url, 18542 response: { 18543 text: xhrResponse.responseText || "", 18544 status: xhrResponse.status, 18545 statusText: xhrResponse.statusText 18546 } 18547 } ); 18548 } 18549 18550 console.info( event.currentTarget ); 18551 console.error( "Error or bad JSON Fetched from url in " + componentName ); 18552} ); 18553 18554// Load template polyfill 18555Modernizr.load( { 18556 test: ( "content" in document.createElement( "template" ) ), 18557 nope: "site!deps/template" + wb.getMode() + ".js" 18558} ); 18559 18560$document.on( "timerpoke.wb " + initEvent + " " + updateEvent + " json-fetched.wb", selector, function( event ) { 18561 18562 if ( event.currentTarget === event.target ) { 18563 switch ( event.type ) { 18564 18565 case "timerpoke": 18566 case "wb-init": 18567 init( event ); 18568 break; 18569 case "wb-update": 18570 jsonUpdate( event ); 18571 break; 18572 default: 18573 jsonFetched( event ); 18574 break; 18575 } 18576 } 18577 18578 return true; 18579} ); 18580 18581// Add the timerpoke to initialize the plugin 18582for ( s = 0; s !== selectorsLength; s += 1 ) { 18583 wb.add( selectors[ s ] ); 18584} 18585 18586} )( jQuery, window, wb ); 18587 18588/** 18589 * @title WET-BOEW Disable Event 18590 * @overview Event creates the active offer for users that have disabled the event. 18591 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 18592 * @author @gc 18593 */ 18594( function( $, window, wb ) { 18595"use strict"; 18596 18597/* 18598 * Variable and function definitions. 18599 * These are global to the event - meaning that they will be initialized once per page, 18600 * not once per instance of event on the page. 18601 */
18602var componentName = "wb-disable", 18603 selector = "#wb-tphp", 18604 $document = wb.doc, 18605 allowOnDisableClass = "wb-disable-allow", 18606 allowedPlugins = [ 18607 { 18608 selectors: "[data-wb-jsonmanager]", 18609 initEvent: "wb-jsonmanager" 18610 }, 18611 { 18612 selectors: "[data-wb-postback]", 18613 initEvent: "wb-postback" 18614 }, 18615 { 18616 selectors: [ 18617 "[data-ajax-after]", 18618 "[data-ajax-append]", 18619 "[data-ajax-before]", 18620 "[data-ajax-prepend]", 18621 "[data-ajax-replace]", 18622 "[data-wb-ajax]" 18623 ], 18624 initEvent: "wb-data-ajax" 18625 }, 18626 { 18627 selectors: [ 18628 "[data-json-after]", 18629 "[data-json-append]", 18630 "[data-json-before]", 18631 "[data-json-prepend]", 18632 "[data-json-replace]", 18633 "[data-json-replacewith]", 18634 "[data-wb-json]" 18635 ], 18636 initEvent: "wb-data-json" 18637 } 18638 ], 18639 18640 /** 18641 * @method init 18642 * @param {jQuery Event} event Event that triggered the function call 18643 */ 18644 init = function( event ) { 18645 18646 // Start initialization 18647 // returns DOM object = proceed with init 18648 // returns undefined = do not proceed with init (e.g., already initialized) 18649 var elm = wb.init( event, componentName, selector, true ), 18650 nQuery = "?", 18651 $html = wb.html, 18652 i18n = wb.i18n, 18653 pageUrl = wb.pageUrlParts, 18654 param, 18655 noticeHeader = i18n( "disable-notice-h" ), 18656 noticeBody = i18n( "disable-notice" ), 18657 noticehtml = "<section", 18658 noticehtmlend = "</a>.</p></section>", 18659 canonicalUrl, 18660 canonicalLink; 18661 18662 if ( elm ) { 18663 18664 // Rebuild the query string 18665 for ( param in pageUrl.params ) { 18666 if ( param && Object.prototype.hasOwnProperty.call( pageUrl.params, param ) && param !== "wbdisable" ) { 18667 nQuery += param + "=" + pageUrl.params[ param ] + "&"; 18668 } 18669 } 18670 18671 try { 18672 if ( wb.isDisabled || wb.ie ) { 18673 $html.addClass( "wb-disable" ); 18674 18675 try { 18676 18677 // Store preference for WET plugins and polyfills to be disabled in localStorage 18678 localStorage.setItem( "wbdisable", "true" ); 18679 } catch ( e ) { 18680 18681 /* swallow error */ 18682 } 18683 18684 // Trigger initialization of plugins that are needed in basic mode 18685 runAllowedPlugins(); 18686 18687 // Add canonical link if not already present 18688 if ( !document.querySelector( "link[rel=canonical]" ) ) { 18689 18690 // Remove wbdisable from URL 18691 canonicalUrl = window.location.href.replace( /&?wbdisable=true/gi, "" ).replace( "?&", "?" ).replace( "?#", "#" ); 18692 18693 if ( canonicalUrl.indexOf( "?" ) === ( canonicalUrl.length - 1 ) ) { 18694 canonicalUrl = canonicalUrl.replace( "?", "" ); 18695 } 18696 18697 canonicalLink = document.createElement( "link" ); 18698 canonicalLink.rel = "canonical"; 18699 canonicalLink.href = canonicalUrl; 18700 18701 document.head.appendChild( canonicalLink ); 18702 } 18703 18704 // Add notice and link to re-enable WET plugins and polyfills 18705 let significantLinkId = wb.getId(); 18706 noticehtml = noticehtml + " class='container-fluid bg-warning text-center mrgn-tp-sm py-4'><h2 class='mrgn-tp-0'>" + noticeHeader + "</h2><p>" + noticeBody + "</p><p><a id='" + significantLinkId + "' rel='alternate' href='" + nQuery + "wbdisable=false'>" + i18n( "wb-enable" ) + noticehtmlend; 18707 $( elm ).after( noticehtml ); 18708 document.querySelector( "#" + significantLinkId ).setAttribute( "property", "significantLink" ); 18709 return true; 18710 } else { 18711 $html.addClass( "wb-enable" ); 18712 18713 if ( localStorage ) { 18714 18715 // Store preference for WET plugins and polyfills to be enabled in localStorage 18716 localStorage.setItem( "wbdisable", "false" ); 18717 } 18718 18719 // Remove variable from URL 18720 var lc = window.location.href.replace( /&?wbdisable=false/gi, "" ).replace( "?&", "?" ).replace( "?#", "#" ); 18721 if ( lc.indexOf( "?" ) === ( lc.length - 1 ) ) { 18722 lc = lc.replace( "?", "" ); 18723 } 18724 window.history.replaceState( "", "", lc ); 18725 } 18726 } catch ( error ) { 18727 18728 /* swallow error */ 18729 } 18730 18731 // Append the Basic HTML version link version 18732 // Add link to disable WET plugins and polyfills 18733 wb.addSkipLink( i18n( "wb-disable" ), { 18734 href: nQuery + "wbdisable=true", 18735 rel: "alternate" 18736 }, false, true ); 18737 18738 // Identify that initialization has completed 18739 wb.ready( $document, componentName ); 18740 } 18741 }, 18742 18743 // Trigger initialization of plugins that are needed in basic mode
18744 // TODO: Remove once basic initialization is implemented at the core level 18745 runAllowedPlugins = function() { 18746 allowedPlugins.forEach( allowedPlugin => { 18747 if ( typeof( allowedPlugin.selectors ) === "object" ) { 18748 allowedPlugin.selectors = allowedPlugin.selectors.join( "." + allowOnDisableClass + "," ); 18749 } 18750 18751 allowedPlugin.selectors = allowedPlugin.selectors + "." + allowOnDisableClass; 18752 18753 $( allowedPlugin.selectors ).trigger( "wb-init." + allowedPlugin.initEvent ); 18754 } ); 18755 }; 18756 18757// Bind the events 18758$document.on( "timerpoke.wb", selector, init ); 18759 18760// Go through allowed plugins once again when content has been added dynamically 18761$document.on( "wb-contentupdated", function() { 18762 runAllowedPlugins(); 18763} ); 18764 18765// Add the timer poke to initialize the plugin 18766wb.add( selector ); 18767 18768} )( jQuery, window, wb ); 18769 18770/** 18771 * @title WET-BOEW Focus 18772 * @overview User agent safe way of assigning focus to an element 18773 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 18774 * @author @pjackson28 18775 */ 18776( function( $, wb ) { 18777"use strict"; 18778 18779var $document = wb.doc, 18780 $window = wb.win, 18781 clickEvents = "click", 18782 setFocusEvent = "setfocus.wb", 18783 linkSelector = "a[href]", 18784 $linkTarget, 18785 18786 /** 18787 * @method processHash 18788 */ 18789 processHash = function() { 18790 var hash = wb.pageUrlParts.hash; 18791 18792 if ( hash && ( $linkTarget = $( "#" + wb.jqEscape( hash.substring( 1 ) ) ) ).length !== 0 ) { 18793 $linkTarget.trigger( setFocusEvent ); 18794 } 18795 }; 18796 18797// Bind the setfocus event 18798$document.on( setFocusEvent, function( event ) { 18799 if ( event.namespace === "wb" ) { 18800 var elm = event.target, 18801 $elm = $( elm ), 18802 $closedParents = $elm.not( "summary" ).parents( "details, [role='tabpanel']" ), 18803 $closedPanels, $closedPanel, len, i; 18804 18805 if ( $closedParents.length !== 0 ) { 18806 18807 // Open any closed ancestor details elements 18808 $closedParents.not( "[open]" ).children( "summary" ).trigger( "click" ); 18809 18810 // Open any closed tabpanels 18811 $closedPanels = $closedParents.filter( "[aria-hidden='true']" ); 18812 len = $closedPanels.length; 18813 for ( i = 0; i !== len; i += 1 ) { 18814 $closedPanel = $closedPanels.eq( i ); 18815 $closedPanel.closest( ".wb-tabs" ) 18816 .find( "#" + $closedPanel.attr( "aria-labelledby" ) ) 18817 .trigger( "click" ); 18818 } 18819 } 18820 18821 // Assigns focus to an element (delay allows for revealing of hidden content) 18822 setTimeout( function() { 18823 let addedTabIndexAttr = false; 18824 18825 $elm.trigger( "focus" ); 18826 18827 // If the element hasn't gained focus, retry with a tabindex attribute... 18828 if ( elm !== document.activeElement && elm.getAttribute( "tabindex" ) === null ) { 18829 18830 // Add a tabindex="-1" attribute and keep track of it for later 18831 elm.setAttribute( "tabindex", "-1" ); 18832 addedTabIndexAttr = true; 18833 18834 // Try re-focusing 18835 $elm.trigger( "focus" ); 18836 } 18837 18838 // If the element has gained focus... 18839 if ( elm === document.activeElement ) { 18840 const $topBar = $( ".wb-bar-t[aria-hidden=false]" ); 18841 18842 // Ensure the top bar overlay does not conceal the focus target 18843 if ( $topBar.length !== 0 ) { 18844 document.documentElement.scrollTop -= $topBar.outerHeight(); 18845 } 18846 } else { 18847 18848 // Remove the element's tabindex attribute (no point in keeping it if it didn't help) 18849 if ( addedTabIndexAttr ) { 18850 elm.removeAttribute( "tabindex" ); 18851 } 18852 18853 // Show a console error 18854 console.error( setFocusEvent + ": Unable to focus onto the destination element... maybe it's hidden?" ); 18855 console.error( elm ); 18856 } 18857 18858 return $elm; 18859 }, 100 ); 18860 } 18861} ); 18862 18863// Set focus to the target of a deep link from a different page 18864// (helps browsers that can't set the focus on their own) 18865$document.on( "wb-ready.wb", processHash ); 18866 18867// Handle any changes to the URL hash after the page has loaded 18868$window.on( "hashchange", function() { 18869 wb.pageUrlParts.hash = window.location.hash; 18870 if ( !wb.ignoreHashChange ) { 18871 processHash(); 18872 } 18873} ); 18874 18875// Helper for browsers that can't change keyboard and/or event focus on a same page link click 18876$document.on( clickEvents, linkSelector, function( event ) { 18877 var testHref = event.currentTarget.getAttribute( "href" ); 18878 18879 // Same page links only 18880 if ( testHref.length > 1 && testHref.charAt( 0 ) === "#" && !event.isDefaultPrevented() && 18881 ( $linkTarget = $( "#" + wb.jqEscape( testHref.substring( 1 ) ) ) ).length !== 0 ) { 18882 wb.ignoreHashChange = true; 18883 $linkTarget.trigger( setFocusEvent ); 18884 } 18885} ); 18886 18887} )( jQuery, wb ); 18888 18889/** 18890 * @title WET-BOEW JSON Manager 18891 * @overview Manage JSON dataset, execute JSON patch operation. 18892 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 18893 * @author @duboisp 18894 */ 18895/*global jsonpointer, jsonpatch */ 18896( function( $, window, wb ) { 18897"use strict"; 18898 18899/* 18900 * Variable and function definitions. 18901 * These are global to the plugin - meaning that they will be initialized once per page, 18902 * not once per instance of plugin on the page. So, this is a good place to define 18903 * variables that are common to all instances of the plugin on a page. 18904 */
18905var componentName = "wb-jsonmanager", 18906 selector = "[data-" + componentName + "]", 18907 initEvent = "wb-init." + componentName, 18908 postponeEvent = "postpone." + componentName, 18909 patchesEvent = "patches." + componentName, 18910 jsonFailedClass = "jsonfail", 18911 reloadFlag = "data-" + componentName + "-reload", 18912 dsNameRegistered = [], 18913 datasetCache = {}, 18914 datasetCacheSettings = {}, 18915 dsDelayed = {}, 18916 dsPostponePatches = {}, 18917 dsFetching = {}, 18918 dsFetchIsArray = {}, 18919 dsFetchMerged = {}, 18920 $document = wb.doc, 18921 defaults = { 18922 ops: [ 18923 { 18924 name: "patches", 18925 fn: function( obj, key, tree ) { 18926 var path = this.path, 18927 patches = this.patches, 18928 newTree = jsonpointer.get( tree, path ); 18929 18930 patches.forEach( ( patchConf ) => { 18931 patchConf.mainTree = tree; 18932 patchConf.pathParent = path; 18933 jsonpatch.apply( newTree, [ patchConf ] ); 18934 } ); 18935 } 18936 }, 18937 { 18938 name: "wb-count", 18939 fn: function( obj, key, tree ) { 18940 var countme = obj[ key ], 18941 len = 0, i_len, i, 18942 filter = this.filter || [ ], 18943 filternot = this.filternot || [ ]; 18944 18945 if ( !Array.isArray( filter ) ) { 18946 filter = [ filter ]; 18947 } 18948 if ( !Array.isArray( filternot ) ) { 18949 filternot = [ filternot ]; 18950 } 18951 18952 if ( ( filter.length || filternot.length ) && Array.isArray( countme ) ) { 18953 18954 // Iterate in obj[key] / item and check if is true for the given path is any. 18955 i_len = countme.length; 18956 18957 for ( i = 0; i !== i_len; i = i + 1 ) { 18958 if ( filterPassJSON( countme[ i ], filter, filternot ) ) { 18959 len = len + 1; 18960 } 18961 } 18962 } else if ( Array.isArray( countme ) ) { 18963 len = countme.length; 18964 } 18965 applyPatch( tree, "add", this.set, len ); 18966 } 18967 }, 18968 { 18969 name: "wb-first", 18970 fn: function( obj, key, tree ) { 18971 var currObj = obj[ key ]; 18972 if ( !Array.isArray( currObj ) || currObj.length === 0 ) { 18973 return; 18974 } 18975 18976 applyPatch( tree, "add", this.set, currObj[ 0 ] ); 18977 } 18978 }, 18979 { 18980 name: "wb-last", 18981 fn: function( obj, key, tree ) { 18982 var currObj = obj[ key ]; 18983 if ( !Array.isArray( currObj ) || currObj.length === 0 ) { 18984 return; 18985 } 18986 18987 applyPatch( tree, "add", this.set, currObj[ currObj.length - 1 ] ); 18988 } 18989 }, 18990 { 18991 name: "wb-nbtolocal", 18992 fn: function( obj, key, tree ) { 18993 var val = obj[ key ], 18994 loc = this.locale || window.wb.lang, 18995 suffix = this.suffix || "", 18996 prefix = this.prefix || ""; 18997 18998 if ( typeof val === "string" ) { 18999 val = parseFloat( val ); 19000 if ( isNaN( val ) ) { 19001 return; 19002 } 19003 } 19004 19005 applyPatch( tree, "replace", this.path, prefix + val.toLocaleString( loc ) + suffix ); 19006 } 19007 }, 19008 { 19009 name: "wb-decodeUTF8Base64", 19010 fn: function( obj, key, tree ) { 19011 var val = obj[ key ]; 19012 19013 if ( !this.set ) { 19014 applyPatch( tree, "replace", this.path, wb.decodeUTF8Base64( val ) ); 19015 } else { 19016 applyPatch( tree, "add", this.set, wb.decodeUTF8Base64( val ) ); 19017 } 19018 } 19019 }, 19020 { 19021 name: "wb-escapeHTML", 19022 fn: function( obj, key, tree ) { 19023 var val = obj[ key ]; 19024 19025 if ( !this.set ) { 19026 applyPatch( tree, "replace", this.path, wb.escapeHTML( val ) ); 19027 } else { 19028 applyPatch( tree, "add", this.set, wb.escapeHTML( val ) ); 19029 } 19030 } 19031 }, 19032 { 19033 name: "wb-toDateISO", 19034 fn: function( obj, key, tree ) { 19035 if ( !this.set ) { 19036 applyPatch( tree, "replace", this.path, wb.date.toDateISO( obj[ key ] ) ); 19037 } else { 19038 applyPatch( tree, "add", this.set, wb.date.toDateISO( obj[ key ] ) ); 19039 } 19040 } 19041 }, 19042 { 19043 name: "wb-toDateTimeISO", 19044 fn: function( obj, key, tree ) { 19045 if ( !this.set ) { 19046 applyPatch( tree, "replace", this.path, wb.date.toDateISO( obj[ key ], true ) ); 19047 } else { 19048 applyPatch( tree, "add", this.set, wb.date.toDateISO( obj[ key ], true ) ); 19049 } 19050 } 19051 }, 19052 { 19053 name: "wb-swap", 19054 fn: function( obj, key, tree ) { 19055 var val = obj[ key ], 19056 ref = this.ref, 19057 mainTree = this.mainTree || obj, 19058 path = this.path, 19059 newVal, 19060 refObject, refIsArray, valWasArray, 19061 i, i_len, i_item, 19062 j, j_len, j_item; 19063 19064 if ( val ) { 19065 19066 refObject = jsonpointer.get( mainTree, ref ); 19067 refIsArray = Array.isArray( refObject ); 19068 valWasArray = Array.isArray( val ); 19069 19070 if ( !valWasArray ) { 19071 val = [ val ]; 19072 } 19073 19074 i_len = val.length; 19075 19076 for ( i = 0; i !== i_len; i++ ) { 19077 i_item = val[ i ]; 19078 newVal = undefined; // Reinit 19079 if ( !refIsArray ) { 19080 i_item = i_item.replaceAll( "~", "~0" ).replaceAll( "/", "~1" ); // Escape slashed and tilde in val when the key is an IRI for JSON pointer compatibility 19081 newVal = mainTree ? jsonpointer.get( mainTree, ref + "/"
19081+ i_item ) : jsonpointer.get( tree, ref + "/" + i_item ); 19082 } else { 19083 19084 // Iterate until we found a corresponding value in the property "@id" 19085 j_len = refObject.length; 19086 for ( j = 0; j !== j_len; j++ ) { 19087 j_item = refObject[ j ]; 19088 if ( j_item[ "@id" ] && j_item[ "@id" ] === i_item ) { 19089 newVal = j_item; 19090 break; 19091 } 19092 } 19093 if ( !newVal ) { 19094 console.error( "wb-swap: Unable to find a corresponding value for: " + val + " in reference " + ref ); 19095 break; 19096 } 19097 } 19098 if ( newVal && !valWasArray ) { 19099 applyPatch( tree, "replace", path, newVal ); 19100 } else if ( newVal ) { 19101 applyPatch( tree, "replace", path + "/" + i, newVal ); 19102 } 19103 } 19104 } 19105 } 19106 } 19107 ], 19108 opsArray: [ 19109 { 19110 name: "wb-toDateISO", 19111 fn: function( arr ) { 19112 var setval = this.set, 19113 pathval = this.path, 19114 i, i_len = arr.length; 19115 for ( i = 0; i !== i_len; i += 1 ) { 19116 if ( setval ) { 19117 jsonpatch.apply( arr, [ 19118 { op: "wb-toDateISO", set: "/" + i + setval, path: "/" + i + pathval } 19119 ] ); 19120 } else { 19121 jsonpatch.apply( arr, [ 19122 { op: "wb-toDateISO", path: "/" + i + pathval } 19123 ] ); 19124 } 19125 } 19126 } 19127 }, 19128 { 19129 name: "wb-toDateTimeISO", 19130 fn: function( arr ) { 19131 var setval = this.set, 19132 pathval = this.path, 19133 i, i_len = arr.length; 19134 for ( i = 0; i !== i_len; i += 1 ) { 19135 if ( setval ) { 19136 jsonpatch.apply( arr, [ 19137 { op: "wb-toDateTimeISO", set: "/" + i + setval, path: "/" + i + pathval } 19138 ] ); 19139 } else { 19140 jsonpatch.apply( arr, [ 19141 { op: "wb-toDateTimeISO", path: "/" + i + pathval } 19142 ] ); 19143 } 19144 } 19145 } 19146 }, 19147 { 19148 name: "wb-swap", 19149 fn: function( arr ) { 19150 arr.forEach( ( item, i ) => { 19151 jsonpatch.apply( arr, [ 19152 { op: "wb-swap", path: "/" + i + this.path, ref: this.ref, mainTree: this.mainTree } 19153 ] ); 19154 } ); 19155 } 19156 }, 19157 { 19158 name: "patches", 19159 fn: function( arr ) { 19160 arr.forEach( ( item, i ) => { 19161 jsonpatch.apply( this.mainTree || arr, [ 19162 { op: "patches", path: ( this.pathParent || "" ) + "/" + i + this.path, patches: this.patches } 19163 ] ); 19164 } ); 19165 } 19166 } 19167 ], 19168 opsRoot: [], 19169 settings: { }, 19170 docMapKeys: { "referer": document.referrer, "locationHref": location.href } 19171 }, 19172 19173 // Add debug information after the JSON manager element 19174 debugPrintOut = function( $elm, name, json, patches ) { 19175 $elm.after( "<p lang=\"en\"><strong>JSON Manager Debug</strong> (" + name + ")</p><ul lang=\"en\"><li>JSON: <pre><code>" + JSON.stringify( json ) + "</code></pre></li><li>Patches: <pre><code>" + JSON.stringify( patches ) + "</code></pre>" ); 19176 console.log( json ); 19177 }, 19178 19179 /** 19180 * @method init 19181 * @param {jQuery Event} event Event that triggered the function call 19182 */ 19183 init = function( event ) { 19184 19185 // Start initialization 19186 // returns DOM object = proceed with init 19187 // returns undefined = do not proceed with init (e.g., already initialized) 19188 var elm = wb.init( event, componentName, selector ), 19189 $elm, 19190 jsSettings = window[ componentName ] || { }, 19191 ops, opsArray, opsRoot, 19192 i, i_len, i_cache, 19193 url, urlActual, dsName, 19194 fetchOpts = { }; 19195 19196 if ( elm ) { 19197 $elm = $( elm ); 19198 19199 // Load handlebars 19200 Modernizr.load( { 19201 19202 // For loading multiple dependencies 19203 load: [ 19204 "site!deps/json-patch" + wb.getMode() + ".js", 19205 "site!deps/jsonpointer" + wb.getMode() + ".js" 19206 ], 19207 testReady: function() { 19208 return window.jsonpatch && window.jsonpointer; 19209 }, 19210 complete: function() { 19211 var elmData = wb.getData( $elm, componentName ); 19212 19213 if ( !defaults.registered ) { 19214 ops = defaults.ops.concat( jsSettings.ops || [ ] ); 19215 opsArray = defaults.opsArray.concat( jsSettings.opsArray || [ ] ); 19216 opsRoot = defaults.opsRoot.concat( jsSettings.opsRoot || [ ] ); 19217 19218 if ( ops.length ) { 19219 for ( i = 0, i_len = ops.length; i !== i_len; i++ ) { 19220 i_cache = ops[ i ]; 19221 jsonpatch.registerOps( i_cache.name, i_cache.fn ); 19222 } 19223 } 19224 if ( opsArray.length ) { 19225 for ( i = 0, i_len = opsArray.length; i !== i_len; i++ ) { 19226 i_cache = opsArray[ i ]; 19227 jsonpatch.registerOpsArray( i_cache.name, i_cache.fn ); 19228 } 19229 } 19230 if ( opsRoot.length ) { 19231 for ( i = 0, i_len = opsRoot.length; i !== i_len; i++ ) { 19232 i_cache = opsRoot[ i ]; 19233 jsonpatch.registerOpsRoot( i_cache.name, i_cache.fn ); 19234 } 19235 } 19236 defaults.settings = $.extend( {}, defaults.settings, jsSettings.settings || {} ); 19237 defaults.registered = true; 19238 } 19239 19240 dsName = elmData.name; 19241 if ( !dsName || dsName in dsNameRegistered ) { 19242 throw "Dataset name must be unique"; 19243 } 19244 dsNameRegistered.push( dsName ); 19245 19246 url = elmData.url; 19247 19248 if ( url ) { 19249 19250 url = typeof url === "string" ? [ url ] : url; 19251 i_len = url.length; 19252 19253 dsFetching[ dsName ] = i_len; 19254 19255 for ( i = 0; i !== i_len; i++ ) { 19256 19257 urlActual = url[ i ]; 19258 19259 // Fetch default configuration 19260 fetchOpts = { 19261 nocache: elmData.nocache, 19262 nocachekey: elmData.nocachekey, 19263 data: elmData.data, 19264 contentType: elmData.contenttype, 19265 method: elmData.method 19266 }; 19267 19268 // When the "url" is an extended configuration 19269 if ( urlActual.url ) {
19270 fetchOpts.savingPath = urlActual.path || ""; 19271 fetchOpts.url = urlActual.url; 19272 } else { 19273 fetchOpts.url = urlActual; 19274 } 19275 19276 // Fetch the JSON 19277 $elm.trigger( { 19278 type: "json-fetch.wb", 19279 fetch: fetchOpts 19280 } ); 19281 19282 // If the URL is a dataset, make it ready 19283 if ( fetchOpts.url.charCodeAt( 0 ) === 35 && fetchOpts.url.charCodeAt( 1 ) === 91 ) { 19284 wb.ready( $elm, componentName ); 19285 } 19286 } 19287 } else if ( !url && elmData.extractor ) { 19288 $elm.trigger( { 19289 type: "json-fetched.wb", 19290 fetch: { 19291 response: {} 19292 } 19293 } ); 19294 wb.ready( $elm, componentName ); 19295 19296 } else { 19297 19298 $elm.trigger( { 19299 type: "json-fetch.wb" 19300 } ); 19301 wb.ready( $elm, componentName ); 19302 } 19303 19304 } 19305 } ); 19306 } 19307 }, 19308 extractData = function( elmObj ) { 19309 19310 var isGroup = false, 19311 selectedTag, 19312 targetTag, 19313 lastIndex = [], 19314 j_tag = "", 19315 group = {}, 19316 arrMap = [], 19317 node_children = [], 19318 j_node = 0, 19319 arrRepeatPath = [], 19320 combineToObj = function( cur_obj ) { 19321 if ( cur_obj.selector === j_tag ) { 19322 if ( !lastIndex.includes( j_tag ) ) { 19323 group[ cur_obj.path ] = cur_obj.attr && node_children[ j_node ].getAttributeNode( cur_obj.attr ) ? 19324 node_children[ j_node ].getAttributeNode( cur_obj.attr ).textContent : 19325 node_children[ j_node ].textContent; 19326 lastIndex.push( j_tag ); 19327 } 19328 } 19329 }, 19330 manageObjDir = function( selector, selectedValue, json_return ) { 19331 var arrPath = selector.path.split( "/" ).filter( Boolean ); 19332 19333 // Check if selectedValue is an empty value returned by querySelectorAll 19334 if ( selectedValue && selectedValue instanceof NodeList && selectedValue.length === 0 ) { 19335 selectedValue = null; 19336 } 19337 19338 if ( arrPath.length > 1 ) { 19339 var pointer = ""; 19340 pointer = arrPath.pop(); 19341 19342 if ( arrPath[ 0 ] && arrPath[ 0 ] !== "" ) { 19343 19344 if ( !json_return[ arrPath[ 0 ] ] && !arrRepeatPath.includes( arrPath[ 0 ] ) ) { 19345 arrRepeatPath.push( arrPath[ 0 ] ); 19346 json_return[ arrPath[ 0 ] ] = {}; 19347 } 19348 if ( selector.selectAll && !json_return[ arrPath[ 0 ] ] [ pointer ] ) { 19349 json_return[ arrPath[ 0 ] ] [ pointer ] = []; 19350 } 19351 if ( selector.selectAll ) { 19352 json_return[ arrPath[ 0 ] ] [ pointer ].push( selectedValue ); 19353 } else { 19354 json_return[ arrPath[ 0 ] ] [ pointer ] = selectedValue; 19355 } 19356 19357 } else { 19358 19359 if ( selector.selectAll ) { 19360 json_return[ arrPath[ 0 ] ].push( selectedValue ); 19361 } else { 19362 json_return[ pointer ] = selectedValue; 19363 } 19364 } 19365 } else { 19366 19367 if ( selector.selectAll ) { 19368 if ( !json_return[ selectedTag.path ] ) { 19369 json_return[ selectedTag.path ] = []; 19370 } 19371 json_return[ selectedTag.path ].push( selectedValue ); 19372 } else { 19373 json_return[ selectedTag.path ] = selectedValue; 19374 } 19375 } 19376 }, 19377 jsonSource = {}; 19378 19379 19380 for ( var tag = 0; tag <= elmObj.length - 1; tag++ ) { 19381 19382 selectedTag = elmObj[ tag ]; 19383 19384 if ( !selectedTag.interface ) { 19385 19386 targetTag = document.querySelectorAll( selectedTag.selector || "" ); 19387 isGroup = selectedTag.extractor && selectedTag.extractor.length >= 1 ? true : false; 19388 19389 if ( selectedTag.selectAll ) { 19390 19391 for ( var i_node = 0; i_node <= targetTag.length - 1; i_node++ ) { 19392 19393 var selectedTagValue = selectedTag.attr && targetTag [ i_node ].getAttributeNode( selectedTag.attr ) ? 19394 targetTag [ i_node ].getAttributeNode( selectedTag.attr ).textContent : 19395 targetTag [ i_node ].textContent; 19396 19397 manageObjDir( selectedTag, selectedTagValue, jsonSource ); 19398 } 19399 } 19400 19401 // extract from combined selectors and group the values e.g dt with dd 19402 if ( isGroup ) { 19403 19404 jsonSource[ selectedTag.path ] = []; 19405 19406 node_children = targetTag[ 0 ].children; 19407 19408 var extractorLength = Object.keys( selectedTag.extractor ).length; 19409 19410 for ( j_node = 0; j_node <= node_children.length - 1; j_node++ ) { 19411 19412 j_tag = node_children[ j_node ].tagName.toLowerCase(); 19413 19414 selectedTag.extractor.find( combineToObj ); 19415 if ( Object.keys( group ).length === extractorLength ) { 19416 arrMap.push( group ); 19417 group = {}; 19418 lastIndex = []; 19419 } 19420 } 19421 $.extend( jsonSource[ selectedTag.path ], arrMap ); 19422 } 19423 19424 if ( targetTag.length ) { 19425 targetTag = selectedTag.attr && targetTag [ 0 ].getAttributeNode( selectedTag.attr ) ? 19426 targetTag [ 0 ].getAttributeNode( selectedTag.attr ).textContent : 19427 targetTag [ 0 ].textContent; 19428 } 19429 19430 } else { 19431 19432 targetTag = defaults.docMapKeys[ selectedTag.interface ]; 19433 19434 manageObjDir( selectedTag, targetTag, jsonSource ); 19435 } 19436 19437 if ( !selectedTag.selectAll ) { 19438 if ( isGroup === false ) { 19439 manageObjDir( selectedTag, targetTag, jsonSource ); 19440 } 19441 } 19442 } 19443 19444 return jsonSource; 19445 }, 19446 19447 // Filtering a JSON 19448 // Return true if trueness && falseness 19449 // Return false if !( trueness && falseness ) 19450 // trueness and falseness is an array of { "path": "", "value": "" } object 19451 filterPassJSON = function( obj, trueness, falseness ) { 19452 var i, i_cache, 19453 trueness_len = trueness.length, 19454 falseness_len = falseness.length, 19455 compareResult = false, 19456 isEqual; 19457 19458 if ( trueness_len || falseness_len ) { 19459 19460 for ( i = 0; i < trueness_len; i += 1 ) { 19461 i_cache = trueness[ i ]; 19462 isEqual = _equalsJSON( jsonpointer.get( obj, i_cache.path ), i_cache.value ); 19463 19464 if ( i_cache.optional ) { 19465 compareResult = compareResult || isEqual; 19466 } else if ( !isEqual ) { 19467 return false;
19468 } else { 19469 compareResult = true; 19470 } 19471 } 19472 if ( trueness_len && !compareResult ) { 19473 return false; 19474 } 19475 19476 for ( i = 0; i < falseness_len; i += 1 ) { 19477 i_cache = falseness[ i ]; 19478 isEqual = _equalsJSON( jsonpointer.get( obj, i_cache.path ), i_cache.value ); 19479 19480 if ( isEqual && !i_cache.optional || isEqual && i_cache.optional ) { 19481 return false; 19482 } 19483 } 19484 19485 } 19486 return true; 19487 }, 19488 19489 // Utility function to compare two JSON value 19490 _equalsJSON = function( a, b ) { 19491 switch ( typeof a ) { 19492 case "undefined": 19493 return false; 19494 case "boolean": 19495 case "string": 19496 case "number": 19497 return a === b; 19498 case "object": 19499 if ( a === null ) { 19500 return b === null; 19501 } 19502 var i, l; 19503 if ( Array.isArray( a ) ) { 19504 if ( Array.isArray( b ) || a.length !== b.length ) { 19505 return false; 19506 } 19507 for ( i = 0, l = a.length; i < l; i++ ) { 19508 if ( !_equalsJSON( a[ i ], b[ i ] ) ) { 19509 return false; 19510 } 19511 } 19512 return true; 19513 } 19514 var bKeys = _objectKeys( b ), 19515 bLength = bKeys.length; 19516 if ( _objectKeys( a ).length !== bLength ) { 19517 return false; 19518 } 19519 for ( i = 0; i < bLength; i++ ) { 19520 if ( !_equalsJSON( a[ i ], b[ i ] ) ) { 19521 return false; 19522 } 19523 } 19524 return true; 19525 default: 19526 return false; 19527 } 19528 }, 19529 _objectKeys = function( obj ) { 19530 var keys; 19531 if ( Array.isArray( obj ) ) { 19532 keys = new Array( obj.length ); 19533 for ( var k = 0; k < keys.length; k++ ) { 19534 keys[ k ] = "" + k; 19535 } 19536 return keys; 19537 } 19538 if ( Object.keys ) { 19539 return Object.keys( obj ); 19540 } 19541 keys = []; 19542 for ( var i in obj ) { 19543 if ( Object.prototype.hasOwnProperty.call( obj, i ) ) { 19544 keys.push( i ); 19545 } 19546 } 19547 return keys; 19548 }, 19549 19550 // Utility function to apply a JSON patch 19551 applyPatch = function( tree, op, path, value ) { 19552 jsonpatch.apply( tree, [ 19553 { op: op, path: path, value: value } 19554 ] ); 19555 }, 19556 19557 // Create series of patches for filtering 19558 getPatchesToFilter = function( JSONsource, filterPath, filterTrueness, filterFalseness ) { 19559 var filterObj, 19560 i, i_len; 19561 19562 if ( !Array.isArray( filterTrueness ) ) { 19563 filterTrueness = [ filterTrueness ]; 19564 } 19565 if ( !Array.isArray( filterFalseness ) ) { 19566 filterFalseness = [ filterFalseness ]; 19567 } 19568 19569 filterObj = jsonpointer.get( JSONsource, filterPath ); 19570 if ( Array.isArray( filterObj ) ) { 19571 i_len = filterObj.length - 1; 19572 for ( i = i_len; i !== -1; i -= 1 ) { 19573 if ( !filterPassJSON( filterObj[ i ], filterTrueness, filterFalseness ) ) { 19574 jsonpatch.apply( JSONsource, [ { op: "remove", path: filterPath + "/" + i } ] ); 19575 } 19576 } 19577 } 19578 return JSONsource; 19579 }; 19580 19581$document.on( "json-failed.wb", selector, function( event ) { 19582 var elm = event.target, 19583 $elm; 19584 19585 if ( elm === event.currentTarget ) { 19586 $elm = $( elm ); 19587 $elm.addClass( jsonFailedClass ); 19588 19589 // Identify that initialization has completed 19590 wb.ready( $elm, componentName ); 19591 } 19592} ); 19593 19594$document.on( "json-fetched.wb", selector, function( event ) { 19595 var elm = event.target, 19596 $elm = $( elm ), 19597 settings, 19598 fetchedOpts = event.fetch.fetchedOpts, 19599 isReloading = elm.hasAttribute( reloadFlag ), 19600 dsName, 19601 JSONresponse = event.fetch.response, 19602 isArrayResponse, 19603 resultSet, 19604 i, i_len, i_cache, backlog, selector, 19605 objIterator, savingPathSplit, 19606 patches, filterTrueness, filterFalseness, filterPath, extractor; 19607 19608 if ( elm === event.currentTarget ) { 19609 settings = wb.getData( $elm, componentName ); 19610 19611 // Is the fetched JSON need to be wrap in another plain object 19612 if ( fetchedOpts && fetchedOpts.savingPath ) { 19613 savingPathSplit = fetchedOpts.savingPath.split( "/" ); 19614 19615 for ( i = savingPathSplit.length - 1; i > 0; i-- ) { 19616 if ( !savingPathSplit[ i ] ) { 19617 continue; 19618 } 19619 objIterator = {}; 19620 objIterator[ savingPathSplit[ i ] ] = JSONresponse; 19621 JSONresponse = objIterator; 19622 } 19623 } 19624 19625 // Determine if the response is an array 19626 isArrayResponse = Array.isArray( JSONresponse ); 19627 19628 // Ensure the response is an independent clone 19629 if ( isArrayResponse ) { 19630 JSONresponse = $.extend( true, [], JSONresponse ); 19631 } else { 19632 JSONresponse = $.extend( true, {}, JSONresponse ); 19633 } 19634 19635 dsName = settings.name; 19636 dsFetching[ dsName ]--; 19637 19638 // Ensure that we do have fetched and merged all urls everything before to move ahead 19639 dsFetchIsArray[ dsName ] = dsFetchIsArray[ dsName ] ? dsFetchIsArray[ dsName ] : isArrayResponse; 19640 19641 if ( dsFetchIsArray[ dsName ] !== isArrayResponse ) { 19642 throw "Can't merge, incompatible JSON type (array vs object)"; 19643 } 19644 19645 if ( !dsFetchMerged[ dsName ] ) { 19646 dsFetchMerged[ dsName ] = JSONresponse; 19647 } else if ( dsFetchMerged[ dsName ] && isArrayResponse ) { 19648 dsFetchMerged[ dsName ] = dsFetchMerged[ dsName ].concat( JSONresponse ); 19649 } else { 19650 dsFetchMerged[ dsName ] = $.extend( dsFetchMerged[ dsName ], JSONresponse ); 19651 } 19652 19653 // Quit and wait for the next fetch 19654 if ( !isReloading && dsFetching[ dsName ] ) { 19655 return; 19656 } 19657 19658 JSONresponse = dsFetchMerged[ dsName ]; 19659 19660 extractor = settings.extractor; 19661 if ( extractor ) { 19662 if ( !Array.isArray( extractor ) ) { 19663 extractor = [ extractor ]; 19664 } 19665 JSONresponse = $.extend( JSONresponse, extractData( extractor ) ); 19666 19667 } 19668 19669 dsName = "[" + dsName + "]"; 19670 patches = settings.patches || []; 19671 filterPath = settings.fpath; 19672 filterTrueness = settings.filter || []; 19673 filterFalseness = settings.filternot || []; 19674 19675 if ( !Array.isArray( patches ) ) { 19676 patches = [ patches ]; 19677 } 19678 19679 // Apply a filtering 19680 if ( filterPath ) { 19681 JSONresponse = getPatchesToFilter( JSONresponse, filterPath, filterTrueness, filterFalseness ); 19682 } 19683 19684 // Apply the wraproot 19685 if ( settings.wraproot ) { 19686 i_cache = { }; 19687 i_cache[ settings.wraproot ] = JSONresponse; 19688 JSONresponse = i_cache; 19689 } 19690 19691 // Apply the patches 19692 if ( patches.length ) { 19693 jsonpatch.apply( JSONresponse, patches ); 19694 } 19695 19696 if ( settings.debug ) { 19697 debugPrintOut( $elm, "initEvent", JSONresponse, patches ); 19698 } 19699 19700 try { 19701 datasetCache[ dsName ] = JSONresponse; 19702 } catch ( error ) { 19703 return; 19704 } 19705 datasetCacheSettings[ dsName ] = settings; 19706 19707 if ( isReloading ) { 19708 elm.removeAttribute( reloadFlag ); 19709 i_cache = dsPostponePatches[ dsName ]; 19710 if ( i_cache ) { 19711 $elm.trigger( i_cache ); 19712 } 19713 } 19714 19715 if ( !settings.wait && dsDelayed[ dsName ] ) { 19716 backlog = dsDelayed[ dsName ]; 19717 i_len = backlog.length; 19718 for ( i = 0; i !== i_len; i += 1 ) { 19719 i_cache = backlog[ i ]; 19720 selector = i_cache.selector; 19721 if ( selector.length ) { 19722 try { 19723 resultSet = jsonpointer.get( JSONresponse, selector ); 19724 } catch ( e ) { 19725 throw dsName + " - JSON selector not found: " + selector; 19726 } 19727 } else { 19728 resultSet = JSONresponse; 19729 } 19730 $( "#" + i_cache.callerId ).trigger( { 19731 type: "json-fetched.wb", 19732 fetch: { 19733 response: resultSet, 19734 status: "200", 19735 refId: i_cache.refId, 19736 xhr: null 19737 } 19738 }, this ); 19739 } 19740 } 19741 19742 // Identify that initialization has completed 19743 wb.ready( $elm, componentName ); 19744 } 19745} ); 19746 19747// Apply patches to a preloaded JSON data 19748$document.on( patchesEvent, selector, function( event ) { 19749 var elm = event.target, 19750 $elm = $( elm ), 19751 patches = event.patches, 19752 filterPath = event.fpath, 19753 filterTrueness = event.filter || [], 19754 filterFalseness = event.filternot || [], 19755 isCumulative = !!event.cumulative, 19756 settings, 19757 dsName, 19758 dsJSON, resultSet, 19759 delayedLst, 19760 i, i_len, i_cache, pntrSelector; 19761 19762 if ( elm === event.currentTarget && Array.isArray( patches ) ) { 19763 settings = wb.getData( $elm, componentName ); 19764 19765 if ( !settings ) { 19766 return true; 19767 } 19768 dsName = "[" + settings.name + "]"; 19769 19770 // Check if the patches need to be hold until the next json-fetch event 19771 if ( elm.hasAttribute( reloadFlag ) ) { 19772 dsPostponePatches[ dsName ] = event; 19773 return true; 19774 } 19775 19776 if ( !dsDelayed[ dsName ] ) { 19777 throw "Applying patched on undefined dataset name: " + dsName; 19778 } 19779 19780 dsJSON = datasetCache[ dsName ]; 19781 if ( !isCumulative ) { 19782 dsJSON = $.extend( true, ( Array.isArray( dsJSON ) ? [] : {} ), dsJSON ); 19783 } 19784 19785 // Apply a filtering 19786 if ( filterPath ) { 19787 dsJSON = getPatchesToFilter( dsJSON, filterPath, filterTrueness, filterFalseness ); 19788 } 19789 19790 jsonpatch.apply( dsJSON, patches ); 19791 19792 if ( settings.debug ) { 19793 debugPrintOut( $elm, "patchesEvent", dsJSON, patches ); 19794 } 19795 19796 delayedLst = dsDelayed[ dsName ]; 19797 i_len = delayedLst.length; 19798 for ( i = 0; i !== i_len; i += 1 ) { 19799 i_cache = delayedLst[ i ]; 19800 pntrSelector = i_cache.selector; 19801 if ( pntrSelector.length ) { 19802 try { 19803 resultSet = jsonpointer.get( dsJSON, pntrSelector ); 19804 } catch ( e ) { 19805 throw dsName + " - JSON selector not found: " + pntrSelector; 19806 } 19807 } else { 19808 resultSet = dsJSON; 19809 } 19810 $( "#" + i_cache.callerId ).trigger( { 19811 type: "json-fetched.wb", 19812 fetch: { 19813 response: resultSet, 19814 status: "200", 19815 refId: i_cache.refId, 19816 xhr: null 19817 } 19818 }, this ); 19819 } 19820 } 19821} ); 19822 19823 19824// Used by the JSON-fetch plugin for when trying fetching a resource that is mapped a dataset name 19825$document.on( postponeEvent, function( event ) { 19826 var jsonPostpone = event.postpone, 19827 dsName = jsonPostpone.dsname, 19828 callerId = jsonPostpone.callerId, 19829 refId = jsonPostpone.refId, 19830 selector = jsonPostpone.selector, 19831 resultSet; 19832 19833 if ( !dsDelayed[ dsName ] ) { 19834 dsDelayed[ dsName ] = [ ]; 19835 } 19836 19837 // Add to the delayed updates list 19838 dsDelayed[ dsName ].push( { 19839 "callerId": callerId, 19840 "refId": refId, 19841 "selector": selector 19842 } ); 19843 19844 // Send the data if the dataset is ready? 19845 if ( datasetCache[ dsName ] && !datasetCacheSettings[ dsName ].wait ) { 19846 resultSet = datasetCache[ dsName ]; 19847 if ( selector.length ) { 19848 try { 19849 resultSet = jsonpointer.get( resultSet, selector ); 19850 } catch ( e ) { 19851 throw dsName + " - JSON selector not found: " + selector; 19852 } 19853 } 19854 $( "#" + callerId ).trigger( { 19855 type: "json-fetched.wb", 19856 fetch: { 19857 response: resultSet, 19858 status: "200", 19859 refId: refId, 19860 xhr: null 19861 } 19862 }, this ); 19863 } 19864 19865} ); 19866 19867/* 19868 * Integration with wb-fieldflow 19869 * 19870 */ 19871function pushData( $elm, prop, data, reset ) { 19872 var dtCache = $elm.data( prop ); 19873 if ( !dtCache || reset ) { 19874 dtCache = []; 19875 } 19876 dtCache.push( data ); 19877 return $elm.data( prop, dtCache ); 19878} 19879 19880// Fieldflow "op" action 19881$document.on( "op.action.wb-fieldflow", ".wb-fieldflow", function( event, data ) { 19882 19883 if ( !data.op ) { 19884 return; 19885 } 19886 19887 // Postpone the event for form submission 19888 data.preventSubmit = true; 19889 pushData( $( data.provEvt ), "wb-fieldflow-submit", data ); 19890} ); 19891 19892// Fieldflow "op" submit 19893$document.on( "op.submit.wb-fieldflow", ".wb-fieldflow", function( event, data ) { 19894 19895 // Get the hbs Plugin 19896 var op = data.op, 19897 source = data.source, 19898 ops; 19899 19900 if ( !op ) { 19901 return true; 19902 } 19903 19904 if ( !Array.isArray( op ) ) { 19905 ops = []; 19906 ops.push( op ); 19907 } else { 19908 ops = op; 19909 } 19910 19911 $( source ).trigger( { 19912 type: "patches.wb-jsonmanager", 19913 patches: ops 19914 } ); 19915} ); 19916 19917// Bind the init event of the plugin 19918$document.on( "timerpoke.wb " + initEvent, selector, init ); 19919 19920 19921// Add the timer poke to initialize the plugin
19922wb.add( selector ); 19923 19924} )( jQuery, window, wb ); 19925 19926/** 19927 * @title WET-BOEW wb-postback 19928 * @overview This plugin implements AJAX request for form data to submit on same page without refresh 19929 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 19930 * @author @masterbee, @namjohn920, @GormFrank 19931 **/ 19932( function( $, wb ) { 19933"use strict"; 19934 19935var $document = wb.doc, 19936 componentName = "wb-postback", 19937 selector = "." + componentName, 19938 initEvent = "wb-init" + selector, 19939 failEvent = "fail" + selector, 19940 successEvent = "success" + selector, 19941 defaults = {}, 19942 19943 init = function( event ) { 19944 var elm = wb.init( event, componentName, selector ); 19945 19946 if ( elm ) { 19947 var $elm = $( elm ), 19948 settings = $.extend( 19949 true, 19950 {}, 19951 defaults, 19952 wb.getData( $elm, componentName ) 19953 ), 19954 multiple = typeof $elm.data( componentName + "-multiple" ) !== "undefined", 19955 classToggle = settings.toggle || "hide", 19956 selectorSuccess = settings.success, 19957 selectorFailure = settings.failure || selectorSuccess; 19958 const attrBlocked = "data-wb-blocked", 19959 attrPIIBlocked = "data-wb-pii-blocked", 19960 attrSending = "data-wb-sending"; 19961 19962 elm.addEventListener( "submit", function( e ) { 19963 19964 // Prevent regular form submit 19965 e.preventDefault(); 19966 19967 //Check if the form use the validation plugin 19968 if ( elm.parentElement.classList.contains( "wb-frmvld" ) ) { 19969 19970 // Block invalid forms and allow valid ones 19971 if ( !$elm.valid() ) { 19972 $( this ).attr( attrBlocked, "true" ); 19973 } else { 19974 $( this ).removeAttr( attrBlocked ); 19975 } 19976 } 19977 19978 // Submit the form unless it's blocked or currently being sent 19979 if ( !$( this ).attr( attrBlocked ) && !$( this ).attr( attrSending ) && !$( this ).attr( attrPIIBlocked ) ) { 19980 $elm.trigger( componentName + ".submit", e.submitter ); 19981 } 19982 } ); 19983 19984 $elm.on( componentName + ".submit", function( event, submitter ) { 19985 var data = $elm.serializeArray(), 19986 $selectorSuccess = $( selectorSuccess ), 19987 $selectorFailure = $( selectorFailure ); 19988 19989 // Indicate that the form is currently being sent (to prevent multiple submissions in parallel) 19990 $( this ).attr( attrSending, true ); 19991 19992 // If the submit button contains a variable, add it to the form's parameters 19993 // Note: Submitting a form via Enter will act as if the FIRST submit button was pressed. Therefore, that button's variable will be added (as opposed to nothing). This is in line with default form submission behaviour. 19994 if ( submitter && submitter.name ) { 19995 data.push( { name: submitter.name, value: submitter.value } ); 19996 } 19997 19998 // Hide feedback messages 19999 $selectorFailure.addClass( classToggle ); 20000 $selectorSuccess.addClass( classToggle ); 20001 20002 // Send the form through ajax and ignore the response body. 20003 $.ajax( { 20004 type: this.method, 20005 url: this.action, 20006 data: $.param( data ) 20007 } ) 20008 .done( function() { 20009 $elm.trigger( successEvent ); 20010 $selectorSuccess.removeClass( classToggle ); 20011 } ) 20012 .fail( function( response ) { 20013 $elm.trigger( failEvent, response ); 20014 $selectorFailure.removeClass( classToggle ); 20015 } ) 20016 .always( function() { 20017 20018 // Hide the form unless multiple submits are allowed 20019 if ( !multiple ) { 20020 $elm.addClass( classToggle ); 20021 } 20022 20023 // Remove the sending indicator now that submission is fully complete (i.e. HTTP response code has been received) 20024 $elm.removeAttr( attrSending ); 20025 } ); 20026 } ); 20027 20028 wb.ready( $( elm ), componentName ); 20029 } 20030 }; 20031 20032// Bind the init event of the plugin 20033$document.on( "timerpoke.wb " + initEvent, selector, init ); 20034 20035// Add the timer poke to initialize the plugin 20036wb.add( selector ); 20037 20038} )( jQuery, wb ); 20039 20040/** 20041 * @title WET-BOEW Randomize 20042 * @overview This plugin randomly picks one of the child component to be shown on the browser. 20043 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 20044 * @author @masterbee @namjohn920 20045 */ 20046( function( $, window, wb ) { 20047"use strict"; 20048 20049var $document = wb.doc, 20050 componentName = "wb-randomize", 20051 selector = "[data-wb-randomize]", 20052 initEvent = "wb-init" + selector, 20053 defaults = {}, 20054 20055 init = function( event ) { 20056 var elm = wb.init( event, componentName, selector ), 20057 $elm, settings, $selectedElm, valuesList; 20058 20059 if ( elm ) { 20060 $elm = $( elm ); 20061 settings = $.extend( 20062 true, 20063 {}, 20064 defaults, 20065 window[ componentName ], 20066 wb.getData( $elm, componentName ) 20067 ); 20068 20069 if ( settings.attribute ) { 20070 if ( settings.values && Array.isArray( settings.values ) ) { 20071 valuesList = settings.values; 20072 shuffleArray( valuesList ); 20073 elm.setAttribute( settings.attribute, valuesList[ 0 ] ); 20074 } else { 20075 throw componentName + ": You must define the property \"values\" to an array of str
20075ings when \"attribute\" property is defined."; 20076 } 20077 } else { 20078 $selectedElm = settings.selector ? $( settings.selector, $elm ) : $elm.children(); 20079 20080 if ( !$selectedElm.length ) { 20081 throw componentName + " selector setting is invalid or no children"; 20082 } 20083 20084 if ( settings.shuffle ) { 20085 $selectedElm = wb.shuffleDOM( $selectedElm ); 20086 } 20087 20088 if ( settings.toggle ) { 20089 $selectedElm = wb.pickElements( $selectedElm, settings.number ); 20090 $selectedElm.toggleClass( settings.toggle ); 20091 } 20092 } 20093 20094 wb.ready( $elm, componentName ); 20095 } 20096 }, 20097 20098 shuffleArray = function( array ) { 20099 for ( let i = array.length - 1; i > 0; i-- ) { 20100 const j = Math.floor( Math.random() * ( i + 1 ) ); 20101 [ array[ i ], array[ j ] ] = [ array[ j ], array[ i ] ]; 20102 } 20103 }; 20104 20105// Bind the init event of the plugin 20106$document.on( "timerpoke.wb " + initEvent, selector, init ); 20107 20108// Add the timer poke to initialize the plugin 20109wb.add( selector ); 20110 20111} )( jQuery, window, wb ); 20112 20113/** 20114 * Web Experience Toolkit (WET) / Boîte à outils de l'expérience Web (BOEW) 20115 * @title Zebra 20116 * @overview Apply Zebra stripping on a complex data table and simulate column hovering effect 20117 * @license wet-boew.github.io/wet-boew/License-en.html / wet-boew.github.io/wet-boew/Licence-fr.html 20118 * @author @duboisp 20119 * 20120 */ 20121( function( $, window, document, wb ) { 20122"use strict"; 20123 20124/** 20125 * Variable and function definitions. 20126 * These are global to the plugin - meaning that they will be initialized once per page, 20127 * not once per instance of plugin on the page. So, this is a good place to define 20128 * variables that are common to all instances of the plugin on a page. 20129 */ 20130var componentName = "wb-zebra", 20131 selector = "." + componentName, 20132 hoverColClass = componentName + "-col-hover", 20133 selectorHoverCol = "." + hoverColClass + " td, " + hoverColClass + " th", 20134 initEvent = "wb-init" + selector, 20135 tableParsingEvent = "passiveparse.wb-tableparser", 20136 tableParsingCompleteEvent = "parsecomplete.wb-tableparser", 20137 $document = wb.doc, 20138 idCount = 0, 20139 i18n, i18nText, 20140 20141 /** 20142 * Main Entry function to apply the complex zebra stripping 20143 * @method zebraTable 20144 * @param {jQuery DOM element} $elm table element use to apply complex zebra stripping 20145 */ 20146 zebraTable = function( $elm ) { 20147 var i, iLength, tblGroup, 20148 20149 // Cache the table parsed results 20150 tblparser = $elm.data().tblparser; 20151 20152 function addCellClass( arr, className ) { 20153 var i, iLength; 20154 20155 for ( i = 0, iLength = arr.length; i !== iLength; i += 1 ) { 20156 $( arr[ i ].elem ).addClass( className ); 20157 } 20158 } 20159 20160 // Key Cell 20161 if ( tblparser.keycell ) { 20162 addCellClass( tblparser.keycell, "wb-cell-key" ); 20163 } 20164 20165 // Description Cell 20166 if ( tblparser.desccell ) { 20167 addCellClass( tblparser.desccell, "wb-cell-desc" ); 20168 } 20169 20170 // Layout Cell 20171 if ( tblparser.layoutCell ) { 20172 addCellClass( tblparser.layoutCell, "wb-cell-layout" ); 20173 } 20174 20175 // Summary Row Group 20176 if ( tblparser.lstrowgroup ) { 20177 for ( i = 0, iLength = tblparser.lstrowgroup.length; i !== iLength; i += 1 ) { 20178 tblGroup = tblparser.lstrowgroup[ i ]; 20179 20180 // Add a class to the row 20181 if ( tblGroup.type === 3 || tblGroup.row[ 0 ].type === 3 ) { 20182 $( tblGroup.elem ).addClass( "wb-group-summary" ); 20183 } 20184 } 20185 } 20186 20187 // Summary Column Group 20188 if ( tblparser.colgroup ) { 20189 for ( i = 0, iLength = tblparser.colgroup.length; i !== iLength; i += 1 ) { 20190 tblGroup = tblparser.colgroup[ i ]; 20191 20192 // Add a class to the row 20193 if ( tblGroup.type === 3 ) { 20194 $( tblGroup.elem ).addClass( "wb-group-summary" ); 20195 } 20196 } 20197 } 20198 20199 // Identify that initialization has completed 20200 wb.ready( $elm, componentName ); 20201 }, 20202 20203 /** 20204 * @method init 20205 * @param {jQuery Event} event Event that triggered the function call 20206 */ 20207 init = function( event ) { 20208 20209 // Start initialization 20210 // returns DOM object = proceed with init 20211 // returns undefined = do not proceed with init (e.g., already initialized) 20212 var elm = wb.init( event, componentName, selector ), 20213 deps = [ 20214 "site!deps/tableparser" + wb.getMode() + ".js" 20215 ], 20216 elmId; 20217 20218 if ( elm ) { 20219 elmId = elm.id; 20220 20221 // Ensure there is a unique id on the element 20222 if ( !elmId ) { 20223 elmId = componentName + "-id-" + idCount; 20224 idCount += 1; 20225 elm.id = elmId; 20226 } 20227 20228 // Only initialize the i18nText once 20229 if ( !i18nText ) { 20230 i18n = wb.i18n; 20231 i18nText = { 20232 tableMention: i18n( "hyphen" ) + i18n( "tbl-txt" ), 20233 tableFollowing: i18n( "hyphen" ) + i18n( "tbl-dtls" ) 20234 }; 20235 } 20236 20237 // Load the required dependencies 20238 Modernizr.load( { 20239 20240 // For loading multiple dependencies 20241 load: deps, 20242 complete: function() { 20243 20244 // Let's parse the table 20245 $( "#" + elmId ).trigger( tableParsingEvent ); 20246 } 20247 } ); 20248 } 20249 }; 20250 20251// Bind the init event of the plugin 20252$document.on( "timerpoke.wb " + initEvent + " " + tableParsingCompleteEvent, selector, function( event ) { 20253 var eventTarget = event.target; 20254 20255 switch ( event.type ) { 20256 20257 /* 20258 * Init 20259 */ 20260 case "timerpoke": 20261 case "wb-init": 20262 init( event ); 20263 break; 20264 20265 /* 20266 * Data table parsed 20267 */ 20268 case "parsecomplete": 20269 if ( event.currentTarget === eventTarget ) { 20270 zebraTable( $( eventTarget ) ); 20271 } 20272 break; 20273 } 20274 20275 /* 20276 * Since we are working with events we want to ensure that we are being passive about our control, 20277 * so returning true allows for events to always continue 20278 */ 20279 return true; 20280} ); 20281 20282// Applying the hover, Simulate Column Hovering Effect 20283$document.on( "mouseenter focusin", selectorHoverCol, function( event ) { 20284 var tblparserCell = $( event.currentTarget ).data().tblparser; 20285 20286 if ( tblparserCell.col && tblparserCell.col.elem ) { 20287 $( tblparserCell.col.elem ).addClass( "table-hover" ); 20288 } 20289} ); 20290 20291// Removing the hover, Simulate Column Hovering Effect 20292$document.on( "mouseleave focusout", selectorHoverCol, function( event ) { 20293 var tblparserCell = $( event.currentTarget ).data().tblparser; 20294 20295 if ( tblparserCell.col && tblparserCell.col.elem ) { 20296 $( tblparserCell.col.elem ).removeClass( "table-hover" ); 20297 } 20298} ); 20299 20300// Add the timer poke to initialize the plugin
20301wb.add( selector ); 20302 20303} )( jQuery, window, document, wb );
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.