[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
1 var top = 'top'; 2 var bottom = 'bottom'; 3 var right = 'right'; 4 var left = 'left'; 5 var auto = 'auto'; 6 var basePlacements = [top, bottom, right, left]; 7 var start = 'start'; 8 var end = 'end'; 9 var clippingParents = 'clippingParents'; 10 var viewport = 'viewport'; 11 var popper = 'popper'; 12 var reference = 'reference'; 13 var variationPlacements = /*#__PURE__*/basePlacements.reduce(function (acc, placement) { 14 return acc.concat([placement + "-" + start, placement + "-" + end]); 15 }, []); 16 var placements = /*#__PURE__*/[].concat(basePlacements, [auto]).reduce(function (acc, placement) { 17 return acc.concat([placement, placement + "-" + start, placement + "-" + end]); 18 }, []); // modifiers that need to read the DOM 19 20 var beforeRead = 'beforeRead'; 21 var read = 'read'; 22 var afterRead = 'afterRead'; // pure-logic modifiers 23 24 var beforeMain = 'beforeMain'; 25 var main = 'main'; 26 var afterMain = 'afterMain'; // modifier with the purpose to write to the DOM (or write into a framework state) 27 28 var beforeWrite = 'beforeWrite'; 29 var write = 'write'; 30 var afterWrite = 'afterWrite'; 31 var modifierPhases = [beforeRead, read, afterRead, beforeMain, main, afterMain, beforeWrite, write, afterWrite]; 32 33 function getNodeName(element) { 34 return element ? (element.nodeName || '').toLowerCase() : null; 35 } 36 37 function getWindow(node) { 38 if (node == null) { 39 return window; 40 } 41 42 if (node.toString() !== '[object Window]') { 43 var ownerDocument = node.ownerDocument; 44 return ownerDocument ? ownerDocument.defaultView || window : window; 45 } 46 47 return node; 48 } 49 50 function isElement$1(node) { 51 var OwnElement = getWindow(node).Element; 52 return node instanceof OwnElement || node instanceof Element; 53 } 54 55 function isHTMLElement(node) { 56 var OwnElement = getWindow(node).HTMLElement; 57 return node instanceof OwnElement || node instanceof HTMLElement; 58 } 59 60 function isShadowRoot(node) { 61 // IE 11 has no ShadowRoot 62 if (typeof ShadowRoot === 'undefined') { 63 return false; 64 } 65 66 var OwnElement = getWindow(node).ShadowRoot; 67 return node instanceof OwnElement || node instanceof ShadowRoot; 68 } // and applies them to the HTMLElements such as popper and arrow 69 70 71 function applyStyles(_ref) { 72 var state = _ref.state; 73 Object.keys(state.elements).forEach(function (name) { 74 var style = state.styles[name] || {}; 75 var attributes = state.attributes[name] || {}; 76 var element = state.elements[name]; // arrow is optional + virtual elements 77 78 if (!isHTMLElement(element) || !getNodeName(element)) { 79 return; 80 } // Flow doesn't support to extend this property, but it's the most 81 // effective way to apply styles to an HTMLElement 82 // $FlowFixMe[cannot-write] 83 84 85 Object.assign(element.style, style); 86 Object.keys(attributes).forEach(function (name) { 87 var value = attributes[name]; 88 89 if (value === false) { 90 element.removeAttribute(name); 91 } else { 92 element.setAttribute(name, value === true ? '' : value); 93 } 94 }); 95 }); 96 } 97 98 function effect$2(_ref2) { 99 var state = _ref2.state; 100 var initialStyles = { 101 popper: { 102 position: state.options.strategy, 103 left: '0', 104 top: '0', 105 margin: '0' 106 }, 107 arrow: { 108 position: 'absolute' 109 }, 110 reference: {} 111 }; 112 Object.assign(state.elements.popper.style, initialStyles.popper); 113 state.styles = initialStyles; 114 115 if (state.elements.arrow) { 116 Object.assign(state.elements.arrow.style, initialStyles.arrow); 117 } 118 119 return function () { 120 Object.keys(state.elements).forEach(function (name) { 121 var element = state.elements[name]; 122 var attributes = state.attributes[name] || {}; 123 var styleProperties = Object.keys(state.styles.hasOwnProperty(name) ? state.styles[name] : initialStyles[name]); // Set all values to an empty string to unset them 124 125 var style = styleProperties.reduce(function (style, property) { 126 style[property] = ''; 127 return style; 128 }, {}); // arrow is optional + virtual elements 129 130 if (!isHTMLElement(element) || !getNodeName(element)) { 131 return; 132 } 133 134 Object.assign(element.style, style); 135 Object.keys(attributes).forEach(function (attribute) { 136 element.removeAttribute(attribute); 137 }); 138 }); 139 }; 140 } // eslint-disable-next-line import/no-unused-modules 141 142 143 var applyStyles$1 = { 144 name: 'applyStyles', 145 enabled: true, 146 phase: 'write', 147 fn: applyStyles, 148 effect: effect$2, 149 requires: ['computeStyles'] 150 }; 151 152 function getBasePlacement$1(placement) { 153 return placement.split('-')[0]; 154 } 155 156 var max = Math.max; 157 var min = Math.min; 158 var round = Math.round; 159 160 function getBoundingClientRect(element, includeScale) { 161 if (includeScale === void 0) { 162 includeScale = false; 163 } 164 165 var rect = element.getBoundingClientRect(); 166 var scaleX = 1; 167 var scaleY = 1; 168 169 if (isHTMLElement(element) && includeScale) { 170 var offsetHeight = element.offsetHeight; 171 var offsetWidth = element.offsetWidth; // Do not attempt to divide by 0, otherwise we get `Infinity` as scale 172 // Fallback to 1 in case both values are `0` 173 174 if (offsetWidth > 0) { 175 scaleX = round(rect.width) / offsetWidth || 1; 176 } 177 178 if (offsetHeight > 0) { 179 scaleY = round(rect.height) / offsetHeight || 1; 180 } 181 } 182 183 return { 184 width: rect.width / scaleX, 185 height: rect.height / scaleY, 186 top: rect.top / scaleY, 187 right: rect.right / scaleX, 188 bottom: rect.bottom / scaleY, 189 left: rect.left / scaleX, 190 x: rect.left / scaleX, 191 y: rect.top / scaleY 192 }; 193 } // means it doesn't take into account transforms. 194 195 196 function getLayoutRect(element) { 197 var clientRect = getBoundingClientRect(element); // Use the clientRect sizes if it's not been transformed. 198 // Fixes https://github.com/popperjs/popper-core/issues/1223 199 200 var width = element.offsetWidth; 201 var height = element.offsetHeight; 202 203 if (Math.abs(clientRect.width - width) <= 1) { 204 width = clientRect.width; 205 } 206 207 if (Math.abs(clientRect.height - height) <= 1) { 208 height = clientRect.height; 209 } 210 211 return { 212 x: element.offsetLeft, 213 y: element.offsetTop, 214 width: width, 215 height: height 216 }; 217 } 218 219 function contains(parent, child) { 220 var rootNode = child.getRootNode && child.getRootNode(); // First, attempt with faster native method 221 222 if (parent.contains(child)) { 223 return true; 224 } // then fallback to custom implementation with Shadow DOM support 225 else if (rootNode && isShadowRoot(rootNode)) { 226 var next = child; 227 228 do { 229 if (next && parent.isSameNode(next)) { 230 return true; 231 } // $FlowFixMe[prop-missing]: need a better way to handle this... 232 233 234 next = next.parentNode || next.host; 235 } while (next); 236 } // Give up, the result is false 237 238 239 return false; 240 } 241 242 function getComputedStyle$1(element) { 243 return getWindow(element).getComputedStyle(element); 244 } 245 246 function isTableElement(element) { 247 return ['table', 'td', 'th'].indexOf(getNodeName(element)) >= 0; 248 } 249 250 function getDocumentElement(element) { 251 // $FlowFixMe[incompatible-return]: assume body is always available 252 return ((isElement$1(element) ? element.ownerDocument : // $FlowFixMe[prop-missing] 253 element.document) || window.document).documentElement; 254 } 255 256 function getParentNode(element) { 257 if (getNodeName(element) === 'html') { 258 return element; 259 } 260 261 return (// this is a quicker (but less type safe) way to save quite some bytes from the bundle 262 // $FlowFixMe[incompatible-return] 263 // $FlowFixMe[prop-missing] 264 element.assignedSlot || // step into the shadow DOM of the parent of a slotted node 265 element.parentNode || ( // DOM Element detected 266 isShadowRoot(element) ? element.host : null) || // ShadowRoot detected 267 // $FlowFixMe[incompatible-call]: HTMLElement is a Node 268 getDocumentElement(element) // fallback 269 270 ); 271 } 272 273 function getTrueOffsetParent(element) { 274 if (!isHTMLElement(element) || // https://github.com/popperjs/popper-core/issues/837 275 getComputedStyle$1(element).position === 'fixed') { 276 return null; 277 } 278 279 return element.offsetParent; 280 } // `.offsetParent` reports `null` for fixed elements, while absolute elements 281 // return the containing block 282 283 284 function getContainingBlock(element) { 285 var isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') !== -1; 286 var isIE = navigator.userAgent.indexOf('Trident') !== -1; 287 288 if (isIE && isHTMLElement(element)) { 289 // In IE 9, 10 and 11 fixed elements containing block is always established by the viewport 290 var elementCss = getComputedStyle$1(element); 291 292 if (elementCss.position === 'fixed') { 293 return null; 294 } 295 } 296 297 var currentNode = getParentNode(element); 298 299 while (isHTMLElement(currentNode) && ['html', 'body'].indexOf(getNodeName(currentNode)) < 0) { 300 var css = getComputedStyle$1(currentNode); // This is non-exhaustive but covers the most common CSS properties that 301 // create a containing block. 302 // https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#identifying_the_containing_block 303 304 if (css.transform !== 'none' || css.perspective !== 'none' || css.contain === 'paint' || ['transform', 'perspective'].indexOf(css.willChange) !== -1 || isFirefox && css.willChange === 'filter' || isFirefox && css.filter && css.filter !== 'none') { 305 return currentNode; 306 } else { 307 currentNode = currentNode.parentNode; 308 } 309 } 310 311 return null; 312 } // Gets the closest ancestor positioned element. Handles some edge cases, 313 // such as table ancestors and cross browser bugs. 314 315 316 function getOffsetParent(element) { 317 var window = getWindow(element); 318 var offsetParent = getTrueOffsetParent(element); 319 320 while (offsetParent && isTableElement(offsetParent) && getComputedStyle$1(offsetParent).position === 'static') { 321 offsetParent = getTrueOffsetParent(offsetParent); 322 } 323 324 if (offsetParent && (getNodeName(offsetParent) === 'html' || getNodeName(offsetParent) === 'body' && getComputedStyle$1(offsetParent).position === 'static')) { 325 return window; 326 } 327 328 return offsetParent || getContainingBlock(element) || window; 329 } 330 331 function getMainAxisFromPlacement(placement) { 332 return ['top', 'bottom'].indexOf(placement) >= 0 ? 'x' : 'y'; 333 } 334 335 function within(min$1, value, max$1) { 336 return max(min$1, min(value, max$1)); 337 } 338 339 function withinMaxClamp(min, value, max) { 340 var v = within(min, value, max); 341 return v > max ? max : v; 342 } 343 344 function getFreshSideObject() { 345 return { 346 top: 0, 347 right: 0, 348 bottom: 0, 349 left: 0 350 }; 351 } 352 353 function mergePaddingObject(paddingObject) { 354 return Object.assign({}, getFreshSideObject(), paddingObject); 355 } 356 357 function expandToHashMap(value, keys) { 358 return keys.reduce(function (hashMap, key) { 359 hashMap[key] = value; 360 return hashMap; 361 }, {}); 362 } 363 364 var toPaddingObject = function toPaddingObject(padding, state) { 365 padding = typeof padding === 'function' ? padding(Object.assign({}, state.rects, { 366 placement: state.placement 367 })) : padding; 368 return mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); 369 }; 370 371 function arrow(_ref) { 372 var _state$modifiersData$; 373 374 var state = _ref.state, 375 name = _ref.name, 376 options = _ref.options; 377 var arrowElement = state.elements.arrow; 378 var popperOffsets = state.modifiersData.popperOffsets; 379 var basePlacement = getBasePlacement$1(state.placement); 380 var axis = getMainAxisFromPlacement(basePlacement); 381 var isVertical = [left, right].indexOf(basePlacement) >= 0; 382 var len = isVertical ? 'height' : 'width'; 383 384 if (!arrowElement || !popperOffsets) { 385 return; 386 } 387 388 var paddingObject = toPaddingObject(options.padding, state); 389 var arrowRect = getLayoutRect(arrowElement); 390 var minProp = axis === 'y' ? top : left; 391 var maxProp = axis === 'y' ? bottom : right; 392 var endDiff = state.rects.reference[len] + state.rects.reference[axis] - popperOffsets[axis] - state.rects.popper[len]; 393 var startDiff = popperOffsets[axis] - state.rects.reference[axis]; 394 var arrowOffsetParent = getOffsetParent(arrowElement); 395 var clientSize = arrowOffsetParent ? axis === 'y' ? arrowOffsetParent.clientHeight || 0 : arrowOffsetParent.clientWidth || 0 : 0; 396 var centerToReference = endDiff / 2 - startDiff / 2; // Make sure the arrow doesn't overflow the popper if the center point is 397 // outside of the popper bounds 398 399 var min = paddingObject[minProp]; 400 var max = clientSize - arrowRect[len] - paddingObject[maxProp]; 401 var center = clientSize / 2 - arrowRect[len] / 2 + centerToReference; 402 var offset = within(min, center, max); // Prevents breaking syntax highlighting... 403 404 var axisProp = axis; 405 state.modifiersData[name] = (_state$modifiersData$ = {}, _state$modifiersData$[axisProp] = offset, _state$modifiersData$.centerOffset = offset - center, _state$modifiersData$); 406 } 407 408 function effect$1(_ref2) { 409 var state = _ref2.state, 410 options = _ref2.options; 411 var _options$element = options.element, 412 arrowElement = _options$element === void 0 ? '[data-popper-arrow]' : _options$element; 413 414 if (arrowElement == null) { 415 return; 416 } // CSS selector 417 418 419 if (typeof arrowElement === 'string') { 420 arrowElement = state.elements.popper.querySelector(arrowElement); 421 422 if (!arrowElement) { 423 return; 424 } 425 } 426 427 if (!contains(state.elements.popper, arrowElement)) { 428 return; 429 } 430 431 state.elements.arrow = arrowElement; 432 } // eslint-disable-next-line import/no-unused-modules 433 434 435 var arrow$1 = { 436 name: 'arrow', 437 enabled: true, 438 phase: 'main', 439 fn: arrow, 440 effect: effect$1, 441 requires: ['popperOffsets'], 442 requiresIfExists: ['preventOverflow'] 443 }; 444 445 function getVariation(placement) { 446 return placement.split('-')[1]; 447 } 448 449 var unsetSides = { 450 top: 'auto', 451 right: 'auto', 452 bottom: 'auto', 453 left: 'auto' 454 }; // Round the offsets to the nearest suitable subpixel based on the DPR. 455 // Zooming can change the DPR, but it seems to report a value that will 456 // cleanly divide the values into the appropriate subpixels. 457 458 function roundOffsetsByDPR(_ref) { 459 var x = _ref.x, 460 y = _ref.y; 461 var win = window; 462 var dpr = win.devicePixelRatio || 1; 463 return { 464 x: round(x * dpr) / dpr || 0, 465 y: round(y * dpr) / dpr || 0 466 }; 467 } 468 469 function mapToStyles(_ref2) { 470 var _Object$assign2; 471 472 var popper = _ref2.popper, 473 popperRect = _ref2.popperRect, 474 placement = _ref2.placement, 475 variation = _ref2.variation, 476 offsets = _ref2.offsets, 477 position = _ref2.position, 478 gpuAcceleration = _ref2.gpuAcceleration, 479 adaptive = _ref2.adaptive, 480 roundOffsets = _ref2.roundOffsets, 481 isFixed = _ref2.isFixed; 482 var _offsets$x = offsets.x, 483 x = _offsets$x === void 0 ? 0 : _offsets$x, 484 _offsets$y = offsets.y, 485 y = _offsets$y === void 0 ? 0 : _offsets$y; 486 487 var _ref3 = typeof roundOffsets === 'function' ? roundOffsets({ 488 x: x, 489 y: y 490 }) : { 491 x: x, 492 y: y 493 }; 494 495 x = _ref3.x; 496 y = _ref3.y; 497 var hasX = offsets.hasOwnProperty('x'); 498 var hasY = offsets.hasOwnProperty('y'); 499 var sideX = left; 500 var sideY = top; 501 var win = window; 502 503 if (adaptive) { 504 var offsetParent = getOffsetParent(popper); 505 var heightProp = 'clientHeight'; 506 var widthProp = 'clientWidth'; 507 508 if (offsetParent === getWindow(popper)) { 509 offsetParent = getDocumentElement(popper); 510 511 if (getComputedStyle$1(offsetParent).position !== 'static' && position === 'absolute') { 512 heightProp = 'scrollHeight'; 513 widthProp = 'scrollWidth'; 514 } 515 } // $FlowFixMe[incompatible-cast]: force type refinement, we compare offsetParent with window above, but Flow doesn't detect it 516 517 518 offsetParent = offsetParent; 519 520 if (placement === top || (placement === left || placement === right) && variation === end) { 521 sideY = bottom; 522 var offsetY = isFixed && win.visualViewport ? win.visualViewport.height : // $FlowFixMe[prop-missing] 523 offsetParent[heightProp]; 524 y -= offsetY - popperRect.height; 525 y *= gpuAcceleration ? 1 : -1; 526 } 527 528 if (placement === left || (placement === top || placement === bottom) && variation === end) { 529 sideX = right; 530 var offsetX = isFixed && win.visualViewport ? win.visualViewport.width : // $FlowFixMe[prop-missing] 531 offsetParent[widthProp]; 532 x -= offsetX - popperRect.width; 533 x *= gpuAcceleration ? 1 : -1; 534 } 535 } 536 537 var commonStyles = Object.assign({ 538 position: position 539 }, adaptive && unsetSides); 540 541 var _ref4 = roundOffsets === true ? roundOffsetsByDPR({ 542 x: x, 543 y: y 544 }) : { 545 x: x, 546 y: y 547 }; 548 549 x = _ref4.x; 550 y = _ref4.y; 551 552 if (gpuAcceleration) { 553 var _Object$assign; 554 555 return Object.assign({}, commonStyles, (_Object$assign = {}, _Object$assign[sideY] = hasY ? '0' : '', _Object$assign[sideX] = hasX ? '0' : '', _Object$assign.transform = (win.devicePixelRatio || 1) <= 1 ? "translate(" + x + "px, " + y + "px)" : "translate3d(" + x + "px, " + y + "px, 0)", _Object$assign)); 556 } 557 558 return Object.assign({}, commonStyles, (_Object$assign2 = {}, _Object$assign2[sideY] = hasY ? y + "px" : '', _Object$assign2[sideX] = hasX ? x + "px" : '', _Object$assign2.transform = '', _Object$assign2)); 559 } 560 561 function computeStyles(_ref5) { 562 var state = _ref5.state, 563 options = _ref5.options; 564 var _options$gpuAccelerat = options.gpuAcceleration, 565 gpuAcceleration = _options$gpuAccelerat === void 0 ? true : _options$gpuAccelerat, 566 _options$adaptive = options.adaptive, 567 adaptive = _options$adaptive === void 0 ? true : _options$adaptive, 568 _options$roundOffsets = options.roundOffsets, 569 roundOffsets = _options$roundOffsets === void 0 ? true : _options$roundOffsets; 570 var commonStyles = { 571 placement: getBasePlacement$1(state.placement), 572 variation: getVariation(state.placement), 573 popper: state.elements.popper, 574 popperRect: state.rects.popper, 575 gpuAcceleration: gpuAcceleration, 576 isFixed: state.options.strategy === 'fixed' 577 }; 578 579 if (state.modifiersData.popperOffsets != null) { 580 state.styles.popper = Object.assign({}, state.styles.popper, mapToStyles(Object.assign({}, commonStyles, { 581 offsets: state.modifiersData.popperOffsets, 582 position: state.options.strategy, 583 adaptive: adaptive, 584 roundOffsets: roundOffsets 585 }))); 586 } 587 588 if (state.modifiersData.arrow != null) { 589 state.styles.arrow = Object.assign({}, state.styles.arrow, mapToStyles(Object.assign({}, commonStyles, { 590 offsets: state.modifiersData.arrow, 591 position: 'absolute', 592 adaptive: false, 593 roundOffsets: roundOffsets 594 }))); 595 } 596 597 state.attributes.popper = Object.assign({}, state.attributes.popper, { 598 'data-popper-placement': state.placement 599 }); 600 } // eslint-disable-next-line import/no-unused-modules 601 602 603 var computeStyles$1 = { 604 name: 'computeStyles', 605 enabled: true, 606 phase: 'beforeWrite', 607 fn: computeStyles, 608 data: {} 609 }; 610 var passive = { 611 passive: true 612 }; 613 614 function effect(_ref) { 615 var state = _ref.state, 616 instance = _ref.instance, 617 options = _ref.options; 618 var _options$scroll = options.scroll, 619 scroll = _options$scroll === void 0 ? true : _options$scroll, 620 _options$resize = options.resize, 621 resize = _options$resize === void 0 ? true : _options$resize; 622 var window = getWindow(state.elements.popper); 623 var scrollParents = [].concat(state.scrollParents.reference, state.scrollParents.popper); 624 625 if (scroll) { 626 scrollParents.forEach(function (scrollParent) { 627 scrollParent.addEventListener('scroll', instance.update, passive); 628 }); 629 } 630 631 if (resize) { 632 window.addEventListener('resize', instance.update, passive); 633 } 634 635 return function () { 636 if (scroll) { 637 scrollParents.forEach(function (scrollParent) { 638 scrollParent.removeEventListener('scroll', instance.update, passive); 639 }); 640 } 641 642 if (resize) { 643 window.removeEventListener('resize', instance.update, passive); 644 } 645 }; 646 } // eslint-disable-next-line import/no-unused-modules 647 648 649 var eventListeners = { 650 name: 'eventListeners', 651 enabled: true, 652 phase: 'write', 653 fn: function fn() {}, 654 effect: effect, 655 data: {} 656 }; 657 var hash$1 = { 658 left: 'right', 659 right: 'left', 660 bottom: 'top', 661 top: 'bottom' 662 }; 663 664 function getOppositePlacement(placement) { 665 return placement.replace(/left|right|bottom|top/g, function (matched) { 666 return hash$1[matched]; 667 }); 668 } 669 670 var hash = { 671 start: 'end', 672 end: 'start' 673 }; 674 675 function getOppositeVariationPlacement(placement) { 676 return placement.replace(/start|end/g, function (matched) { 677 return hash[matched]; 678 }); 679 } 680 681 function getWindowScroll(node) { 682 var win = getWindow(node); 683 var scrollLeft = win.pageXOffset; 684 var scrollTop = win.pageYOffset; 685 return { 686 scrollLeft: scrollLeft, 687 scrollTop: scrollTop 688 }; 689 } 690 691 function getWindowScrollBarX(element) { 692 // If <html> has a CSS width greater than the viewport, then this will be 693 // incorrect for RTL. 694 // Popper 1 is broken in this case and never had a bug report so let's assume 695 // it's not an issue. I don't think anyone ever specifies width on <html> 696 // anyway. 697 // Browsers where the left scrollbar doesn't cause an issue report `0` for 698 // this (e.g. Edge 2019, IE11, Safari) 699 return getBoundingClientRect(getDocumentElement(element)).left + getWindowScroll(element).scrollLeft; 700 } 701 702 function getViewportRect(element) { 703 var win = getWindow(element); 704 var html = getDocumentElement(element); 705 var visualViewport = win.visualViewport; 706 var width = html.clientWidth; 707 var height = html.clientHeight; 708 var x = 0; 709 var y = 0; // NB: This isn't supported on iOS <= 12. If the keyboard is open, the popper 710 // can be obscured underneath it. 711 // Also, `html.clientHeight` adds the bottom bar height in Safari iOS, even 712 // if it isn't open, so if this isn't available, the popper will be detected 713 // to overflow the bottom of the screen too early. 714 715 if (visualViewport) { 716 width = visualViewport.width; 717 height = visualViewport.height; // Uses Layout Viewport (like Chrome; Safari does not currently) 718 // In Chrome, it returns a value very close to 0 (+/-) but contains rounding 719 // errors due to floating point numbers, so we need to check precision. 720 // Safari returns a number <= 0, usually < -1 when pinch-zoomed 721 // Feature detection fails in mobile emulation mode in Chrome. 722 // Math.abs(win.innerWidth / visualViewport.scale - visualViewport.width) < 723 // 0.001 724 // Fallback here: "Not Safari" userAgent 725 726 if (!/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { 727 x = visualViewport.offsetLeft; 728 y = visualViewport.offsetTop; 729 } 730 } 731 732 return { 733 width: width, 734 height: height, 735 x: x + getWindowScrollBarX(element), 736 y: y 737 }; 738 } // of the `<html>` and `<body>` rect bounds if horizontally scrollable 739 740 741 function getDocumentRect(element) { 742 var _element$ownerDocumen; 743 744 var html = getDocumentElement(element); 745 var winScroll = getWindowScroll(element); 746 var body = (_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body; 747 var width = max(html.scrollWidth, html.clientWidth, body ? body.scrollWidth : 0, body ? body.clientWidth : 0); 748 var height = max(html.scrollHeight, html.clientHeight, body ? body.scrollHeight : 0, body ? body.clientHeight : 0); 749 var x = -winScroll.scrollLeft + getWindowScrollBarX(element); 750 var y = -winScroll.scrollTop; 751 752 if (getComputedStyle$1(body || html).direction === 'rtl') { 753 x += max(html.clientWidth, body ? body.clientWidth : 0) - width; 754 } 755 756 return { 757 width: width, 758 height: height, 759 x: x, 760 y: y 761 }; 762 } 763 764 function isScrollParent(element) { 765 // Firefox wants us to check `-x` and `-y` variations as well 766 var _getComputedStyle = getComputedStyle$1(element), 767 overflow = _getComputedStyle.overflow, 768 overflowX = _getComputedStyle.overflowX, 769 overflowY = _getComputedStyle.overflowY; 770 771 return /auto|scroll|overlay|hidden/.test(overflow + overflowY + overflowX); 772 } 773 774 function getScrollParent(node) { 775 if (['html', 'body', '#document'].indexOf(getNodeName(node)) >= 0) { 776 // $FlowFixMe[incompatible-return]: assume body is always available 777 return node.ownerDocument.body; 778 } 779 780 if (isHTMLElement(node) && isScrollParent(node)) { 781 return node; 782 } 783 784 return getScrollParent(getParentNode(node)); 785 } 786 /* 787 given a DOM element, return the list of all scroll parents, up the list of ancesors 788 until we get to the top window object. This list is what we attach scroll listeners 789 to, because if any of these parent elements scroll, we'll need to re-calculate the 790 reference element's position. 791 */ 792 793 794 function listScrollParents(element, list) { 795 var _element$ownerDocumen; 796 797 if (list === void 0) { 798 list = []; 799 } 800 801 var scrollParent = getScrollParent(element); 802 var isBody = scrollParent === ((_element$ownerDocumen = element.ownerDocument) == null ? void 0 : _element$ownerDocumen.body); 803 var win = getWindow(scrollParent); 804 var target = isBody ? [win].concat(win.visualViewport || [], isScrollParent(scrollParent) ? scrollParent : []) : scrollParent; 805 var updatedList = list.concat(target); 806 return isBody ? updatedList : // $FlowFixMe[incompatible-call]: isBody tells us target will be an HTMLElement here 807 updatedList.concat(listScrollParents(getParentNode(target))); 808 } 809 810 function rectToClientRect(rect) { 811 return Object.assign({}, rect, { 812 left: rect.x, 813 top: rect.y, 814 right: rect.x + rect.width, 815 bottom: rect.y + rect.height 816 }); 817 } 818 819 function getInnerBoundingClientRect(element) { 820 var rect = getBoundingClientRect(element); 821 rect.top = rect.top + element.clientTop; 822 rect.left = rect.left + element.clientLeft; 823 rect.bottom = rect.top + element.clientHeight; 824 rect.right = rect.left + element.clientWidth; 825 rect.width = element.clientWidth; 826 rect.height = element.clientHeight; 827 rect.x = rect.left; 828 rect.y = rect.top; 829 return rect; 830 } 831 832 function getClientRectFromMixedType(element, clippingParent) { 833 return clippingParent === viewport ? rectToClientRect(getViewportRect(element)) : isElement$1(clippingParent) ? getInnerBoundingClientRect(clippingParent) : rectToClientRect(getDocumentRect(getDocumentElement(element))); 834 } // A "clipping parent" is an overflowable container with the characteristic of 835 // clipping (or hiding) overflowing elements with a position different from 836 // `initial` 837 838 839 function getClippingParents(element) { 840 var clippingParents = listScrollParents(getParentNode(element)); 841 var canEscapeClipping = ['absolute', 'fixed'].indexOf(getComputedStyle$1(element).position) >= 0; 842 var clipperElement = canEscapeClipping && isHTMLElement(element) ? getOffsetParent(element) : element; 843 844 if (!isElement$1(clipperElement)) { 845 return []; 846 } // $FlowFixMe[incompatible-return]: https://github.com/facebook/flow/issues/1414 847 848 849 return clippingParents.filter(function (clippingParent) { 850 return isElement$1(clippingParent) && contains(clippingParent, clipperElement) && getNodeName(clippingParent) !== 'body'; 851 }); 852 } // Gets the maximum area that the element is visible in due to any number of 853 // clipping parents 854 855 856 function getClippingRect(element, boundary, rootBoundary) { 857 var mainClippingParents = boundary === 'clippingParents' ? getClippingParents(element) : [].concat(boundary); 858 var clippingParents = [].concat(mainClippingParents, [rootBoundary]); 859 var firstClippingParent = clippingParents[0]; 860 var clippingRect = clippingParents.reduce(function (accRect, clippingParent) { 861 var rect = getClientRectFromMixedType(element, clippingParent); 862 accRect.top = max(rect.top, accRect.top); 863 accRect.right = min(rect.right, accRect.right); 864 accRect.bottom = min(rect.bottom, accRect.bottom); 865 accRect.left = max(rect.left, accRect.left); 866 return accRect; 867 }, getClientRectFromMixedType(element, firstClippingParent)); 868 clippingRect.width = clippingRect.right - clippingRect.left; 869 clippingRect.height = clippingRect.bottom - clippingRect.top; 870 clippingRect.x = clippingRect.left; 871 clippingRect.y = clippingRect.top; 872 return clippingRect; 873 } 874 875 function computeOffsets(_ref) { 876 var reference = _ref.reference, 877 element = _ref.element, 878 placement = _ref.placement; 879 var basePlacement = placement ? getBasePlacement$1(placement) : null; 880 var variation = placement ? getVariation(placement) : null; 881 var commonX = reference.x + reference.width / 2 - element.width / 2; 882 var commonY = reference.y + reference.height / 2 - element.height / 2; 883 var offsets; 884 885 switch (basePlacement) { 886 case top: 887 offsets = { 888 x: commonX, 889 y: reference.y - element.height 890 }; 891 break; 892 893 case bottom: 894 offsets = { 895 x: commonX, 896 y: reference.y + reference.height 897 }; 898 break; 899 900 case right: 901 offsets = { 902 x: reference.x + reference.width, 903 y: commonY 904 }; 905 break; 906 907 case left: 908 offsets = { 909 x: reference.x - element.width, 910 y: commonY 911 }; 912 break; 913 914 default: 915 offsets = { 916 x: reference.x, 917 y: reference.y 918 }; 919 } 920 921 var mainAxis = basePlacement ? getMainAxisFromPlacement(basePlacement) : null; 922 923 if (mainAxis != null) { 924 var len = mainAxis === 'y' ? 'height' : 'width'; 925 926 switch (variation) { 927 case start: 928 offsets[mainAxis] = offsets[mainAxis] - (reference[len] / 2 - element[len] / 2); 929 break; 930 931 case end: 932 offsets[mainAxis] = offsets[mainAxis] + (reference[len] / 2 - element[len] / 2); 933 break; 934 } 935 } 936 937 return offsets; 938 } 939 940 function detectOverflow(state, options) { 941 if (options === void 0) { 942 options = {}; 943 } 944 945 var _options = options, 946 _options$placement = _options.placement, 947 placement = _options$placement === void 0 ? state.placement : _options$placement, 948 _options$boundary = _options.boundary, 949 boundary = _options$boundary === void 0 ? clippingParents : _options$boundary, 950 _options$rootBoundary = _options.rootBoundary, 951 rootBoundary = _options$rootBoundary === void 0 ? viewport : _options$rootBoundary, 952 _options$elementConte = _options.elementContext, 953 elementContext = _options$elementConte === void 0 ? popper : _options$elementConte, 954 _options$altBoundary = _options.altBoundary, 955 altBoundary = _options$altBoundary === void 0 ? false : _options$altBoundary, 956 _options$padding = _options.padding, 957 padding = _options$padding === void 0 ? 0 : _options$padding; 958 var paddingObject = mergePaddingObject(typeof padding !== 'number' ? padding : expandToHashMap(padding, basePlacements)); 959 var altContext = elementContext === popper ? reference : popper; 960 var popperRect = state.rects.popper; 961 var element = state.elements[altBoundary ? altContext : elementContext]; 962 var clippingClientRect = getClippingRect(isElement$1(element) ? element : element.contextElement || getDocumentElement(state.elements.popper), boundary, rootBoundary); 963 var referenceClientRect = getBoundingClientRect(state.elements.reference); 964 var popperOffsets = computeOffsets({ 965 reference: referenceClientRect, 966 element: popperRect, 967 strategy: 'absolute', 968 placement: placement 969 }); 970 var popperClientRect = rectToClientRect(Object.assign({}, popperRect, popperOffsets)); 971 var elementClientRect = elementContext === popper ? popperClientRect : referenceClientRect; // positive = overflowing the clipping rect 972 // 0 or negative = within the clipping rect 973 974 var overflowOffsets = { 975 top: clippingClientRect.top - elementClientRect.top + paddingObject.top, 976 bottom: elementClientRect.bottom - clippingClientRect.bottom + paddingObject.bottom, 977 left: clippingClientRect.left - elementClientRect.left + paddingObject.left, 978 right: elementClientRect.right - clippingClientRect.right + paddingObject.right 979 }; 980 var offsetData = state.modifiersData.offset; // Offsets can be applied only to the popper element 981 982 if (elementContext === popper && offsetData) { 983 var offset = offsetData[placement]; 984 Object.keys(overflowOffsets).forEach(function (key) { 985 var multiply = [right, bottom].indexOf(key) >= 0 ? 1 : -1; 986 var axis = [top, bottom].indexOf(key) >= 0 ? 'y' : 'x'; 987 overflowOffsets[key] += offset[axis] * multiply; 988 }); 989 } 990 991 return overflowOffsets; 992 } 993 994 function computeAutoPlacement(state, options) { 995 if (options === void 0) { 996 options = {}; 997 } 998 999 var _options = options, 1000 placement = _options.placement, 1001 boundary = _options.boundary, 1002 rootBoundary = _options.rootBoundary, 1003 padding = _options.padding, 1004 flipVariations = _options.flipVariations, 1005 _options$allowedAutoP = _options.allowedAutoPlacements, 1006 allowedAutoPlacements = _options$allowedAutoP === void 0 ? placements : _options$allowedAutoP; 1007 var variation = getVariation(placement); 1008 var placements$1 = variation ? flipVariations ? variationPlacements : variationPlacements.filter(function (placement) { 1009 return getVariation(placement) === variation; 1010 }) : basePlacements; 1011 var allowedPlacements = placements$1.filter(function (placement) { 1012 return allowedAutoPlacements.indexOf(placement) >= 0; 1013 }); 1014 1015 if (allowedPlacements.length === 0) { 1016 allowedPlacements = placements$1; 1017 } // $FlowFixMe[incompatible-type]: Flow seems to have problems with two array unions... 1018 1019 1020 var overflows = allowedPlacements.reduce(function (acc, placement) { 1021 acc[placement] = detectOverflow(state, { 1022 placement: placement, 1023 boundary: boundary, 1024 rootBoundary: rootBoundary, 1025 padding: padding 1026 })[getBasePlacement$1(placement)]; 1027 return acc; 1028 }, {}); 1029 return Object.keys(overflows).sort(function (a, b) { 1030 return overflows[a] - overflows[b]; 1031 }); 1032 } 1033 1034 function getExpandedFallbackPlacements(placement) { 1035 if (getBasePlacement$1(placement) === auto) { 1036 return []; 1037 } 1038 1039 var oppositePlacement = getOppositePlacement(placement); 1040 return [getOppositeVariationPlacement(placement), oppositePlacement, getOppositeVariationPlacement(oppositePlacement)]; 1041 } 1042 1043 function flip(_ref) { 1044 var state = _ref.state, 1045 options = _ref.options, 1046 name = _ref.name; 1047 1048 if (state.modifiersData[name]._skip) { 1049 return; 1050 } 1051 1052 var _options$mainAxis = options.mainAxis, 1053 checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, 1054 _options$altAxis = options.altAxis, 1055 checkAltAxis = _options$altAxis === void 0 ? true : _options$altAxis, 1056 specifiedFallbackPlacements = options.fallbackPlacements, 1057 padding = options.padding, 1058 boundary = options.boundary, 1059 rootBoundary = options.rootBoundary, 1060 altBoundary = options.altBoundary, 1061 _options$flipVariatio = options.flipVariations, 1062 flipVariations = _options$flipVariatio === void 0 ? true : _options$flipVariatio, 1063 allowedAutoPlacements = options.allowedAutoPlacements; 1064 var preferredPlacement = state.options.placement; 1065 var basePlacement = getBasePlacement$1(preferredPlacement); 1066 var isBasePlacement = basePlacement === preferredPlacement; 1067 var fallbackPlacements = specifiedFallbackPlacements || (isBasePlacement || !flipVariations ? [getOppositePlacement(preferredPlacement)] : getExpandedFallbackPlacements(preferredPlacement)); 1068 var placements = [preferredPlacement].concat(fallbackPlacements).reduce(function (acc, placement) { 1069 return acc.concat(getBasePlacement$1(placement) === auto ? computeAutoPlacement(state, { 1070 placement: placement, 1071 boundary: boundary, 1072 rootBoundary: rootBoundary, 1073 padding: padding, 1074 flipVariations: flipVariations, 1075 allowedAutoPlacements: allowedAutoPlacements 1076 }) : placement); 1077 }, []); 1078 var referenceRect = state.rects.reference; 1079 var popperRect = state.rects.popper; 1080 var checksMap = new Map(); 1081 var makeFallbackChecks = true; 1082 var firstFittingPlacement = placements[0]; 1083 1084 for (var i = 0; i < placements.length; i++) { 1085 var placement = placements[i]; 1086 1087 var _basePlacement = getBasePlacement$1(placement); 1088 1089 var isStartVariation = getVariation(placement) === start; 1090 var isVertical = [top, bottom].indexOf(_basePlacement) >= 0; 1091 var len = isVertical ? 'width' : 'height'; 1092 var overflow = detectOverflow(state, { 1093 placement: placement, 1094 boundary: boundary, 1095 rootBoundary: rootBoundary, 1096 altBoundary: altBoundary, 1097 padding: padding 1098 }); 1099 var mainVariationSide = isVertical ? isStartVariation ? right : left : isStartVariation ? bottom : top; 1100 1101 if (referenceRect[len] > popperRect[len]) { 1102 mainVariationSide = getOppositePlacement(mainVariationSide); 1103 } 1104 1105 var altVariationSide = getOppositePlacement(mainVariationSide); 1106 var checks = []; 1107 1108 if (checkMainAxis) { 1109 checks.push(overflow[_basePlacement] <= 0); 1110 } 1111 1112 if (checkAltAxis) { 1113 checks.push(overflow[mainVariationSide] <= 0, overflow[altVariationSide] <= 0); 1114 } 1115 1116 if (checks.every(function (check) { 1117 return check; 1118 })) { 1119 firstFittingPlacement = placement; 1120 makeFallbackChecks = false; 1121 break; 1122 } 1123 1124 checksMap.set(placement, checks); 1125 } 1126 1127 if (makeFallbackChecks) { 1128 // `2` may be desired in some cases – research later 1129 var numberOfChecks = flipVariations ? 3 : 1; 1130 1131 var _loop = function _loop(_i) { 1132 var fittingPlacement = placements.find(function (placement) { 1133 var checks = checksMap.get(placement); 1134 1135 if (checks) { 1136 return checks.slice(0, _i).every(function (check) { 1137 return check; 1138 }); 1139 } 1140 }); 1141 1142 if (fittingPlacement) { 1143 firstFittingPlacement = fittingPlacement; 1144 return "break"; 1145 } 1146 }; 1147 1148 for (var _i = numberOfChecks; _i > 0; _i--) { 1149 var _ret = _loop(_i); 1150 1151 if (_ret === "break") break; 1152 } 1153 } 1154 1155 if (state.placement !== firstFittingPlacement) { 1156 state.modifiersData[name]._skip = true; 1157 state.placement = firstFittingPlacement; 1158 state.reset = true; 1159 } 1160 } // eslint-disable-next-line import/no-unused-modules 1161 1162 1163 var flip$1 = { 1164 name: 'flip', 1165 enabled: true, 1166 phase: 'main', 1167 fn: flip, 1168 requiresIfExists: ['offset'], 1169 data: { 1170 _skip: false 1171 } 1172 }; 1173 1174 function getSideOffsets(overflow, rect, preventedOffsets) { 1175 if (preventedOffsets === void 0) { 1176 preventedOffsets = { 1177 x: 0, 1178 y: 0 1179 }; 1180 } 1181 1182 return { 1183 top: overflow.top - rect.height - preventedOffsets.y, 1184 right: overflow.right - rect.width + preventedOffsets.x, 1185 bottom: overflow.bottom - rect.height + preventedOffsets.y, 1186 left: overflow.left - rect.width - preventedOffsets.x 1187 }; 1188 } 1189 1190 function isAnySideFullyClipped(overflow) { 1191 return [top, right, bottom, left].some(function (side) { 1192 return overflow[side] >= 0; 1193 }); 1194 } 1195 1196 function hide(_ref) { 1197 var state = _ref.state, 1198 name = _ref.name; 1199 var referenceRect = state.rects.reference; 1200 var popperRect = state.rects.popper; 1201 var preventedOffsets = state.modifiersData.preventOverflow; 1202 var referenceOverflow = detectOverflow(state, { 1203 elementContext: 'reference' 1204 }); 1205 var popperAltOverflow = detectOverflow(state, { 1206 altBoundary: true 1207 }); 1208 var referenceClippingOffsets = getSideOffsets(referenceOverflow, referenceRect); 1209 var popperEscapeOffsets = getSideOffsets(popperAltOverflow, popperRect, preventedOffsets); 1210 var isReferenceHidden = isAnySideFullyClipped(referenceClippingOffsets); 1211 var hasPopperEscaped = isAnySideFullyClipped(popperEscapeOffsets); 1212 state.modifiersData[name] = { 1213 referenceClippingOffsets: referenceClippingOffsets, 1214 popperEscapeOffsets: popperEscapeOffsets, 1215 isReferenceHidden: isReferenceHidden, 1216 hasPopperEscaped: hasPopperEscaped 1217 }; 1218 state.attributes.popper = Object.assign({}, state.attributes.popper, { 1219 'data-popper-reference-hidden': isReferenceHidden, 1220 'data-popper-escaped': hasPopperEscaped 1221 }); 1222 } // eslint-disable-next-line import/no-unused-modules 1223 1224 1225 var hide$1 = { 1226 name: 'hide', 1227 enabled: true, 1228 phase: 'main', 1229 requiresIfExists: ['preventOverflow'], 1230 fn: hide 1231 }; 1232 1233 function distanceAndSkiddingToXY(placement, rects, offset) { 1234 var basePlacement = getBasePlacement$1(placement); 1235 var invertDistance = [left, top].indexOf(basePlacement) >= 0 ? -1 : 1; 1236 1237 var _ref = typeof offset === 'function' ? offset(Object.assign({}, rects, { 1238 placement: placement 1239 })) : offset, 1240 skidding = _ref[0], 1241 distance = _ref[1]; 1242 1243 skidding = skidding || 0; 1244 distance = (distance || 0) * invertDistance; 1245 return [left, right].indexOf(basePlacement) >= 0 ? { 1246 x: distance, 1247 y: skidding 1248 } : { 1249 x: skidding, 1250 y: distance 1251 }; 1252 } 1253 1254 function offset(_ref2) { 1255 var state = _ref2.state, 1256 options = _ref2.options, 1257 name = _ref2.name; 1258 var _options$offset = options.offset, 1259 offset = _options$offset === void 0 ? [0, 0] : _options$offset; 1260 var data = placements.reduce(function (acc, placement) { 1261 acc[placement] = distanceAndSkiddingToXY(placement, state.rects, offset); 1262 return acc; 1263 }, {}); 1264 var _data$state$placement = data[state.placement], 1265 x = _data$state$placement.x, 1266 y = _data$state$placement.y; 1267 1268 if (state.modifiersData.popperOffsets != null) { 1269 state.modifiersData.popperOffsets.x += x; 1270 state.modifiersData.popperOffsets.y += y; 1271 } 1272 1273 state.modifiersData[name] = data; 1274 } // eslint-disable-next-line import/no-unused-modules 1275 1276 1277 var offset$1 = { 1278 name: 'offset', 1279 enabled: true, 1280 phase: 'main', 1281 requires: ['popperOffsets'], 1282 fn: offset 1283 }; 1284 1285 function popperOffsets(_ref) { 1286 var state = _ref.state, 1287 name = _ref.name; // Offsets are the actual position the popper needs to have to be 1288 // properly positioned near its reference element 1289 // This is the most basic placement, and will be adjusted by 1290 // the modifiers in the next step 1291 1292 state.modifiersData[name] = computeOffsets({ 1293 reference: state.rects.reference, 1294 element: state.rects.popper, 1295 strategy: 'absolute', 1296 placement: state.placement 1297 }); 1298 } // eslint-disable-next-line import/no-unused-modules 1299 1300 1301 var popperOffsets$1 = { 1302 name: 'popperOffsets', 1303 enabled: true, 1304 phase: 'read', 1305 fn: popperOffsets, 1306 data: {} 1307 }; 1308 1309 function getAltAxis(axis) { 1310 return axis === 'x' ? 'y' : 'x'; 1311 } 1312 1313 function preventOverflow(_ref) { 1314 var state = _ref.state, 1315 options = _ref.options, 1316 name = _ref.name; 1317 var _options$mainAxis = options.mainAxis, 1318 checkMainAxis = _options$mainAxis === void 0 ? true : _options$mainAxis, 1319 _options$altAxis = options.altAxis, 1320 checkAltAxis = _options$altAxis === void 0 ? false : _options$altAxis, 1321 boundary = options.boundary, 1322 rootBoundary = options.rootBoundary, 1323 altBoundary = options.altBoundary, 1324 padding = options.padding, 1325 _options$tether = options.tether, 1326 tether = _options$tether === void 0 ? true : _options$tether, 1327 _options$tetherOffset = options.tetherOffset, 1328 tetherOffset = _options$tetherOffset === void 0 ? 0 : _options$tetherOffset; 1329 var overflow = detectOverflow(state, { 1330 boundary: boundary, 1331 rootBoundary: rootBoundary, 1332 padding: padding, 1333 altBoundary: altBoundary 1334 }); 1335 var basePlacement = getBasePlacement$1(state.placement); 1336 var variation = getVariation(state.placement); 1337 var isBasePlacement = !variation; 1338 var mainAxis = getMainAxisFromPlacement(basePlacement); 1339 var altAxis = getAltAxis(mainAxis); 1340 var popperOffsets = state.modifiersData.popperOffsets; 1341 var referenceRect = state.rects.reference; 1342 var popperRect = state.rects.popper; 1343 var tetherOffsetValue = typeof tetherOffset === 'function' ? tetherOffset(Object.assign({}, state.rects, { 1344 placement: state.placement 1345 })) : tetherOffset; 1346 var normalizedTetherOffsetValue = typeof tetherOffsetValue === 'number' ? { 1347 mainAxis: tetherOffsetValue, 1348 altAxis: tetherOffsetValue 1349 } : Object.assign({ 1350 mainAxis: 0, 1351 altAxis: 0 1352 }, tetherOffsetValue); 1353 var offsetModifierState = state.modifiersData.offset ? state.modifiersData.offset[state.placement] : null; 1354 var data = { 1355 x: 0, 1356 y: 0 1357 }; 1358 1359 if (!popperOffsets) { 1360 return; 1361 } 1362 1363 if (checkMainAxis) { 1364 var _offsetModifierState$; 1365 1366 var mainSide = mainAxis === 'y' ? top : left; 1367 var altSide = mainAxis === 'y' ? bottom : right; 1368 var len = mainAxis === 'y' ? 'height' : 'width'; 1369 var offset = popperOffsets[mainAxis]; 1370 var min$1 = offset + overflow[mainSide]; 1371 var max$1 = offset - overflow[altSide]; 1372 var additive = tether ? -popperRect[len] / 2 : 0; 1373 var minLen = variation === start ? referenceRect[len] : popperRect[len]; 1374 var maxLen = variation === start ? -popperRect[len] : -referenceRect[len]; // We need to include the arrow in the calculation so the arrow doesn't go 1375 // outside the reference bounds 1376 1377 var arrowElement = state.elements.arrow; 1378 var arrowRect = tether && arrowElement ? getLayoutRect(arrowElement) : { 1379 width: 0, 1380 height: 0 1381 }; 1382 var arrowPaddingObject = state.modifiersData['arrow#persistent'] ? state.modifiersData['arrow#persistent'].padding : getFreshSideObject(); 1383 var arrowPaddingMin = arrowPaddingObject[mainSide]; 1384 var arrowPaddingMax = arrowPaddingObject[altSide]; // If the reference length is smaller than the arrow length, we don't want 1385 // to include its full size in the calculation. If the reference is small 1386 // and near the edge of a boundary, the popper can overflow even if the 1387 // reference is not overflowing as well (e.g. virtual elements with no 1388 // width or height) 1389 1390 var arrowLen = within(0, referenceRect[len], arrowRect[len]); 1391 var minOffset = isBasePlacement ? referenceRect[len] / 2 - additive - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis : minLen - arrowLen - arrowPaddingMin - normalizedTetherOffsetValue.mainAxis; 1392 var maxOffset = isBasePlacement ? -referenceRect[len] / 2 + additive + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis : maxLen + arrowLen + arrowPaddingMax + normalizedTetherOffsetValue.mainAxis; 1393 var arrowOffsetParent = state.elements.arrow && getOffsetParent(state.elements.arrow); 1394 var clientOffset = arrowOffsetParent ? mainAxis === 'y' ? arrowOffsetParent.clientTop || 0 : arrowOffsetParent.clientLeft || 0 : 0; 1395 var offsetModifierValue = (_offsetModifierState$ = offsetModifierState == null ? void 0 : offsetModifierState[mainAxis]) != null ? _offsetModifierState$ : 0; 1396 var tetherMin = offset + minOffset - offsetModifierValue - clientOffset; 1397 var tetherMax = offset + maxOffset - offsetModifierValue; 1398 var preventedOffset = within(tether ? min(min$1, tetherMin) : min$1, offset, tether ? max(max$1, tetherMax) : max$1); 1399 popperOffsets[mainAxis] = preventedOffset; 1400 data[mainAxis] = preventedOffset - offset; 1401 } 1402 1403 if (checkAltAxis) { 1404 var _offsetModifierState$2; 1405 1406 var _mainSide = mainAxis === 'x' ? top : left; 1407 1408 var _altSide = mainAxis === 'x' ? bottom : right; 1409 1410 var _offset = popperOffsets[altAxis]; 1411 1412 var _len = altAxis === 'y' ? 'height' : 'width'; 1413 1414 var _min = _offset + overflow[_mainSide]; 1415 1416 var _max = _offset - overflow[_altSide]; 1417 1418 var isOriginSide = [top, left].indexOf(basePlacement) !== -1; 1419 1420 var _offsetModifierValue = (_offsetModifierState$2 = offsetModifierState == null ? void 0 : offsetModifierState[altAxis]) != null ? _offsetModifierState$2 : 0; 1421 1422 var _tetherMin = isOriginSide ? _min : _offset - referenceRect[_len] - popperRect[_len] - _offsetModifierValue + normalizedTetherOffsetValue.altAxis; 1423 1424 var _tetherMax = isOriginSide ? _offset + referenceRect[_len] + popperRect[_len] - _offsetModifierValue - normalizedTetherOffsetValue.altAxis : _max; 1425 1426 var _preventedOffset = tether && isOriginSide ? withinMaxClamp(_tetherMin, _offset, _tetherMax) : within(tether ? _tetherMin : _min, _offset, tether ? _tetherMax : _max); 1427 1428 popperOffsets[altAxis] = _preventedOffset; 1429 data[altAxis] = _preventedOffset - _offset; 1430 } 1431 1432 state.modifiersData[name] = data; 1433 } // eslint-disable-next-line import/no-unused-modules 1434 1435 1436 var preventOverflow$1 = { 1437 name: 'preventOverflow', 1438 enabled: true, 1439 phase: 'main', 1440 fn: preventOverflow, 1441 requiresIfExists: ['offset'] 1442 }; 1443 1444 function getHTMLElementScroll(element) { 1445 return { 1446 scrollLeft: element.scrollLeft, 1447 scrollTop: element.scrollTop 1448 }; 1449 } 1450 1451 function getNodeScroll(node) { 1452 if (node === getWindow(node) || !isHTMLElement(node)) { 1453 return getWindowScroll(node); 1454 } else { 1455 return getHTMLElementScroll(node); 1456 } 1457 } 1458 1459 function isElementScaled(element) { 1460 var rect = element.getBoundingClientRect(); 1461 var scaleX = round(rect.width) / element.offsetWidth || 1; 1462 var scaleY = round(rect.height) / element.offsetHeight || 1; 1463 return scaleX !== 1 || scaleY !== 1; 1464 } // Returns the composite rect of an element relative to its offsetParent. 1465 // Composite means it takes into account transforms as well as layout. 1466 1467 1468 function getCompositeRect(elementOrVirtualElement, offsetParent, isFixed) { 1469 if (isFixed === void 0) { 1470 isFixed = false; 1471 } 1472 1473 var isOffsetParentAnElement = isHTMLElement(offsetParent); 1474 var offsetParentIsScaled = isHTMLElement(offsetParent) && isElementScaled(offsetParent); 1475 var documentElement = getDocumentElement(offsetParent); 1476 var rect = getBoundingClientRect(elementOrVirtualElement, offsetParentIsScaled); 1477 var scroll = { 1478 scrollLeft: 0, 1479 scrollTop: 0 1480 }; 1481 var offsets = { 1482 x: 0, 1483 y: 0 1484 }; 1485 1486 if (isOffsetParentAnElement || !isOffsetParentAnElement && !isFixed) { 1487 if (getNodeName(offsetParent) !== 'body' || // https://github.com/popperjs/popper-core/issues/1078 1488 isScrollParent(documentElement)) { 1489 scroll = getNodeScroll(offsetParent); 1490 } 1491 1492 if (isHTMLElement(offsetParent)) { 1493 offsets = getBoundingClientRect(offsetParent, true); 1494 offsets.x += offsetParent.clientLeft; 1495 offsets.y += offsetParent.clientTop; 1496 } else if (documentElement) { 1497 offsets.x = getWindowScrollBarX(documentElement); 1498 } 1499 } 1500 1501 return { 1502 x: rect.left + scroll.scrollLeft - offsets.x, 1503 y: rect.top + scroll.scrollTop - offsets.y, 1504 width: rect.width, 1505 height: rect.height 1506 }; 1507 } 1508 1509 function order(modifiers) { 1510 var map = new Map(); 1511 var visited = new Set(); 1512 var result = []; 1513 modifiers.forEach(function (modifier) { 1514 map.set(modifier.name, modifier); 1515 }); // On visiting object, check for its dependencies and visit them recursively 1516 1517 function sort(modifier) { 1518 visited.add(modifier.name); 1519 var requires = [].concat(modifier.requires || [], modifier.requiresIfExists || []); 1520 requires.forEach(function (dep) { 1521 if (!visited.has(dep)) { 1522 var depModifier = map.get(dep); 1523 1524 if (depModifier) { 1525 sort(depModifier); 1526 } 1527 } 1528 }); 1529 result.push(modifier); 1530 } 1531 1532 modifiers.forEach(function (modifier) { 1533 if (!visited.has(modifier.name)) { 1534 // check for visited object 1535 sort(modifier); 1536 } 1537 }); 1538 return result; 1539 } 1540 1541 function orderModifiers(modifiers) { 1542 // order based on dependencies 1543 var orderedModifiers = order(modifiers); // order based on phase 1544 1545 return modifierPhases.reduce(function (acc, phase) { 1546 return acc.concat(orderedModifiers.filter(function (modifier) { 1547 return modifier.phase === phase; 1548 })); 1549 }, []); 1550 } 1551 1552 function debounce$1(fn) { 1553 var pending; 1554 return function () { 1555 if (!pending) { 1556 pending = new Promise(function (resolve) { 1557 Promise.resolve().then(function () { 1558 pending = undefined; 1559 resolve(fn()); 1560 }); 1561 }); 1562 } 1563 1564 return pending; 1565 }; 1566 } 1567 1568 function mergeByName(modifiers) { 1569 var merged = modifiers.reduce(function (merged, current) { 1570 var existing = merged[current.name]; 1571 merged[current.name] = existing ? Object.assign({}, existing, current, { 1572 options: Object.assign({}, existing.options, current.options), 1573 data: Object.assign({}, existing.data, current.data) 1574 }) : current; 1575 return merged; 1576 }, {}); // IE11 does not support Object.values 1577 1578 return Object.keys(merged).map(function (key) { 1579 return merged[key]; 1580 }); 1581 } 1582 1583 var DEFAULT_OPTIONS = { 1584 placement: 'bottom', 1585 modifiers: [], 1586 strategy: 'absolute' 1587 }; 1588 1589 function areValidElements() { 1590 for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { 1591 args[_key] = arguments[_key]; 1592 } 1593 1594 return !args.some(function (element) { 1595 return !(element && typeof element.getBoundingClientRect === 'function'); 1596 }); 1597 } 1598 1599 function popperGenerator(generatorOptions) { 1600 if (generatorOptions === void 0) { 1601 generatorOptions = {}; 1602 } 1603 1604 var _generatorOptions = generatorOptions, 1605 _generatorOptions$def = _generatorOptions.defaultModifiers, 1606 defaultModifiers = _generatorOptions$def === void 0 ? [] : _generatorOptions$def, 1607 _generatorOptions$def2 = _generatorOptions.defaultOptions, 1608 defaultOptions = _generatorOptions$def2 === void 0 ? DEFAULT_OPTIONS : _generatorOptions$def2; 1609 return function createPopper(reference, popper, options) { 1610 if (options === void 0) { 1611 options = defaultOptions; 1612 } 1613 1614 var state = { 1615 placement: 'bottom', 1616 orderedModifiers: [], 1617 options: Object.assign({}, DEFAULT_OPTIONS, defaultOptions), 1618 modifiersData: {}, 1619 elements: { 1620 reference: reference, 1621 popper: popper 1622 }, 1623 attributes: {}, 1624 styles: {} 1625 }; 1626 var effectCleanupFns = []; 1627 var isDestroyed = false; 1628 var instance = { 1629 state: state, 1630 setOptions: function setOptions(setOptionsAction) { 1631 var options = typeof setOptionsAction === 'function' ? setOptionsAction(state.options) : setOptionsAction; 1632 cleanupModifierEffects(); 1633 state.options = Object.assign({}, defaultOptions, state.options, options); 1634 state.scrollParents = { 1635 reference: isElement$1(reference) ? listScrollParents(reference) : reference.contextElement ? listScrollParents(reference.contextElement) : [], 1636 popper: listScrollParents(popper) 1637 }; // Orders the modifiers based on their dependencies and `phase` 1638 // properties 1639 1640 var orderedModifiers = orderModifiers(mergeByName([].concat(defaultModifiers, state.options.modifiers))); // Strip out disabled modifiers 1641 1642 state.orderedModifiers = orderedModifiers.filter(function (m) { 1643 return m.enabled; 1644 }); // Validate the provided modifiers so that the consumer will get warned 1645 1646 runModifierEffects(); 1647 return instance.update(); 1648 }, 1649 // Sync update – it will always be executed, even if not necessary. This 1650 // is useful for low frequency updates where sync behavior simplifies the 1651 // logic. 1652 // For high frequency updates (e.g. `resize` and `scroll` events), always 1653 // prefer the async Popper#update method 1654 forceUpdate: function forceUpdate() { 1655 if (isDestroyed) { 1656 return; 1657 } 1658 1659 var _state$elements = state.elements, 1660 reference = _state$elements.reference, 1661 popper = _state$elements.popper; // Don't proceed if `reference` or `popper` are not valid elements 1662 // anymore 1663 1664 if (!areValidElements(reference, popper)) { 1665 return; 1666 } // Store the reference and popper rects to be read by modifiers 1667 1668 1669 state.rects = { 1670 reference: getCompositeRect(reference, getOffsetParent(popper), state.options.strategy === 'fixed'), 1671 popper: getLayoutRect(popper) 1672 }; // Modifiers have the ability to reset the current update cycle. The 1673 // most common use case for this is the `flip` modifier changing the 1674 // placement, which then needs to re-run all the modifiers, because the 1675 // logic was previously ran for the previous placement and is therefore 1676 // stale/incorrect 1677 1678 state.reset = false; 1679 state.placement = state.options.placement; // On each update cycle, the `modifiersData` property for each modifier 1680 // is filled with the initial data specified by the modifier. This means 1681 // it doesn't persist and is fresh on each update. 1682 // To ensure persistent data, use `${name}#persistent` 1683 1684 state.orderedModifiers.forEach(function (modifier) { 1685 return state.modifiersData[modifier.name] = Object.assign({}, modifier.data); 1686 }); 1687 1688 for (var index = 0; index < state.orderedModifiers.length; index++) { 1689 if (state.reset === true) { 1690 state.reset = false; 1691 index = -1; 1692 continue; 1693 } 1694 1695 var _state$orderedModifie = state.orderedModifiers[index], 1696 fn = _state$orderedModifie.fn, 1697 _state$orderedModifie2 = _state$orderedModifie.options, 1698 _options = _state$orderedModifie2 === void 0 ? {} : _state$orderedModifie2, 1699 name = _state$orderedModifie.name; 1700 1701 if (typeof fn === 'function') { 1702 state = fn({ 1703 state: state, 1704 options: _options, 1705 name: name, 1706 instance: instance 1707 }) || state; 1708 } 1709 } 1710 }, 1711 // Async and optimistically optimized update – it will not be executed if 1712 // not necessary (debounced to run at most once-per-tick) 1713 update: debounce$1(function () { 1714 return new Promise(function (resolve) { 1715 instance.forceUpdate(); 1716 resolve(state); 1717 }); 1718 }), 1719 destroy: function destroy() { 1720 cleanupModifierEffects(); 1721 isDestroyed = true; 1722 } 1723 }; 1724 1725 if (!areValidElements(reference, popper)) { 1726 return instance; 1727 } 1728 1729 instance.setOptions(options).then(function (state) { 1730 if (!isDestroyed && options.onFirstUpdate) { 1731 options.onFirstUpdate(state); 1732 } 1733 }); // Modifiers have the ability to execute arbitrary code before the first 1734 // update cycle runs. They will be executed in the same order as the update 1735 // cycle. This is useful when a modifier adds some persistent data that 1736 // other modifiers need to use, but the modifier is run after the dependent 1737 // one. 1738 1739 function runModifierEffects() { 1740 state.orderedModifiers.forEach(function (_ref3) { 1741 var name = _ref3.name, 1742 _ref3$options = _ref3.options, 1743 options = _ref3$options === void 0 ? {} : _ref3$options, 1744 effect = _ref3.effect; 1745 1746 if (typeof effect === 'function') { 1747 var cleanupFn = effect({ 1748 state: state, 1749 name: name, 1750 instance: instance, 1751 options: options 1752 }); 1753 1754 var noopFn = function noopFn() {}; 1755 1756 effectCleanupFns.push(cleanupFn || noopFn); 1757 } 1758 }); 1759 } 1760 1761 function cleanupModifierEffects() { 1762 effectCleanupFns.forEach(function (fn) { 1763 return fn(); 1764 }); 1765 effectCleanupFns = []; 1766 } 1767 1768 return instance; 1769 }; 1770 } 1771 1772 var defaultModifiers = [eventListeners, popperOffsets$1, computeStyles$1, applyStyles$1, offset$1, flip$1, preventOverflow$1, arrow$1, hide$1]; 1773 var createPopper = /*#__PURE__*/popperGenerator({ 1774 defaultModifiers: defaultModifiers 1775 }); // eslint-disable-next-line import/no-unused-modules 1776 1777 /**! 1778 * tippy.js v6.3.7 1779 * (c) 2017-2021 atomiks 1780 * MIT License 1781 */ 1782 1783 var BOX_CLASS = "tippy-box"; 1784 var CONTENT_CLASS = "tippy-content"; 1785 var BACKDROP_CLASS = "tippy-backdrop"; 1786 var ARROW_CLASS = "tippy-arrow"; 1787 var SVG_ARROW_CLASS = "tippy-svg-arrow"; 1788 var TOUCH_OPTIONS = { 1789 passive: true, 1790 capture: true 1791 }; 1792 1793 var TIPPY_DEFAULT_APPEND_TO = function TIPPY_DEFAULT_APPEND_TO() { 1794 return document.body; 1795 }; 1796 1797 function getValueAtIndexOrReturn(value, index, defaultValue) { 1798 if (Array.isArray(value)) { 1799 var v = value[index]; 1800 return v == null ? Array.isArray(defaultValue) ? defaultValue[index] : defaultValue : v; 1801 } 1802 1803 return value; 1804 } 1805 1806 function isType(value, type) { 1807 var str = {}.toString.call(value); 1808 return str.indexOf('[object') === 0 && str.indexOf(type + "]") > -1; 1809 } 1810 1811 function invokeWithArgsOrReturn(value, args) { 1812 return typeof value === 'function' ? value.apply(void 0, args) : value; 1813 } 1814 1815 function debounce(fn, ms) { 1816 // Avoid wrapping in `setTimeout` if ms is 0 anyway 1817 if (ms === 0) { 1818 return fn; 1819 } 1820 1821 var timeout; 1822 return function (arg) { 1823 clearTimeout(timeout); 1824 timeout = setTimeout(function () { 1825 fn(arg); 1826 }, ms); 1827 }; 1828 } 1829 1830 function splitBySpaces(value) { 1831 return value.split(/\s+/).filter(Boolean); 1832 } 1833 1834 function normalizeToArray(value) { 1835 return [].concat(value); 1836 } 1837 1838 function pushIfUnique(arr, value) { 1839 if (arr.indexOf(value) === -1) { 1840 arr.push(value); 1841 } 1842 } 1843 1844 function unique(arr) { 1845 return arr.filter(function (item, index) { 1846 return arr.indexOf(item) === index; 1847 }); 1848 } 1849 1850 function getBasePlacement(placement) { 1851 return placement.split('-')[0]; 1852 } 1853 1854 function arrayFrom(value) { 1855 return [].slice.call(value); 1856 } 1857 1858 function removeUndefinedProps(obj) { 1859 return Object.keys(obj).reduce(function (acc, key) { 1860 if (obj[key] !== undefined) { 1861 acc[key] = obj[key]; 1862 } 1863 1864 return acc; 1865 }, {}); 1866 } 1867 1868 function div() { 1869 return document.createElement('div'); 1870 } 1871 1872 function isElement(value) { 1873 return ['Element', 'Fragment'].some(function (type) { 1874 return isType(value, type); 1875 }); 1876 } 1877 1878 function isNodeList(value) { 1879 return isType(value, 'NodeList'); 1880 } 1881 1882 function isMouseEvent(value) { 1883 return isType(value, 'MouseEvent'); 1884 } 1885 1886 function isReferenceElement(value) { 1887 return !!(value && value._tippy && value._tippy.reference === value); 1888 } 1889 1890 function getArrayOfElements(value) { 1891 if (isElement(value)) { 1892 return [value]; 1893 } 1894 1895 if (isNodeList(value)) { 1896 return arrayFrom(value); 1897 } 1898 1899 if (Array.isArray(value)) { 1900 return value; 1901 } 1902 1903 return arrayFrom(document.querySelectorAll(value)); 1904 } 1905 1906 function setTransitionDuration(els, value) { 1907 els.forEach(function (el) { 1908 if (el) { 1909 el.style.transitionDuration = value + "ms"; 1910 } 1911 }); 1912 } 1913 1914 function setVisibilityState(els, state) { 1915 els.forEach(function (el) { 1916 if (el) { 1917 el.setAttribute('data-state', state); 1918 } 1919 }); 1920 } 1921 1922 function getOwnerDocument(elementOrElements) { 1923 var _element$ownerDocumen; 1924 1925 var _normalizeToArray = normalizeToArray(elementOrElements), 1926 element = _normalizeToArray[0]; // Elements created via a <template> have an ownerDocument with no reference to the body 1927 1928 1929 return element != null && (_element$ownerDocumen = element.ownerDocument) != null && _element$ownerDocumen.body ? element.ownerDocument : document; 1930 } 1931 1932 function isCursorOutsideInteractiveBorder(popperTreeData, event) { 1933 var clientX = event.clientX, 1934 clientY = event.clientY; 1935 return popperTreeData.every(function (_ref) { 1936 var popperRect = _ref.popperRect, 1937 popperState = _ref.popperState, 1938 props = _ref.props; 1939 var interactiveBorder = props.interactiveBorder; 1940 var basePlacement = getBasePlacement(popperState.placement); 1941 var offsetData = popperState.modifiersData.offset; 1942 1943 if (!offsetData) { 1944 return true; 1945 } 1946 1947 var topDistance = basePlacement === 'bottom' ? offsetData.top.y : 0; 1948 var bottomDistance = basePlacement === 'top' ? offsetData.bottom.y : 0; 1949 var leftDistance = basePlacement === 'right' ? offsetData.left.x : 0; 1950 var rightDistance = basePlacement === 'left' ? offsetData.right.x : 0; 1951 var exceedsTop = popperRect.top - clientY + topDistance > interactiveBorder; 1952 var exceedsBottom = clientY - popperRect.bottom - bottomDistance > interactiveBorder; 1953 var exceedsLeft = popperRect.left - clientX + leftDistance > interactiveBorder; 1954 var exceedsRight = clientX - popperRect.right - rightDistance > interactiveBorder; 1955 return exceedsTop || exceedsBottom || exceedsLeft || exceedsRight; 1956 }); 1957 } 1958 1959 function updateTransitionEndListener(box, action, listener) { 1960 var method = action + "EventListener"; // some browsers apparently support `transition` (unprefixed) but only fire 1961 // `webkitTransitionEnd`... 1962 1963 ['transitionend', 'webkitTransitionEnd'].forEach(function (event) { 1964 box[method](event, listener); 1965 }); 1966 } 1967 /** 1968 * Compared to xxx.contains, this function works for dom structures with shadow 1969 * dom 1970 */ 1971 1972 1973 function actualContains(parent, child) { 1974 var target = child; 1975 1976 while (target) { 1977 var _target$getRootNode; 1978 1979 if (parent.contains(target)) { 1980 return true; 1981 } 1982 1983 target = target.getRootNode == null ? void 0 : (_target$getRootNode = target.getRootNode()) == null ? void 0 : _target$getRootNode.host; 1984 } 1985 1986 return false; 1987 } 1988 1989 var currentInput = { 1990 isTouch: false 1991 }; 1992 var lastMouseMoveTime = 0; 1993 /** 1994 * When a `touchstart` event is fired, it's assumed the user is using touch 1995 * input. We'll bind a `mousemove` event listener to listen for mouse input in 1996 * the future. This way, the `isTouch` property is fully dynamic and will handle 1997 * hybrid devices that use a mix of touch + mouse input. 1998 */ 1999 2000 function onDocumentTouchStart() { 2001 if (currentInput.isTouch) { 2002 return; 2003 } 2004 2005 currentInput.isTouch = true; 2006 2007 if (window.performance) { 2008 document.addEventListener('mousemove', onDocumentMouseMove); 2009 } 2010 } 2011 /** 2012 * When two `mousemove` event are fired consecutively within 20ms, it's assumed 2013 * the user is using mouse input again. `mousemove` can fire on touch devices as 2014 * well, but very rarely that quickly. 2015 */ 2016 2017 2018 function onDocumentMouseMove() { 2019 var now = performance.now(); 2020 2021 if (now - lastMouseMoveTime < 20) { 2022 currentInput.isTouch = false; 2023 document.removeEventListener('mousemove', onDocumentMouseMove); 2024 } 2025 2026 lastMouseMoveTime = now; 2027 } 2028 /** 2029 * When an element is in focus and has a tippy, leaving the tab/window and 2030 * returning causes it to show again. For mouse users this is unexpected, but 2031 * for keyboard use it makes sense. 2032 * TODO: find a better technique to solve this problem 2033 */ 2034 2035 2036 function onWindowBlur() { 2037 var activeElement = document.activeElement; 2038 2039 if (isReferenceElement(activeElement)) { 2040 var instance = activeElement._tippy; 2041 2042 if (activeElement.blur && !instance.state.isVisible) { 2043 activeElement.blur(); 2044 } 2045 } 2046 } 2047 2048 function bindGlobalEventListeners() { 2049 document.addEventListener('touchstart', onDocumentTouchStart, TOUCH_OPTIONS); 2050 window.addEventListener('blur', onWindowBlur); 2051 } 2052 2053 var isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined'; 2054 var isIE11 = isBrowser ? // @ts-ignore 2055 !!window.msCrypto : false; 2056 var pluginProps = { 2057 animateFill: false, 2058 followCursor: false, 2059 inlinePositioning: false, 2060 sticky: false 2061 }; 2062 var renderProps = { 2063 allowHTML: false, 2064 animation: 'fade', 2065 arrow: true, 2066 content: '', 2067 inertia: false, 2068 maxWidth: 350, 2069 role: 'tooltip', 2070 theme: '', 2071 zIndex: 9999 2072 }; 2073 var defaultProps = Object.assign({ 2074 appendTo: TIPPY_DEFAULT_APPEND_TO, 2075 aria: { 2076 content: 'auto', 2077 expanded: 'auto' 2078 }, 2079 delay: 0, 2080 duration: [300, 250], 2081 getReferenceClientRect: null, 2082 hideOnClick: true, 2083 ignoreAttributes: false, 2084 interactive: false, 2085 interactiveBorder: 2, 2086 interactiveDebounce: 0, 2087 moveTransition: '', 2088 offset: [0, 10], 2089 onAfterUpdate: function onAfterUpdate() {}, 2090 onBeforeUpdate: function onBeforeUpdate() {}, 2091 onCreate: function onCreate() {}, 2092 onDestroy: function onDestroy() {}, 2093 onHidden: function onHidden() {}, 2094 onHide: function onHide() {}, 2095 onMount: function onMount() {}, 2096 onShow: function onShow() {}, 2097 onShown: function onShown() {}, 2098 onTrigger: function onTrigger() {}, 2099 onUntrigger: function onUntrigger() {}, 2100 onClickOutside: function onClickOutside() {}, 2101 placement: 'top', 2102 plugins: [], 2103 popperOptions: {}, 2104 render: null, 2105 showOnCreate: false, 2106 touch: true, 2107 trigger: 'mouseenter focus', 2108 triggerTarget: null 2109 }, pluginProps, renderProps); 2110 var defaultKeys = Object.keys(defaultProps); 2111 2112 var setDefaultProps = function setDefaultProps(partialProps) { 2113 var keys = Object.keys(partialProps); 2114 keys.forEach(function (key) { 2115 defaultProps[key] = partialProps[key]; 2116 }); 2117 }; 2118 2119 function getExtendedPassedProps(passedProps) { 2120 var plugins = passedProps.plugins || []; 2121 var pluginProps = plugins.reduce(function (acc, plugin) { 2122 var name = plugin.name, 2123 defaultValue = plugin.defaultValue; 2124 2125 if (name) { 2126 var _name; 2127 2128 acc[name] = passedProps[name] !== undefined ? passedProps[name] : (_name = defaultProps[name]) != null ? _name : defaultValue; 2129 } 2130 2131 return acc; 2132 }, {}); 2133 return Object.assign({}, passedProps, pluginProps); 2134 } 2135 2136 function getDataAttributeProps(reference, plugins) { 2137 var propKeys = plugins ? Object.keys(getExtendedPassedProps(Object.assign({}, defaultProps, { 2138 plugins: plugins 2139 }))) : defaultKeys; 2140 var props = propKeys.reduce(function (acc, key) { 2141 var valueAsString = (reference.getAttribute("data-tippy-" + key) || '').trim(); 2142 2143 if (!valueAsString) { 2144 return acc; 2145 } 2146 2147 if (key === 'content') { 2148 acc[key] = valueAsString; 2149 } else { 2150 try { 2151 acc[key] = JSON.parse(valueAsString); 2152 } catch (e) { 2153 acc[key] = valueAsString; 2154 } 2155 } 2156 2157 return acc; 2158 }, {}); 2159 return props; 2160 } 2161 2162 function evaluateProps(reference, props) { 2163 var out = Object.assign({}, props, { 2164 content: invokeWithArgsOrReturn(props.content, [reference]) 2165 }, props.ignoreAttributes ? {} : getDataAttributeProps(reference, props.plugins)); 2166 out.aria = Object.assign({}, defaultProps.aria, out.aria); 2167 out.aria = { 2168 expanded: out.aria.expanded === 'auto' ? props.interactive : out.aria.expanded, 2169 content: out.aria.content === 'auto' ? props.interactive ? null : 'describedby' : out.aria.content 2170 }; 2171 return out; 2172 } 2173 2174 var innerHTML = function innerHTML() { 2175 return 'innerHTML'; 2176 }; 2177 2178 function dangerouslySetInnerHTML(element, html) { 2179 element[innerHTML()] = html; 2180 } 2181 2182 function createArrowElement(value) { 2183 var arrow = div(); 2184 2185 if (value === true) { 2186 arrow.className = ARROW_CLASS; 2187 } else { 2188 arrow.className = SVG_ARROW_CLASS; 2189 2190 if (isElement(value)) { 2191 arrow.appendChild(value); 2192 } else { 2193 dangerouslySetInnerHTML(arrow, value); 2194 } 2195 } 2196 2197 return arrow; 2198 } 2199 2200 function setContent(content, props) { 2201 if (isElement(props.content)) { 2202 dangerouslySetInnerHTML(content, ''); 2203 content.appendChild(props.content); 2204 } else if (typeof props.content !== 'function') { 2205 if (props.allowHTML) { 2206 dangerouslySetInnerHTML(content, props.content); 2207 } else { 2208 content.textContent = props.content; 2209 } 2210 } 2211 } 2212 2213 function getChildren(popper) { 2214 var box = popper.firstElementChild; 2215 var boxChildren = arrayFrom(box.children); 2216 return { 2217 box: box, 2218 content: boxChildren.find(function (node) { 2219 return node.classList.contains(CONTENT_CLASS); 2220 }), 2221 arrow: boxChildren.find(function (node) { 2222 return node.classList.contains(ARROW_CLASS) || node.classList.contains(SVG_ARROW_CLASS); 2223 }), 2224 backdrop: boxChildren.find(function (node) { 2225 return node.classList.contains(BACKDROP_CLASS); 2226 }) 2227 }; 2228 } 2229 2230 function render(instance) { 2231 var popper = div(); 2232 var box = div(); 2233 box.className = BOX_CLASS; 2234 box.setAttribute('data-state', 'hidden'); 2235 box.setAttribute('tabindex', '-1'); 2236 var content = div(); 2237 content.className = CONTENT_CLASS; 2238 content.setAttribute('data-state', 'hidden'); 2239 setContent(content, instance.props); 2240 popper.appendChild(box); 2241 box.appendChild(content); 2242 onUpdate(instance.props, instance.props); 2243 2244 function onUpdate(prevProps, nextProps) { 2245 var _getChildren = getChildren(popper), 2246 box = _getChildren.box, 2247 content = _getChildren.content, 2248 arrow = _getChildren.arrow; 2249 2250 if (nextProps.theme) { 2251 box.setAttribute('data-theme', nextProps.theme); 2252 } else { 2253 box.removeAttribute('data-theme'); 2254 } 2255 2256 if (typeof nextProps.animation === 'string') { 2257 box.setAttribute('data-animation', nextProps.animation); 2258 } else { 2259 box.removeAttribute('data-animation'); 2260 } 2261 2262 if (nextProps.inertia) { 2263 box.setAttribute('data-inertia', ''); 2264 } else { 2265 box.removeAttribute('data-inertia'); 2266 } 2267 2268 box.style.maxWidth = typeof nextProps.maxWidth === 'number' ? nextProps.maxWidth + "px" : nextProps.maxWidth; 2269 2270 if (nextProps.role) { 2271 box.setAttribute('role', nextProps.role); 2272 } else { 2273 box.removeAttribute('role'); 2274 } 2275 2276 if (prevProps.content !== nextProps.content || prevProps.allowHTML !== nextProps.allowHTML) { 2277 setContent(content, instance.props); 2278 } 2279 2280 if (nextProps.arrow) { 2281 if (!arrow) { 2282 box.appendChild(createArrowElement(nextProps.arrow)); 2283 } else if (prevProps.arrow !== nextProps.arrow) { 2284 box.removeChild(arrow); 2285 box.appendChild(createArrowElement(nextProps.arrow)); 2286 } 2287 } else if (arrow) { 2288 box.removeChild(arrow); 2289 } 2290 } 2291 2292 return { 2293 popper: popper, 2294 onUpdate: onUpdate 2295 }; 2296 } // Runtime check to identify if the render function is the default one; this 2297 // way we can apply default CSS transitions logic and it can be tree-shaken away 2298 2299 2300 render.$$tippy = true; 2301 var idCounter = 1; 2302 var mouseMoveListeners = []; // Used by `hideAll()` 2303 2304 var mountedInstances = []; 2305 2306 function createTippy(reference, passedProps) { 2307 var props = evaluateProps(reference, Object.assign({}, defaultProps, getExtendedPassedProps(removeUndefinedProps(passedProps)))); // =========================================================================== 2308 // 🔒 Private members 2309 // =========================================================================== 2310 2311 var showTimeout; 2312 var hideTimeout; 2313 var scheduleHideAnimationFrame; 2314 var isVisibleFromClick = false; 2315 var didHideDueToDocumentMouseDown = false; 2316 var didTouchMove = false; 2317 var ignoreOnFirstUpdate = false; 2318 var lastTriggerEvent; 2319 var currentTransitionEndListener; 2320 var onFirstUpdate; 2321 var listeners = []; 2322 var debouncedOnMouseMove = debounce(onMouseMove, props.interactiveDebounce); 2323 var currentTarget; // =========================================================================== 2324 // 🔑 Public members 2325 // =========================================================================== 2326 2327 var id = idCounter++; 2328 var popperInstance = null; 2329 var plugins = unique(props.plugins); 2330 var state = { 2331 // Is the instance currently enabled? 2332 isEnabled: true, 2333 // Is the tippy currently showing and not transitioning out? 2334 isVisible: false, 2335 // Has the instance been destroyed? 2336 isDestroyed: false, 2337 // Is the tippy currently mounted to the DOM? 2338 isMounted: false, 2339 // Has the tippy finished transitioning in? 2340 isShown: false 2341 }; 2342 var instance = { 2343 // properties 2344 id: id, 2345 reference: reference, 2346 popper: div(), 2347 popperInstance: popperInstance, 2348 props: props, 2349 state: state, 2350 plugins: plugins, 2351 // methods 2352 clearDelayTimeouts: clearDelayTimeouts, 2353 setProps: setProps, 2354 setContent: setContent, 2355 show: show, 2356 hide: hide, 2357 hideWithInteractivity: hideWithInteractivity, 2358 enable: enable, 2359 disable: disable, 2360 unmount: unmount, 2361 destroy: destroy 2362 }; // TODO: Investigate why this early return causes a TDZ error in the tests — 2363 // it doesn't seem to happen in the browser 2364 2365 /* istanbul ignore if */ 2366 2367 if (!props.render) { 2368 return instance; 2369 } // =========================================================================== 2370 // Initial mutations 2371 // =========================================================================== 2372 2373 2374 var _props$render = props.render(instance), 2375 popper = _props$render.popper, 2376 onUpdate = _props$render.onUpdate; 2377 2378 popper.setAttribute('data-tippy-root', ''); 2379 popper.id = "tippy-" + instance.id; 2380 instance.popper = popper; 2381 reference._tippy = instance; 2382 popper._tippy = instance; 2383 var pluginsHooks = plugins.map(function (plugin) { 2384 return plugin.fn(instance); 2385 }); 2386 var hasAriaExpanded = reference.hasAttribute('aria-expanded'); 2387 addListeners(); 2388 handleAriaExpandedAttribute(); 2389 handleStyles(); 2390 invokeHook('onCreate', [instance]); 2391 2392 if (props.showOnCreate) { 2393 scheduleShow(); 2394 } // Prevent a tippy with a delay from hiding if the cursor left then returned 2395 // before it started hiding 2396 2397 2398 popper.addEventListener('mouseenter', function () { 2399 if (instance.props.interactive && instance.state.isVisible) { 2400 instance.clearDelayTimeouts(); 2401 } 2402 }); 2403 popper.addEventListener('mouseleave', function () { 2404 if (instance.props.interactive && instance.props.trigger.indexOf('mouseenter') >= 0) { 2405 getDocument().addEventListener('mousemove', debouncedOnMouseMove); 2406 } 2407 }); 2408 return instance; // =========================================================================== 2409 // 🔒 Private methods 2410 // =========================================================================== 2411 2412 function getNormalizedTouchSettings() { 2413 var touch = instance.props.touch; 2414 return Array.isArray(touch) ? touch : [touch, 0]; 2415 } 2416 2417 function getIsCustomTouchBehavior() { 2418 return getNormalizedTouchSettings()[0] === 'hold'; 2419 } 2420 2421 function getIsDefaultRenderFn() { 2422 var _instance$props$rende; // @ts-ignore 2423 2424 2425 return !!((_instance$props$rende = instance.props.render) != null && _instance$props$rende.$$tippy); 2426 } 2427 2428 function getCurrentTarget() { 2429 return currentTarget || reference; 2430 } 2431 2432 function getDocument() { 2433 var parent = getCurrentTarget().parentNode; 2434 return parent ? getOwnerDocument(parent) : document; 2435 } 2436 2437 function getDefaultTemplateChildren() { 2438 return getChildren(popper); 2439 } 2440 2441 function getDelay(isShow) { 2442 // For touch or keyboard input, force `0` delay for UX reasons 2443 // Also if the instance is mounted but not visible (transitioning out), 2444 // ignore delay 2445 if (instance.state.isMounted && !instance.state.isVisible || currentInput.isTouch || lastTriggerEvent && lastTriggerEvent.type === 'focus') { 2446 return 0; 2447 } 2448 2449 return getValueAtIndexOrReturn(instance.props.delay, isShow ? 0 : 1, defaultProps.delay); 2450 } 2451 2452 function handleStyles(fromHide) { 2453 if (fromHide === void 0) { 2454 fromHide = false; 2455 } 2456 2457 popper.style.pointerEvents = instance.props.interactive && !fromHide ? '' : 'none'; 2458 popper.style.zIndex = "" + instance.props.zIndex; 2459 } 2460 2461 function invokeHook(hook, args, shouldInvokePropsHook) { 2462 if (shouldInvokePropsHook === void 0) { 2463 shouldInvokePropsHook = true; 2464 } 2465 2466 pluginsHooks.forEach(function (pluginHooks) { 2467 if (pluginHooks[hook]) { 2468 pluginHooks[hook].apply(pluginHooks, args); 2469 } 2470 }); 2471 2472 if (shouldInvokePropsHook) { 2473 var _instance$props; 2474 2475 (_instance$props = instance.props)[hook].apply(_instance$props, args); 2476 } 2477 } 2478 2479 function handleAriaContentAttribute() { 2480 var aria = instance.props.aria; 2481 2482 if (!aria.content) { 2483 return; 2484 } 2485 2486 var attr = "aria-" + aria.content; 2487 var id = popper.id; 2488 var nodes = normalizeToArray(instance.props.triggerTarget || reference); 2489 nodes.forEach(function (node) { 2490 var currentValue = node.getAttribute(attr); 2491 2492 if (instance.state.isVisible) { 2493 node.setAttribute(attr, currentValue ? currentValue + " " + id : id); 2494 } else { 2495 var nextValue = currentValue && currentValue.replace(id, '').trim(); 2496 2497 if (nextValue) { 2498 node.setAttribute(attr, nextValue); 2499 } else { 2500 node.removeAttribute(attr); 2501 } 2502 } 2503 }); 2504 } 2505 2506 function handleAriaExpandedAttribute() { 2507 if (hasAriaExpanded || !instance.props.aria.expanded) { 2508 return; 2509 } 2510 2511 var nodes = normalizeToArray(instance.props.triggerTarget || reference); 2512 nodes.forEach(function (node) { 2513 if (instance.props.interactive) { 2514 node.setAttribute('aria-expanded', instance.state.isVisible && node === getCurrentTarget() ? 'true' : 'false'); 2515 } else { 2516 node.removeAttribute('aria-expanded'); 2517 } 2518 }); 2519 } 2520 2521 function cleanupInteractiveMouseListeners() { 2522 getDocument().removeEventListener('mousemove', debouncedOnMouseMove); 2523 mouseMoveListeners = mouseMoveListeners.filter(function (listener) { 2524 return listener !== debouncedOnMouseMove; 2525 }); 2526 } 2527 2528 function onDocumentPress(event) { 2529 // Moved finger to scroll instead of an intentional tap outside 2530 if (currentInput.isTouch) { 2531 if (didTouchMove || event.type === 'mousedown') { 2532 return; 2533 } 2534 } 2535 2536 var actualTarget = event.composedPath && event.composedPath()[0] || event.target; // Clicked on interactive popper 2537 2538 if (instance.props.interactive && actualContains(popper, actualTarget)) { 2539 return; 2540 } // Clicked on the event listeners target 2541 2542 2543 if (normalizeToArray(instance.props.triggerTarget || reference).some(function (el) { 2544 return actualContains(el, actualTarget); 2545 })) { 2546 if (currentInput.isTouch) { 2547 return; 2548 } 2549 2550 if (instance.state.isVisible && instance.props.trigger.indexOf('click') >= 0) { 2551 return; 2552 } 2553 } else { 2554 invokeHook('onClickOutside', [instance, event]); 2555 } 2556 2557 if (instance.props.hideOnClick === true) { 2558 instance.clearDelayTimeouts(); 2559 instance.hide(); // `mousedown` event is fired right before `focus` if pressing the 2560 // currentTarget. This lets a tippy with `focus` trigger know that it 2561 // should not show 2562 2563 didHideDueToDocumentMouseDown = true; 2564 setTimeout(function () { 2565 didHideDueToDocumentMouseDown = false; 2566 }); // The listener gets added in `scheduleShow()`, but this may be hiding it 2567 // before it shows, and hide()'s early bail-out behavior can prevent it 2568 // from being cleaned up 2569 2570 if (!instance.state.isMounted) { 2571 removeDocumentPress(); 2572 } 2573 } 2574 } 2575 2576 function onTouchMove() { 2577 didTouchMove = true; 2578 } 2579 2580 function onTouchStart() { 2581 didTouchMove = false; 2582 } 2583 2584 function addDocumentPress() { 2585 var doc = getDocument(); 2586 doc.addEventListener('mousedown', onDocumentPress, true); 2587 doc.addEventListener('touchend', onDocumentPress, TOUCH_OPTIONS); 2588 doc.addEventListener('touchstart', onTouchStart, TOUCH_OPTIONS); 2589 doc.addEventListener('touchmove', onTouchMove, TOUCH_OPTIONS); 2590 } 2591 2592 function removeDocumentPress() { 2593 var doc = getDocument(); 2594 doc.removeEventListener('mousedown', onDocumentPress, true); 2595 doc.removeEventListener('touchend', onDocumentPress, TOUCH_OPTIONS); 2596 doc.removeEventListener('touchstart', onTouchStart, TOUCH_OPTIONS); 2597 doc.removeEventListener('touchmove', onTouchMove, TOUCH_OPTIONS); 2598 } 2599 2600 function onTransitionedOut(duration, callback) { 2601 onTransitionEnd(duration, function () { 2602 if (!instance.state.isVisible && popper.parentNode && popper.parentNode.contains(popper)) { 2603 callback(); 2604 } 2605 }); 2606 } 2607 2608 function onTransitionedIn(duration, callback) { 2609 onTransitionEnd(duration, callback); 2610 } 2611 2612 function onTransitionEnd(duration, callback) { 2613 var box = getDefaultTemplateChildren().box; 2614 2615 function listener(event) { 2616 if (event.target === box) { 2617 updateTransitionEndListener(box, 'remove', listener); 2618 callback(); 2619 } 2620 } // Make callback synchronous if duration is 0 2621 // `transitionend` won't fire otherwise 2622 2623 2624 if (duration === 0) { 2625 return callback(); 2626 } 2627 2628 updateTransitionEndListener(box, 'remove', currentTransitionEndListener); 2629 updateTransitionEndListener(box, 'add', listener); 2630 currentTransitionEndListener = listener; 2631 } 2632 2633 function on(eventType, handler, options) { 2634 if (options === void 0) { 2635 options = false; 2636 } 2637 2638 var nodes = normalizeToArray(instance.props.triggerTarget || reference); 2639 nodes.forEach(function (node) { 2640 node.addEventListener(eventType, handler, options); 2641 listeners.push({ 2642 node: node, 2643 eventType: eventType, 2644 handler: handler, 2645 options: options 2646 }); 2647 }); 2648 } 2649 2650 function addListeners() { 2651 if (getIsCustomTouchBehavior()) { 2652 on('touchstart', onTrigger, { 2653 passive: true 2654 }); 2655 on('touchend', onMouseLeave, { 2656 passive: true 2657 }); 2658 } 2659 2660 splitBySpaces(instance.props.trigger).forEach(function (eventType) { 2661 if (eventType === 'manual') { 2662 return; 2663 } 2664 2665 on(eventType, onTrigger); 2666 2667 switch (eventType) { 2668 case 'mouseenter': 2669 on('mouseleave', onMouseLeave); 2670 break; 2671 2672 case 'focus': 2673 on(isIE11 ? 'focusout' : 'blur', onBlurOrFocusOut); 2674 break; 2675 2676 case 'focusin': 2677 on('focusout', onBlurOrFocusOut); 2678 break; 2679 } 2680 }); 2681 } 2682 2683 function removeListeners() { 2684 listeners.forEach(function (_ref) { 2685 var node = _ref.node, 2686 eventType = _ref.eventType, 2687 handler = _ref.handler, 2688 options = _ref.options; 2689 node.removeEventListener(eventType, handler, options); 2690 }); 2691 listeners = []; 2692 } 2693 2694 function onTrigger(event) { 2695 var _lastTriggerEvent; 2696 2697 var shouldScheduleClickHide = false; 2698 2699 if (!instance.state.isEnabled || isEventListenerStopped(event) || didHideDueToDocumentMouseDown) { 2700 return; 2701 } 2702 2703 var wasFocused = ((_lastTriggerEvent = lastTriggerEvent) == null ? void 0 : _lastTriggerEvent.type) === 'focus'; 2704 lastTriggerEvent = event; 2705 currentTarget = event.currentTarget; 2706 handleAriaExpandedAttribute(); 2707 2708 if (!instance.state.isVisible && isMouseEvent(event)) { 2709 // If scrolling, `mouseenter` events can be fired if the cursor lands 2710 // over a new target, but `mousemove` events don't get fired. This 2711 // causes interactive tooltips to get stuck open until the cursor is 2712 // moved 2713 mouseMoveListeners.forEach(function (listener) { 2714 return listener(event); 2715 }); 2716 } // Toggle show/hide when clicking click-triggered tooltips 2717 2718 2719 if (event.type === 'click' && (instance.props.trigger.indexOf('mouseenter') < 0 || isVisibleFromClick) && instance.props.hideOnClick !== false && instance.state.isVisible) { 2720 shouldScheduleClickHide = true; 2721 } else { 2722 scheduleShow(event); 2723 } 2724 2725 if (event.type === 'click') { 2726 isVisibleFromClick = !shouldScheduleClickHide; 2727 } 2728 2729 if (shouldScheduleClickHide && !wasFocused) { 2730 scheduleHide(event); 2731 } 2732 } 2733 2734 function onMouseMove(event) { 2735 var target = event.target; 2736 var isCursorOverReferenceOrPopper = getCurrentTarget().contains(target) || popper.contains(target); 2737 2738 if (event.type === 'mousemove' && isCursorOverReferenceOrPopper) { 2739 return; 2740 } 2741 2742 var popperTreeData = getNestedPopperTree().concat(popper).map(function (popper) { 2743 var _instance$popperInsta; 2744 2745 var instance = popper._tippy; 2746 var state = (_instance$popperInsta = instance.popperInstance) == null ? void 0 : _instance$popperInsta.state; 2747 2748 if (state) { 2749 return { 2750 popperRect: popper.getBoundingClientRect(), 2751 popperState: state, 2752 props: props 2753 }; 2754 } 2755 2756 return null; 2757 }).filter(Boolean); 2758 2759 if (isCursorOutsideInteractiveBorder(popperTreeData, event)) { 2760 cleanupInteractiveMouseListeners(); 2761 scheduleHide(event); 2762 } 2763 } 2764 2765 function onMouseLeave(event) { 2766 var shouldBail = isEventListenerStopped(event) || instance.props.trigger.indexOf('click') >= 0 && isVisibleFromClick; 2767 2768 if (shouldBail) { 2769 return; 2770 } 2771 2772 if (instance.props.interactive) { 2773 instance.hideWithInteractivity(event); 2774 return; 2775 } 2776 2777 scheduleHide(event); 2778 } 2779 2780 function onBlurOrFocusOut(event) { 2781 if (instance.props.trigger.indexOf('focusin') < 0 && event.target !== getCurrentTarget()) { 2782 return; 2783 } // If focus was moved to within the popper 2784 2785 2786 if (instance.props.interactive && event.relatedTarget && popper.contains(event.relatedTarget)) { 2787 return; 2788 } 2789 2790 scheduleHide(event); 2791 } 2792 2793 function isEventListenerStopped(event) { 2794 return currentInput.isTouch ? getIsCustomTouchBehavior() !== event.type.indexOf('touch') >= 0 : false; 2795 } 2796 2797 function createPopperInstance() { 2798 destroyPopperInstance(); 2799 var _instance$props2 = instance.props, 2800 popperOptions = _instance$props2.popperOptions, 2801 placement = _instance$props2.placement, 2802 offset = _instance$props2.offset, 2803 getReferenceClientRect = _instance$props2.getReferenceClientRect, 2804 moveTransition = _instance$props2.moveTransition; 2805 var arrow = getIsDefaultRenderFn() ? getChildren(popper).arrow : null; 2806 var computedReference = getReferenceClientRect ? { 2807 getBoundingClientRect: getReferenceClientRect, 2808 contextElement: getReferenceClientRect.contextElement || getCurrentTarget() 2809 } : reference; 2810 var tippyModifier = { 2811 name: '$$tippy', 2812 enabled: true, 2813 phase: 'beforeWrite', 2814 requires: ['computeStyles'], 2815 fn: function fn(_ref2) { 2816 var state = _ref2.state; 2817 2818 if (getIsDefaultRenderFn()) { 2819 var _getDefaultTemplateCh = getDefaultTemplateChildren(), 2820 box = _getDefaultTemplateCh.box; 2821 2822 ['placement', 'reference-hidden', 'escaped'].forEach(function (attr) { 2823 if (attr === 'placement') { 2824 box.setAttribute('data-placement', state.placement); 2825 } else { 2826 if (state.attributes.popper["data-popper-" + attr]) { 2827 box.setAttribute("data-" + attr, ''); 2828 } else { 2829 box.removeAttribute("data-" + attr); 2830 } 2831 } 2832 }); 2833 state.attributes.popper = {}; 2834 } 2835 } 2836 }; 2837 var modifiers = [{ 2838 name: 'offset', 2839 options: { 2840 offset: offset 2841 } 2842 }, { 2843 name: 'preventOverflow', 2844 options: { 2845 padding: { 2846 top: 2, 2847 bottom: 2, 2848 left: 5, 2849 right: 5 2850 } 2851 } 2852 }, { 2853 name: 'flip', 2854 options: { 2855 padding: 5 2856 } 2857 }, { 2858 name: 'computeStyles', 2859 options: { 2860 adaptive: !moveTransition 2861 } 2862 }, tippyModifier]; 2863 2864 if (getIsDefaultRenderFn() && arrow) { 2865 modifiers.push({ 2866 name: 'arrow', 2867 options: { 2868 element: arrow, 2869 padding: 3 2870 } 2871 }); 2872 } 2873 2874 modifiers.push.apply(modifiers, (popperOptions == null ? void 0 : popperOptions.modifiers) || []); 2875 instance.popperInstance = createPopper(computedReference, popper, Object.assign({}, popperOptions, { 2876 placement: placement, 2877 onFirstUpdate: onFirstUpdate, 2878 modifiers: modifiers 2879 })); 2880 } 2881 2882 function destroyPopperInstance() { 2883 if (instance.popperInstance) { 2884 instance.popperInstance.destroy(); 2885 instance.popperInstance = null; 2886 } 2887 } 2888 2889 function mount() { 2890 var appendTo = instance.props.appendTo; 2891 var parentNode; // By default, we'll append the popper to the triggerTargets's parentNode so 2892 // it's directly after the reference element so the elements inside the 2893 // tippy can be tabbed to 2894 // If there are clipping issues, the user can specify a different appendTo 2895 // and ensure focus management is handled correctly manually 2896 2897 var node = getCurrentTarget(); 2898 2899 if (instance.props.interactive && appendTo === TIPPY_DEFAULT_APPEND_TO || appendTo === 'parent') { 2900 parentNode = node.parentNode; 2901 } else { 2902 parentNode = invokeWithArgsOrReturn(appendTo, [node]); 2903 } // The popper element needs to exist on the DOM before its position can be 2904 // updated as Popper needs to read its dimensions 2905 2906 2907 if (!parentNode.contains(popper)) { 2908 parentNode.appendChild(popper); 2909 } 2910 2911 instance.state.isMounted = true; 2912 createPopperInstance(); 2913 } 2914 2915 function getNestedPopperTree() { 2916 return arrayFrom(popper.querySelectorAll('[data-tippy-root]')); 2917 } 2918 2919 function scheduleShow(event) { 2920 instance.clearDelayTimeouts(); 2921 2922 if (event) { 2923 invokeHook('onTrigger', [instance, event]); 2924 } 2925 2926 addDocumentPress(); 2927 var delay = getDelay(true); 2928 2929 var _getNormalizedTouchSe = getNormalizedTouchSettings(), 2930 touchValue = _getNormalizedTouchSe[0], 2931 touchDelay = _getNormalizedTouchSe[1]; 2932 2933 if (currentInput.isTouch && touchValue === 'hold' && touchDelay) { 2934 delay = touchDelay; 2935 } 2936 2937 if (delay) { 2938 showTimeout = setTimeout(function () { 2939 instance.show(); 2940 }, delay); 2941 } else { 2942 instance.show(); 2943 } 2944 } 2945 2946 function scheduleHide(event) { 2947 instance.clearDelayTimeouts(); 2948 invokeHook('onUntrigger', [instance, event]); 2949 2950 if (!instance.state.isVisible) { 2951 removeDocumentPress(); 2952 return; 2953 } // For interactive tippies, scheduleHide is added to a document.body handler 2954 // from onMouseLeave so must intercept scheduled hides from mousemove/leave 2955 // events when trigger contains mouseenter and click, and the tip is 2956 // currently shown as a result of a click. 2957 2958 2959 if (instance.props.trigger.indexOf('mouseenter') >= 0 && instance.props.trigger.indexOf('click') >= 0 && ['mouseleave', 'mousemove'].indexOf(event.type) >= 0 && isVisibleFromClick) { 2960 return; 2961 } 2962 2963 var delay = getDelay(false); 2964 2965 if (delay) { 2966 hideTimeout = setTimeout(function () { 2967 if (instance.state.isVisible) { 2968 instance.hide(); 2969 } 2970 }, delay); 2971 } else { 2972 // Fixes a `transitionend` problem when it fires 1 frame too 2973 // late sometimes, we don't want hide() to be called. 2974 scheduleHideAnimationFrame = requestAnimationFrame(function () { 2975 instance.hide(); 2976 }); 2977 } 2978 } // =========================================================================== 2979 // 🔑 Public methods 2980 // =========================================================================== 2981 2982 2983 function enable() { 2984 instance.state.isEnabled = true; 2985 } 2986 2987 function disable() { 2988 // Disabling the instance should also hide it 2989 // https://github.com/atomiks/tippy.js-react/issues/106 2990 instance.hide(); 2991 instance.state.isEnabled = false; 2992 } 2993 2994 function clearDelayTimeouts() { 2995 clearTimeout(showTimeout); 2996 clearTimeout(hideTimeout); 2997 cancelAnimationFrame(scheduleHideAnimationFrame); 2998 } 2999 3000 function setProps(partialProps) { 3001 if (instance.state.isDestroyed) { 3002 return; 3003 } 3004 3005 invokeHook('onBeforeUpdate', [instance, partialProps]); 3006 removeListeners(); 3007 var prevProps = instance.props; 3008 var nextProps = evaluateProps(reference, Object.assign({}, prevProps, removeUndefinedProps(partialProps), { 3009 ignoreAttributes: true 3010 })); 3011 instance.props = nextProps; 3012 addListeners(); 3013 3014 if (prevProps.interactiveDebounce !== nextProps.interactiveDebounce) { 3015 cleanupInteractiveMouseListeners(); 3016 debouncedOnMouseMove = debounce(onMouseMove, nextProps.interactiveDebounce); 3017 } // Ensure stale aria-expanded attributes are removed 3018 3019 3020 if (prevProps.triggerTarget && !nextProps.triggerTarget) { 3021 normalizeToArray(prevProps.triggerTarget).forEach(function (node) { 3022 node.removeAttribute('aria-expanded'); 3023 }); 3024 } else if (nextProps.triggerTarget) { 3025 reference.removeAttribute('aria-expanded'); 3026 } 3027 3028 handleAriaExpandedAttribute(); 3029 handleStyles(); 3030 3031 if (onUpdate) { 3032 onUpdate(prevProps, nextProps); 3033 } 3034 3035 if (instance.popperInstance) { 3036 createPopperInstance(); // Fixes an issue with nested tippies if they are all getting re-rendered, 3037 // and the nested ones get re-rendered first. 3038 // https://github.com/atomiks/tippyjs-react/issues/177 3039 // TODO: find a cleaner / more efficient solution(!) 3040 3041 getNestedPopperTree().forEach(function (nestedPopper) { 3042 // React (and other UI libs likely) requires a rAF wrapper as it flushes 3043 // its work in one 3044 requestAnimationFrame(nestedPopper._tippy.popperInstance.forceUpdate); 3045 }); 3046 } 3047 3048 invokeHook('onAfterUpdate', [instance, partialProps]); 3049 } 3050 3051 function setContent(content) { 3052 instance.setProps({ 3053 content: content 3054 }); 3055 } 3056 3057 function show() { 3058 var isAlreadyVisible = instance.state.isVisible; 3059 var isDestroyed = instance.state.isDestroyed; 3060 var isDisabled = !instance.state.isEnabled; 3061 var isTouchAndTouchDisabled = currentInput.isTouch && !instance.props.touch; 3062 var duration = getValueAtIndexOrReturn(instance.props.duration, 0, defaultProps.duration); 3063 3064 if (isAlreadyVisible || isDestroyed || isDisabled || isTouchAndTouchDisabled) { 3065 return; 3066 } // Normalize `disabled` behavior across browsers. 3067 // Firefox allows events on disabled elements, but Chrome doesn't. 3068 // Using a wrapper element (i.e. <span>) is recommended. 3069 3070 3071 if (getCurrentTarget().hasAttribute('disabled')) { 3072 return; 3073 } 3074 3075 invokeHook('onShow', [instance], false); 3076 3077 if (instance.props.onShow(instance) === false) { 3078 return; 3079 } 3080 3081 instance.state.isVisible = true; 3082 3083 if (getIsDefaultRenderFn()) { 3084 popper.style.visibility = 'visible'; 3085 } 3086 3087 handleStyles(); 3088 addDocumentPress(); 3089 3090 if (!instance.state.isMounted) { 3091 popper.style.transition = 'none'; 3092 } // If flipping to the opposite side after hiding at least once, the 3093 // animation will use the wrong placement without resetting the duration 3094 3095 3096 if (getIsDefaultRenderFn()) { 3097 var _getDefaultTemplateCh2 = getDefaultTemplateChildren(), 3098 box = _getDefaultTemplateCh2.box, 3099 content = _getDefaultTemplateCh2.content; 3100 3101 setTransitionDuration([box, content], 0); 3102 } 3103 3104 onFirstUpdate = function onFirstUpdate() { 3105 var _instance$popperInsta2; 3106 3107 if (!instance.state.isVisible || ignoreOnFirstUpdate) { 3108 return; 3109 } 3110 3111 ignoreOnFirstUpdate = true; // reflow 3112 3113 void popper.offsetHeight; 3114 popper.style.transition = instance.props.moveTransition; 3115 3116 if (getIsDefaultRenderFn() && instance.props.animation) { 3117 var _getDefaultTemplateCh3 = getDefaultTemplateChildren(), 3118 _box = _getDefaultTemplateCh3.box, 3119 _content = _getDefaultTemplateCh3.content; 3120 3121 setTransitionDuration([_box, _content], duration); 3122 setVisibilityState([_box, _content], 'visible'); 3123 } 3124 3125 handleAriaContentAttribute(); 3126 handleAriaExpandedAttribute(); 3127 pushIfUnique(mountedInstances, instance); // certain modifiers (e.g. `maxSize`) require a second update after the 3128 // popper has been positioned for the first time 3129 3130 (_instance$popperInsta2 = instance.popperInstance) == null ? void 0 : _instance$popperInsta2.forceUpdate(); 3131 invokeHook('onMount', [instance]); 3132 3133 if (instance.props.animation && getIsDefaultRenderFn()) { 3134 onTransitionedIn(duration, function () { 3135 instance.state.isShown = true; 3136 invokeHook('onShown', [instance]); 3137 }); 3138 } 3139 }; 3140 3141 mount(); 3142 } 3143 3144 function hide() { 3145 var isAlreadyHidden = !instance.state.isVisible; 3146 var isDestroyed = instance.state.isDestroyed; 3147 var isDisabled = !instance.state.isEnabled; 3148 var duration = getValueAtIndexOrReturn(instance.props.duration, 1, defaultProps.duration); 3149 3150 if (isAlreadyHidden || isDestroyed || isDisabled) { 3151 return; 3152 } 3153 3154 invokeHook('onHide', [instance], false); 3155 3156 if (instance.props.onHide(instance) === false) { 3157 return; 3158 } 3159 3160 instance.state.isVisible = false; 3161 instance.state.isShown = false; 3162 ignoreOnFirstUpdate = false; 3163 isVisibleFromClick = false; 3164 3165 if (getIsDefaultRenderFn()) { 3166 popper.style.visibility = 'hidden'; 3167 } 3168 3169 cleanupInteractiveMouseListeners(); 3170 removeDocumentPress(); 3171 handleStyles(true); 3172 3173 if (getIsDefaultRenderFn()) { 3174 var _getDefaultTemplateCh4 = getDefaultTemplateChildren(), 3175 box = _getDefaultTemplateCh4.box, 3176 content = _getDefaultTemplateCh4.content; 3177 3178 if (instance.props.animation) { 3179 setTransitionDuration([box, content], duration); 3180 setVisibilityState([box, content], 'hidden'); 3181 } 3182 } 3183 3184 handleAriaContentAttribute(); 3185 handleAriaExpandedAttribute(); 3186 3187 if (instance.props.animation) { 3188 if (getIsDefaultRenderFn()) { 3189 onTransitionedOut(duration, instance.unmount); 3190 } 3191 } else { 3192 instance.unmount(); 3193 } 3194 } 3195 3196 function hideWithInteractivity(event) { 3197 getDocument().addEventListener('mousemove', debouncedOnMouseMove); 3198 pushIfUnique(mouseMoveListeners, debouncedOnMouseMove); 3199 debouncedOnMouseMove(event); 3200 } 3201 3202 function unmount() { 3203 if (instance.state.isVisible) { 3204 instance.hide(); 3205 } 3206 3207 if (!instance.state.isMounted) { 3208 return; 3209 } 3210 3211 destroyPopperInstance(); // If a popper is not interactive, it will be appended outside the popper 3212 // tree by default. This seems mainly for interactive tippies, but we should 3213 // find a workaround if possible 3214 3215 getNestedPopperTree().forEach(function (nestedPopper) { 3216 nestedPopper._tippy.unmount(); 3217 }); 3218 3219 if (popper.parentNode) { 3220 popper.parentNode.removeChild(popper); 3221 } 3222 3223 mountedInstances = mountedInstances.filter(function (i) { 3224 return i !== instance; 3225 }); 3226 instance.state.isMounted = false; 3227 invokeHook('onHidden', [instance]); 3228 } 3229 3230 function destroy() { 3231 if (instance.state.isDestroyed) { 3232 return; 3233 } 3234 3235 instance.clearDelayTimeouts(); 3236 instance.unmount(); 3237 removeListeners(); 3238 delete reference._tippy; 3239 instance.state.isDestroyed = true; 3240 invokeHook('onDestroy', [instance]); 3241 } 3242 } 3243 3244 function tippy(targets, optionalProps) { 3245 if (optionalProps === void 0) { 3246 optionalProps = {}; 3247 } 3248 3249 var plugins = defaultProps.plugins.concat(optionalProps.plugins || []); 3250 bindGlobalEventListeners(); 3251 var passedProps = Object.assign({}, optionalProps, { 3252 plugins: plugins 3253 }); 3254 var elements = getArrayOfElements(targets); 3255 var instances = elements.reduce(function (acc, reference) { 3256 var instance = reference && createTippy(reference, passedProps); 3257 3258 if (instance) { 3259 acc.push(instance); 3260 } 3261 3262 return acc; 3263 }, []); 3264 return isElement(targets) ? instances[0] : instances; 3265 } 3266 3267 tippy.defaultProps = defaultProps; 3268 tippy.setDefaultProps = setDefaultProps; 3269 tippy.currentInput = currentInput; // every time the popper is destroyed (i.e. a new target), removing the styles 3270 // and causing transitions to break for singletons when the console is open, but 3271 // most notably for non-transform styles being used, `gpuAcceleration: false`. 3272 3273 Object.assign({}, applyStyles$1, { 3274 effect: function effect(_ref) { 3275 var state = _ref.state; 3276 var initialStyles = { 3277 popper: { 3278 position: state.options.strategy, 3279 left: '0', 3280 top: '0', 3281 margin: '0' 3282 }, 3283 arrow: { 3284 position: 'absolute' 3285 }, 3286 reference: {} 3287 }; 3288 Object.assign(state.elements.popper.style, initialStyles.popper); 3289 state.styles = initialStyles; 3290 3291 if (state.elements.arrow) { 3292 Object.assign(state.elements.arrow.style, initialStyles.arrow); 3293 } // intentionally return no cleanup function 3294 // return () => { ... } 3295 3296 } 3297 }); 3298 tippy.setDefaultProps({ 3299 render: render 3300 }); // import 'tippy.js/dist/tippy.css'; 3301 3302 /** 3303 * Utility methods 3304 */ 3305 // Determine element visibility 3306 3307 const isElementHidden = $el => { 3308 if ($el.getAttribute('hidden') || $el.offsetWidth === 0 && $el.offsetHeight === 0) { 3309 return true; 3310 } else { 3311 const compStyles = getComputedStyle($el); 3312 return compStyles.getPropertyValue('display') === 'none'; 3313 } 3314 }; // Escape HTML, encode HTML symbols 3315 3316 3317 const escapeHTML = text => { 3318 const $div = document.createElement('div'); 3319 $div.textContent = text; 3320 return $div.innerHTML.replaceAll('"', '"').replaceAll("'", ''').replaceAll("`", '`'); 3321 }; 3322 /** 3323 * Jooa11y Translation object 3324 */ 3325 3326 3327 const Lang = { 3328 langStrings: {}, 3329 addI18n: function (strings) { 3330 this.langStrings = strings; 3331 }, 3332 _: function (string) { 3333 return this.translate(string); 3334 }, 3335 sprintf: function (string, ...args) { 3336 let transString = this._(string); 3337 3338 if (args && args.length) { 3339 args.forEach(arg => { 3340 transString = transString.replace(/%\([a-zA-z]+\)/, arg); 3341 }); 3342 } 3343 3344 return transString; 3345 }, 3346 translate: function (string) { 3347 return this.langStrings[string] || string; 3348 } 3349 }; 3350 /** 3351 * Jooa11y default options 3352 */ 3353 3354 const defaultOptions = { 3355 langCode: 'en', 3356 // Target area to scan. 3357 checkRoot: 'main', 3358 // A content container 3359 // Readability configuration. 3360 readabilityRoot: 'main', 3361 readabilityLang: 'en', 3362 // Inclusions and exclusions. Use commas to seperate classes or elements. 3363 containerIgnore: '.jooa11y-ignore', 3364 // Ignore specific regions. 3365 outlineIgnore: '', 3366 // Exclude headings from outline panel. 3367 headerIgnore: '', 3368 // Ignore specific headings. E.g. "h1.jumbotron-heading" 3369 imageIgnore: '', 3370 // Ignore specific images. 3371 linkIgnore: '', 3372 // Ignore specific links. 3373 linkIgnoreSpan: 'noscript, span.sr-only-example', 3374 // Ignore specific classes within links. Example: <a href="#">learn more <span class="sr-only-example">(opens new tab)</span></a>. 3375 linksToFlag: '', 3376 // Links you don't want your content editors pointing to (e.g. development environments). 3377 // Embedded content. 3378 videoContent: "video, [src*='youtube.com'], [src*='vimeo.com'], [src*='yuja.com'], [src*='panopto.com']", 3379 audioContent: "audio, [src*='soundcloud.com'], [src*='simplecast.com'], [src*='podbean.com'], [src*='buzzsprout.com'], [src*='blubrry.com'], [src*='transistor.fm'], [src*='fusebox.fm'], [src*='libsyn.com']", 3380 embeddedContent: '', 3381 // Alt Text stop words. 3382 suspiciousAltWords: ['image', 'graphic', 'picture', 'photo'], 3383 placeholderAltStopWords: ['alt', 'image', 'photo', 'decorative', 'photo', 'placeholder', 'placeholder image', 'spacer', '.'], 3384 // Link Text stop words 3385 partialAltStopWords: ['click', 'click here', 'click here for more', 'click here to learn more', 'click here to learn more.', 'check out', 'download', 'download here', 'download here.', 'find out', 'find out more', 'find out more.', 'form', 'here', 'here.', 'info', 'information', 'link', 'learn', 'learn more', 'learn more.', 'learn to', 'more', 'page', 'paper', 'read more', 'read', 'read this', 'this', 'this page', 'this page.', 'this website', 'this website.', 'view', 'view our', 'website', '.'], 3386 warningAltWords: ['< ', ' >', 'click here'], 3387 // Link Text (Advanced) 3388 newWindowPhrases: ['external', 'new tab', 'new window', 'pop-up', 'pop up'], 3389 // Link Text (Advanced). Only some items in list would need to be translated. 3390 fileTypePhrases: ['document', 'pdf', 'doc', 'docx', 'word', 'mp3', 'ppt', 'text', 'pptx', 'powerpoint', 'txt', 'exe', 'dmg', 'rtf', 'install', 'windows', 'macos', 'spreadsheet', 'worksheet', 'csv', 'xls', 'xlsx', 'video', 'mp4', 'mov', 'avi'] 3391 }; 3392 defaultOptions.embeddedContent = `$defaultOptions.videoContent}, $defaultOptions.audioContent}`; 3393 /** 3394 * Load and validate options 3395 * 3396 * @param {Jooa11y} instance 3397 * @param {Object} customOptions 3398 * @returns {Object} 3399 */ 3400 3401 const loadOptions = (instance, customOptions) => { 3402 const options = customOptions ? Object.assign(defaultOptions, customOptions) : defaultOptions; // Check required options 3403 3404 ['langCode', 'checkRoot'].forEach(option => { 3405 if (!options[option]) { 3406 throw new Error(`Option [$option}] is required`); 3407 } 3408 }); 3409 3410 if (!options.readabilityRoot) { 3411 options.readabilityRoot = options.checkRoot; 3412 } // Container ignores apply to self and children. 3413 3414 3415 if (options.containerIgnore) { 3416 let containerSelectors = options.containerIgnore.split(',').map(el => { 3417 return `$el} *, $el}`; 3418 }); 3419 options.containerIgnore = '[aria-hidden="true"], #jooa11y-container *, .jooa11y-instance *, ' + containerSelectors.join(', '); 3420 } else { 3421 options.containerIgnore = '[aria-hidden="true"], #jooa11y-container *, .jooa11y-instance *'; 3422 } 3423 3424 instance.containerIgnore = options.containerIgnore; // Images ignore 3425 3426 instance.imageIgnore = instance.containerIgnore + ', [role="presentation"], [src^="https://trck.youvisit.com"]'; 3427 3428 if (options.imageIgnore) { 3429 instance.imageIgnore = options.imageIgnore + ',' + instance.imageIgnore; 3430 } // Ignore specific headings 3431 3432 3433 instance.headerIgnore = options.containerIgnore; 3434 3435 if (options.headerIgnore) { 3436 instance.headerIgnore = options.headerIgnore + ',' + instance.headerIgnore; 3437 } // Links ignore defaults plus jooa11y links. 3438 3439 3440 instance.linkIgnore = instance.containerIgnore + ', [aria-hidden="true"], .anchorjs-link'; 3441 3442 if (options.linkIgnore) { 3443 instance.linkIgnore = options.linkIgnore + ',' + instance.linkIgnore; 3444 } 3445 3446 return options; 3447 }; 3448 /** 3449 * Jooa11y class 3450 */ 3451 3452 3453 class Jooa11y { 3454 constructor(options) { 3455 this.checkAll = async () => { 3456 this.errorCount = 0; 3457 this.warningCount = 0; 3458 this.$root = document.querySelector(this.options.checkRoot); 3459 this.findElements(); //Ruleset checks 3460 3461 this.checkHeaders(); 3462 this.checkLinkText(); 3463 this.checkUnderline(); 3464 this.checkAltText(); 3465 3466 if (localStorage.getItem("jooa11y-remember-contrast") === "On") { 3467 this.checkContrast(); 3468 } 3469 3470 if (localStorage.getItem("jooa11y-remember-labels") === "On") { 3471 this.checkLabels(); 3472 } 3473 3474 if (localStorage.getItem("jooa11y-remember-links-advanced") === "On") { 3475 this.checkLinksAdvanced(); 3476 } 3477 3478 if (localStorage.getItem("jooa11y-remember-readability") === "On") { 3479 this.checkReadability(); 3480 } 3481 3482 this.checkEmbeddedContent(); 3483 this.checkQA(); //Update panel 3484 3485 if (this.panelActive) { 3486 this.resetAll(); 3487 } else { 3488 this.updatePanel(); 3489 } 3490 3491 this.initializeTooltips(); 3492 this.detectOverflow(); 3493 this.nudge(); //Don't show badge when panel is opened. 3494 3495 if (!document.getElementsByClassName('jooa11y-on').length) { 3496 this.updateBadge(); 3497 } 3498 }; 3499 3500 this.nudge = () => { 3501 const jooa11yInstance = document.querySelectorAll('.jooa11y-instance, .jooa11y-instance-inline'); 3502 jooa11yInstance.forEach($el => { 3503 const sibling = $el.nextElementSibling; 3504 3505 if (sibling !== null && (sibling.classList.contains("jooa11y-instance") || sibling.classList.contains("jooa11y-instance-inline"))) { 3506 sibling.querySelector("button").setAttribute("style", "margin: -10px -20px !important;"); 3507 } 3508 }); 3509 }; 3510 3511 this.buildPanel = () => { 3512 const $outlineToggle = document.getElementById("jooa11y-outline-toggle"); 3513 const $outlinePanel = document.getElementById("jooa11y-outline-panel"); 3514 const $outlineList = document.getElementById("jooa11y-outline-list"); 3515 const $settingsToggle = document.getElementById("jooa11y-settings-toggle"); 3516 const $settingsPanel = document.getElementById("jooa11y-settings-panel"); 3517 const $settingsContent = document.getElementById("jooa11y-settings-content"); 3518 const $headingAnnotations = document.querySelectorAll(".jooa11y-heading-label"); //Show outline panel 3519 3520 $outlineToggle.addEventListener('click', () => { 3521 if ($outlineToggle.getAttribute("aria-expanded") === "true") { 3522 $outlineToggle.classList.remove("jooa11y-outline-active"); 3523 $outlinePanel.classList.remove("jooa11y-active"); 3524 $outlineToggle.textContent = Lang._('SHOW_OUTLINE'); 3525 $outlineToggle.setAttribute("aria-expanded", "false"); 3526 localStorage.setItem("jooa11y-remember-outline", "Closed"); 3527 } else { 3528 $outlineToggle.classList.add("jooa11y-outline-active"); 3529 $outlinePanel.classList.add("jooa11y-active"); 3530 $outlineToggle.textContent = Lang._('HIDE_OUTLINE'); 3531 $outlineToggle.setAttribute("aria-expanded", "true"); 3532 localStorage.setItem("jooa11y-remember-outline", "Opened"); 3533 } //Set focus on Page Outline heading for accessibility. 3534 3535 3536 document.querySelector("#jooa11y-outline-header > h2").focus(); //Show heading level annotations. 3537 3538 $headingAnnotations.forEach($el => $el.classList.toggle("jooa11y-label-visible")); //Close Settings panel when Show Outline is active. 3539 3540 $settingsPanel.classList.remove("jooa11y-active"); 3541 $settingsToggle.classList.remove("jooa11y-settings-active"); 3542 $settingsToggle.setAttribute("aria-expanded", "false"); 3543 $settingsToggle.textContent = Lang._('SHOW_SETTINGS'); //Keyboard accessibility fix for scrollable panel content. 3544 3545 if ($outlineList.clientHeight > 250) { 3546 $outlineList.setAttribute("tabindex", "0"); 3547 } 3548 }); //Remember to leave outline open 3549 3550 if (localStorage.getItem("jooa11y-remember-outline") === "Opened") { 3551 $outlineToggle.classList.add("jooa11y-outline-active"); 3552 $outlinePanel.classList.add("jooa11y-active"); 3553 $outlineToggle.textContent = Lang._('HIDE_OUTLINE'); 3554 $outlineToggle.setAttribute("aria-expanded", "true"); 3555 $headingAnnotations.forEach($el => $el.classList.toggle("jooa11y-label-visible")); //Keyboard accessibility fix for scrollable panel content. 3556 3557 if ($outlineList.clientHeight > 250) { 3558 $outlineList.setAttribute("tabindex", "0"); 3559 } 3560 } //Show settings panel 3561 3562 3563 $settingsToggle.addEventListener('click', () => { 3564 if ($settingsToggle.getAttribute("aria-expanded") === "true") { 3565 $settingsToggle.classList.remove("jooa11y-settings-active"); 3566 $settingsPanel.classList.remove("jooa11y-active"); 3567 $settingsToggle.textContent = Lang._('SHOW_SETTINGS'); 3568 $settingsToggle.setAttribute("aria-expanded", "false"); 3569 } else { 3570 $settingsToggle.classList.add("jooa11y-settings-active"); 3571 $settingsPanel.classList.add("jooa11y-active"); 3572 $settingsToggle.textContent = Lang._('HIDE_SETTINGS'); 3573 $settingsToggle.setAttribute("aria-expanded", "true"); 3574 } //Set focus on Settings heading for accessibility. 3575 3576 3577 document.querySelector("#jooa11y-settings-header > h2").focus(); //Close Show Outline panel when Settings is active. 3578 3579 $outlinePanel.classList.remove("jooa11y-active"); 3580 $outlineToggle.classList.remove("jooa11y-outline-active"); 3581 $outlineToggle.setAttribute("aria-expanded", "false"); 3582 $outlineToggle.textContent = Lang._('SHOW_OUTLINE'); 3583 $headingAnnotations.forEach($el => $el.classList.remove("jooa11y-label-visible")); 3584 localStorage.setItem("jooa11y-remember-outline", "Closed"); //Keyboard accessibility fix for scrollable panel content. 3585 3586 if ($settingsContent.clientHeight > 350) { 3587 $settingsContent.setAttribute("tabindex", "0"); 3588 } 3589 }); //Enhanced keyboard accessibility for panel. 3590 3591 document.getElementById('jooa11y-panel-controls').addEventListener('keydown', function (e) { 3592 const $tab = document.querySelectorAll('#jooa11y-outline-toggle[role=tab], #jooa11y-settings-toggle[role=tab]'); 3593 3594 if (e.key === 'ArrowRight') { 3595 for (let i = 0; i < $tab.length; i++) { 3596 if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttribute('aria-expanded') === "false") { 3597 $tab[i + 1].focus(); 3598 e.preventDefault(); 3599 break; 3600 } 3601 } 3602 } 3603 3604 if (e.key === 'ArrowDown') { 3605 for (let i = 0; i < $tab.length; i++) { 3606 if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttribute('aria-expanded') === "false") { 3607 $tab[i + 1].focus(); 3608 e.preventDefault(); 3609 break; 3610 } 3611 } 3612 } 3613 3614 if (e.key === 'ArrowLeft') { 3615 for (let i = $tab.length - 1; i > 0; i--) { 3616 if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttribute('aria-expanded') === "false") { 3617 $tab[i - 1].focus(); 3618 e.preventDefault(); 3619 break; 3620 } 3621 } 3622 } 3623 3624 if (e.key === 'ArrowUp') { 3625 for (let i = $tab.length - 1; i > 0; i--) { 3626 if ($tab[i].getAttribute('aria-expanded') === "true" || $tab[i].getAttribute('aria-expanded') === "false") { 3627 $tab[i - 1].focus(); 3628 e.preventDefault(); 3629 break; 3630 } 3631 } 3632 } 3633 }); 3634 const $closeAlertToggle = document.getElementById("jooa11y-close-alert"); 3635 const $alertPanel = document.getElementById("jooa11y-panel-alert"); 3636 const $alertText = document.getElementById("jooa11y-panel-alert-text"); 3637 const $jooa11ySkipBtn = document.getElementById("jooa11y-cycle-toggle"); 3638 $closeAlertToggle.addEventListener('click', () => { 3639 $alertPanel.classList.remove("jooa11y-active"); 3640 3641 while ($alertText.firstChild) $alertText.removeChild($alertText.firstChild); 3642 3643 document.querySelectorAll('.jooa11y-pulse-border').forEach(el => el.classList.remove('jooa11y-pulse-border')); 3644 $jooa11ySkipBtn.focus(); 3645 }); 3646 }; 3647 3648 this.skipToIssue = () => { 3649 /* Polyfill for scrollTo. scrollTo instead of .animate(), so Jooa11y could use jQuery slim build. Credit: https://stackoverflow.com/a/67108752 & https://github.com/iamdustan/smoothscroll */ 3650 //let reducedMotionQuery = false; 3651 //let scrollBehavior = 'smooth'; 3652 3653 /* 3654 if (!('scrollBehavior' in document.documentElement.style)) { 3655 var js = document.createElement('script'); 3656 js.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/smoothscroll.min.js"; 3657 document.head.appendChild(js); 3658 } 3659 if (!(document.documentMode)) { 3660 if (typeof window.matchMedia === "function") { 3661 reducedMotionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); 3662 } 3663 if (!reducedMotionQuery || reducedMotionQuery.matches) { 3664 scrollBehavior = "auto"; 3665 } 3666 } 3667 */ 3668 let jooa11yBtnLocation = 0; 3669 const findJooa11yBtn = document.querySelectorAll('.jooa11y-btn').length; //Jump to issue using keyboard shortcut. 3670 3671 document.addEventListener('keyup', e => { 3672 if (e.altKey && e.code === "Period" || e.code == "KeyS") { 3673 skipToIssueToggle(); 3674 e.preventDefault(); 3675 } 3676 }); //Jump to issue using click. 3677 3678 const $skipToggle = document.getElementById("jooa11y-cycle-toggle"); 3679 $skipToggle.addEventListener('click', e => { 3680 skipToIssueToggle(); 3681 e.preventDefault(); 3682 }); 3683 3684 const skipToIssueToggle = function () { 3685 //Calculate location of both visible and hidden buttons. 3686 const $findButtons = document.querySelectorAll('.jooa11y-btn'); 3687 const $alertPanel = document.getElementById("jooa11y-panel-alert"); 3688 const $alertText = document.getElementById("jooa11y-panel-alert-text"); 3689 const $alertPanelPreview = document.getElementById("jooa11y-panel-alert-preview"); //const $closeAlertToggle = document.getElementById("jooa11y-close-alert"); 3690 //Mini function: Find visibible parent of hidden element. 3691 3692 const findVisibleParent = ($el, property, value) => { 3693 while ($el !== null) { 3694 const style = window.getComputedStyle($el); 3695 const propValue = style.getPropertyValue(property); 3696 3697 if (propValue === value) { 3698 return $el; 3699 } 3700 3701 $el = $el.parentElement; 3702 } 3703 3704 return null; 3705 }; //Mini function: Calculate top of element. 3706 3707 3708 const offset = $el => { 3709 let rect = $el.getBoundingClientRect(), 3710 scrollTop = window.pageYOffset || document.documentElement.scrollTop; 3711 return { 3712 top: rect.top + scrollTop 3713 }; 3714 }; //'offsetTop' will always return 0 if element is hidden. We rely on offsetTop to determine if element is hidden, although we use 'getBoundingClientRect' to set the scroll position. 3715 3716 3717 let scrollPosition; 3718 let offsetTopPosition = $findButtons[jooa11yBtnLocation].offsetTop; 3719 3720 if (offsetTopPosition === 0) { 3721 let visiblePosition = findVisibleParent($findButtons[jooa11yBtnLocation], 'display', 'none'); 3722 scrollPosition = offset(visiblePosition.previousElementSibling).top - 50; 3723 } else { 3724 scrollPosition = offset($findButtons[jooa11yBtnLocation]).top - 50; 3725 } //Scroll to element if offsetTop is less than or equal to 0. 3726 3727 3728 if (offsetTopPosition >= 0) { 3729 setTimeout(function () { 3730 window.scrollTo({ 3731 top: scrollPosition, 3732 behavior: 'smooth' 3733 }); 3734 }, 1); //Add pulsing border to visible parent of hidden element. 3735 3736 $findButtons.forEach(function ($el) { 3737 const overflowing = findVisibleParent($el, 'display', 'none'); 3738 3739 if (overflowing !== null) { 3740 let hiddenparent = overflowing.previousElementSibling; 3741 hiddenparent.classList.add("jooa11y-pulse-border"); 3742 } 3743 }); 3744 $findButtons[jooa11yBtnLocation].focus(); 3745 } else { 3746 $findButtons[jooa11yBtnLocation].focus(); 3747 } //Alert if element is hidden. 3748 3749 3750 if (offsetTopPosition === 0) { 3751 $alertPanel.classList.add("jooa11y-active"); 3752 $alertText.textContent = `$Lang._('PANEL_STATUS_HIDDEN')}`; 3753 $alertPanelPreview.innerHTML = $findButtons[jooa11yBtnLocation].getAttribute('data-tippy-content'); 3754 } else if (offsetTopPosition < 1) { 3755 $alertPanel.classList.remove("jooa11y-active"); 3756 document.querySelectorAll('.jooa11y-pulse-border').forEach($el => $el.classList.remove('jooa11y-pulse-border')); 3757 } //Reset index so it scrolls back to top of page. 3758 3759 3760 jooa11yBtnLocation += 1; 3761 3762 if (jooa11yBtnLocation >= findJooa11yBtn) { 3763 jooa11yBtnLocation = 0; 3764 } 3765 }; 3766 }; 3767 3768 this.containerIgnore = ''; 3769 this.imageIgnore = ''; 3770 this.headerIgnore = ''; 3771 this.linkIgnore = ''; // Load options 3772 3773 this.options = loadOptions(this, options); //Icon on the main toggle. Easy to replace. 3774 3775 const MainToggleIcon = "<svg role='img' focusable='false' width='35px' height='35px' aria-hidden='true' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path fill='#ffffff' d='M256 48c114.953 0 208 93.029 208 208 0 114.953-93.029 208-208 208-114.953 0-208-93.029-208-208 0-114.953 93.029-208 208-208m0-40C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 56C149.961 64 64 149.961 64 256s85.961 192 192 192 192-85.961 192-192S362.039 64 256 64zm0 44c19.882 0 36 16.118 36 36s-16.118 36-36 36-36-16.118-36-36 16.118-36 36-36zm117.741 98.023c-28.712 6.779-55.511 12.748-82.14 15.807.851 101.023 12.306 123.052 25.037 155.621 3.617 9.26-.957 19.698-10.217 23.315-9.261 3.617-19.699-.957-23.316-10.217-8.705-22.308-17.086-40.636-22.261-78.549h-9.686c-5.167 37.851-13.534 56.208-22.262 78.549-3.615 9.255-14.05 13.836-23.315 10.217-9.26-3.617-13.834-14.056-10.217-23.315 12.713-32.541 24.185-54.541 25.037-155.621-26.629-3.058-53.428-9.027-82.141-15.807-8.6-2.031-13.926-10.648-11.895-19.249s10.647-13.926 19.249-11.895c96.686 22.829 124.283 22.783 220.775 0 8.599-2.03 17.218 3.294 19.249 11.895 2.029 8.601-3.297 17.219-11.897 19.249z'/></svg>"; 3776 const jooa11ycontainer = document.createElement("div"); 3777 jooa11ycontainer.setAttribute("id", "jooa11y-container"); 3778 jooa11ycontainer.setAttribute("role", "region"); 3779 jooa11ycontainer.setAttribute("lang", this.options.langCode); 3780 jooa11ycontainer.setAttribute("aria-label", Lang._('CONTAINER_LABEL')); 3781 let loadContrastPreference = localStorage.getItem("jooa11y-remember-contrast") === "On"; 3782 let loadLabelsPreference = localStorage.getItem("jooa11y-remember-labels") === "On"; 3783 let loadChangeRequestPreference = localStorage.getItem("jooa11y-remember-links-advanced") === "On"; 3784 let loadReadabilityPreference = localStorage.getItem("jooa11y-remember-readability") === "On"; 3785 jooa11ycontainer.innerHTML = //Main toggle button. 3786 `<button type="button" aria-expanded="false" id="jooa11y-toggle" aria-describedby="jooa11y-notification-badge" aria-label="$Lang._('MAIN_TOGGLE_LABEL')}"> 3787 $MainToggleIcon} 3788 <div id="jooa11y-notification-badge"> 3789 <span id="jooa11y-notification-count"></span> 3790 </div> 3791 </button>` + //Start of main container. 3792 `<div id="jooa11y-panel">` + //Page Outline tab. 3793 `<div id="jooa11y-outline-panel" role="tabpanel" aria-labelledby="jooa11y-outline-header"> 3794 <div id="jooa11y-outline-header" class="jooa11y-header-text"> 3795 <h2 tabindex="-1">$Lang._('PAGE_OUTLINE')}</h2> 3796 </div> 3797 <div id="jooa11y-outline-content"> 3798 <ul id="jooa11y-outline-list"></ul> 3799 </div>` + //Readability tab. 3800 `<div id="jooa11y-readability-panel"> 3801 <div id="jooa11y-readability-content"> 3802 <h2 class="jooa11y-header-text-inline">$Lang._('READABILITY')}</h2> 3803 <p id="jooa11y-readability-info"></p> 3804 <ul id="jooa11y-readability-details"></ul> 3805 </div> 3806 </div> 3807 </div>` + //End of Page Outline tab. 3808 //Settings tab. 3809 `<div id="jooa11y-settings-panel" role="tabpanel" aria-labelledby="jooa11y-settings-header"> 3810 <div id="jooa11y-settings-header" class="jooa11y-header-text"> 3811 <h2 tabindex="-1">$Lang._('SETTINGS')}</h2> 3812 </div> 3813 <div id="jooa11y-settings-content"> 3814 <ul id="jooa11y-settings-options"> 3815 <li> 3816 <label id="check-contrast" for="jooa11y-contrast-toggle">$Lang._('CONTRAST')}</label> 3817 <button id="jooa11y-contrast-toggle" 3818 aria-labelledby="check-contrast" 3819 class="jooa11y-settings-switch" 3820 aria-pressed="$loadContrastPreference ? "true" : "false"}">$loadContrastPreference ? Lang._('ON') : Lang._('OFF')}</button> 3821 </li> 3822 <li> 3823 <label id="check-labels" for="jooa11y-labels-toggle">$Lang._('FORM_LABELS')}</label> 3824 <button id="jooa11y-labels-toggle" aria-labelledby="check-labels" class="jooa11y-settings-switch" 3825 aria-pressed="$loadLabelsPreference ? "true" : "false"}">$loadLabelsPreference ? Lang._('ON') : Lang._('OFF')}</button> 3826 </li> 3827 <li> 3828 <label id="check-changerequest" for="jooa11y-links-advanced-toggle">$Lang._('LINKS_ADVANCED')}<span class="jooa11y-badge">AAA</span></label> 3829 <button id="jooa11y-links-advanced-toggle" aria-labelledby="check-changerequest" class="jooa11y-settings-switch" 3830 aria-pressed="$loadChangeRequestPreference ? "true" : "false"}">$loadChangeRequestPreference ? Lang._('ON') : Lang._('OFF')}</button> 3831 </li> 3832 <li> 3833 <label id="check-readability" for="jooa11y-readability-toggle">$Lang._('READABILITY')}<span class="jooa11y-badge">AAA</span></label> 3834 <button id="jooa11y-readability-toggle" aria-labelledby="check-readability" class="jooa11y-settings-switch" 3835 aria-pressed="$loadReadabilityPreference ? "true" : "false"}">$loadReadabilityPreference ? Lang._('ON') : Lang._('OFF')}</button> 3836 </li> 3837 <li> 3838 <label id="dark-mode" for="jooa11y-theme-toggle">$Lang._('DARK_MODE')}</label> 3839 <button id="jooa11y-theme-toggle" aria-labelledby="dark-mode" class="jooa11y-settings-switch"></button> 3840 </li> 3841 </ul> 3842 </div> 3843 </div>` + //Console warning messages. 3844 `<div id="jooa11y-panel-alert"> 3845 <div class="jooa11y-header-text"> 3846 <button id="jooa11y-close-alert" class="jooa11y-close-btn" aria-label="$Lang._('ALERT_CLOSE')}" aria-describedby="jooa11y-alert-heading jooa11y-panel-alert-text"></button> 3847 <h2 id="jooa11y-alert-heading">$Lang._('ALERT_TEXT')}</h2> 3848 </div> 3849 <p id="jooa11y-panel-alert-text"></p> 3850 <div id="jooa11y-panel-alert-preview"></div> 3851 </div>` + //Main panel that conveys state of page. 3852 `<div id="jooa11y-panel-content"> 3853 <button id="jooa11y-cycle-toggle" type="button" aria-label="$Lang._('SHORTCUT_SR')}"> 3854 <div class="jooa11y-panel-icon"></div> 3855 </button> 3856 <div id="jooa11y-panel-text"><p id="jooa11y-status" aria-live="polite"></p></div> 3857 </div>` + //Show Outline & Show Settings button. 3858 `<div id="jooa11y-panel-controls" role="tablist" aria-orientation="horizontal"> 3859 <button type="button" role="tab" aria-expanded="false" id="jooa11y-outline-toggle" aria-controls="jooa11y-outline-panel"> 3860 $Lang._('SHOW_OUTLINE')} 3861 </button> 3862 <button type="button" role="tab" aria-expanded="false" id="jooa11y-settings-toggle" aria-controls="jooa11y-settings-panel"> 3863 $Lang._('SHOW_SETTINGS')} 3864 </button> 3865 <div style="width:35px"></div> 3866 </div>` + //End of main container. 3867 `</div>`; 3868 document.body.append(jooa11ycontainer); //Put before document.ready because of CSS flicker when dark mode is enabled. 3869 3870 this.settingPanelToggles(); // Preload before CheckAll function. 3871 3872 this.jooa11yMainToggle(); 3873 this.sanitizeHTMLandComputeARIA(); 3874 this.initializeJumpToIssueTooltip(); 3875 } //---------------------------------------------------------------------- 3876 // Main toggle button 3877 //---------------------------------------------------------------------- 3878 3879 3880 jooa11yMainToggle() { 3881 //Keeps checker active when navigating between pages until it is toggled off. 3882 const jooa11yToggle = document.getElementById("jooa11y-toggle"); 3883 jooa11yToggle.addEventListener('click', e => { 3884 if (localStorage.getItem("jooa11y-remember-panel") === "Opened") { 3885 localStorage.setItem("jooa11y-remember-panel", "Closed"); 3886 jooa11yToggle.classList.remove("jooa11y-on"); 3887 jooa11yToggle.setAttribute("aria-expanded", "false"); 3888 this.resetAll(); 3889 this.updateBadge(); 3890 e.preventDefault(); 3891 } else { 3892 localStorage.setItem("jooa11y-remember-panel", "Opened"); 3893 jooa11yToggle.classList.add("jooa11y-on"); 3894 jooa11yToggle.setAttribute("aria-expanded", "true"); 3895 this.checkAll(); //Don't show badge when panel is opened. 3896 3897 document.getElementById("jooa11y-notification-badge").style.display = 'none'; 3898 e.preventDefault(); 3899 } 3900 }); //Remember to leave it open 3901 3902 if (localStorage.getItem("jooa11y-remember-panel") === "Opened") { 3903 jooa11yToggle.classList.add("jooa11y-on"); 3904 jooa11yToggle.setAttribute("aria-expanded", "true"); 3905 } //Crudely give a little time to load any other content or slow post-rendered JS, iFrames, etc. 3906 3907 3908 if (jooa11yToggle.classList.contains("jooa11y-on")) { 3909 jooa11yToggle.classList.toggle("loading-jooa11y"); 3910 jooa11yToggle.setAttribute("aria-expanded", "true"); 3911 setTimeout(this.checkAll, 800); 3912 } //Keyboard commands 3913 3914 3915 document.onkeydown = evt => { 3916 evt = evt || window.event; //Escape key to close accessibility checker panel 3917 3918 var isEscape = false; 3919 3920 if ("key" in evt) { 3921 isEscape = evt.key === "Escape" || evt.key === "Esc"; 3922 } else { 3923 isEscape = evt.keyCode === 27; 3924 } 3925 3926 if (isEscape && document.getElementById("jooa11y-panel").classList.contains("jooa11y-active")) { 3927 jooa11yToggle.setAttribute("aria-expanded", "false"); 3928 jooa11yToggle.classList.remove("jooa11y-on"); 3929 jooa11yToggle.click(); 3930 this.resetAll(); 3931 } //Alt + A to open accessibility checker panel 3932 3933 3934 if (evt.altKey && evt.code == "KeyA") { 3935 const jooa11yToggle = document.getElementById("jooa11y-toggle"); 3936 jooa11yToggle.click(); 3937 jooa11yToggle.focus(); 3938 evt.preventDefault(); 3939 } 3940 }; 3941 } // ============================================================ 3942 // Helpers: Sanitize HTML and compute ARIA for hyperlinks 3943 // ============================================================ 3944 3945 3946 sanitizeHTMLandComputeARIA() { 3947 //Helper: Compute alt text on images within a text node. 3948 this.computeTextNodeWithImage = function ($el) { 3949 const imgArray = Array.from($el.querySelectorAll("img")); 3950 let returnText = ""; //No image, has text. 3951 3952 if (imgArray.length === 0 && $el.textContent.trim().length > 1) { 3953 returnText = $el.textContent.trim(); 3954 } //Has image, no text. 3955 else if (imgArray.length && $el.textContent.trim().length === 0) { 3956 let imgalt = imgArray[0].getAttribute("alt"); 3957 3958 if (!imgalt || imgalt === " ") { 3959 returnText = " "; 3960 } else if (imgalt !== undefined) { 3961 returnText = imgalt; 3962 } 3963 } //Has image and text. 3964 //To-do: This is a hack? Any way to do this better? 3965 else if (imgArray.length && $el.textContent.trim().length) { 3966 imgArray.forEach(element => { 3967 element.insertAdjacentHTML("afterend", " <span class='jooa11y-clone-image-text' aria-hidden='true'>" + imgArray[0].getAttribute("alt") + "</span> "); 3968 }); 3969 returnText = $el.textContent.trim(); 3970 } 3971 3972 return returnText; 3973 }; //Helper: Handle ARIA labels for Link Text module. 3974 3975 3976 this.computeAriaLabel = function (el) { 3977 if (el.matches("[aria-label]")) { 3978 return el.getAttribute("aria-label"); 3979 } else if (el.matches("[aria-labelledby]")) { 3980 let target = el.getAttribute("aria-labelledby").split(/\s+/); 3981 3982 if (target.length > 0) { 3983 let returnText = ""; 3984 target.forEach(x => { 3985 if (document.querySelector("#" + x) === null) { 3986 returnText += " "; 3987 } else { 3988 returnText += document.querySelector("#" + x).firstChild.nodeValue + " "; 3989 } 3990 }); 3991 return returnText; 3992 } else { 3993 return ""; 3994 } 3995 } //Children of element. 3996 else if (Array.from(el.children).filter(x => x.matches("[aria-label]")).length > 0) { 3997 return Array.from(el.children)[0].getAttribute("aria-label"); 3998 } else if (Array.from(el.children).filter(x => x.matches("[title]")).length > 0) { 3999 return Array.from(el.children)[0].getAttribute("title"); 4000 } else if (Array.from(el.children).filter(x => x.matches("[aria-labelledby]")).length > 0) { 4001 let target = Array.from(el.children)[0].getAttribute("aria-labelledby").split(/\s+/); 4002 4003 if (target.length > 0) { 4004 let returnText = ""; 4005 target.forEach(x => { 4006 if (document.querySelector("#" + x) === null) { 4007 returnText += " "; 4008 } else { 4009 returnText += document.querySelector("#" + x).firstChild.nodeValue + " "; 4010 } 4011 }); 4012 return returnText; 4013 } else { 4014 return ""; 4015 } 4016 } else { 4017 return "noAria"; 4018 } 4019 }; 4020 } //---------------------------------------------------------------------- 4021 // Setting's panel: Additional ruleset toggles. 4022 //---------------------------------------------------------------------- 4023 4024 4025 settingPanelToggles() { 4026 //Toggle: Contrast 4027 const $jooa11yContrastCheck = document.getElementById("jooa11y-contrast-toggle"); 4028 4029 $jooa11yContrastCheck.onclick = async () => { 4030 if (localStorage.getItem("jooa11y-remember-contrast") === "On") { 4031 localStorage.setItem("jooa11y-remember-contrast", "Off"); 4032 $jooa11yContrastCheck.textContent = Lang._('OFF'); 4033 $jooa11yContrastCheck.setAttribute("aria-pressed", "false"); 4034 this.resetAll(false); 4035 await this.checkAll(); 4036 } else { 4037 localStorage.setItem("jooa11y-remember-contrast", "On"); 4038 $jooa11yContrastCheck.textContent = Lang._('ON'); 4039 $jooa11yContrastCheck.setAttribute("aria-pressed", "true"); 4040 this.resetAll(false); 4041 await this.checkAll(); 4042 } 4043 }; //Toggle: Form labels 4044 4045 4046 const $jooa11yLabelsCheck = document.getElementById("jooa11y-labels-toggle"); 4047 4048 $jooa11yLabelsCheck.onclick = async () => { 4049 if (localStorage.getItem("jooa11y-remember-labels") === "On") { 4050 localStorage.setItem("jooa11y-remember-labels", "Off"); 4051 $jooa11yLabelsCheck.textContent = Lang._('OFF'); 4052 $jooa11yLabelsCheck.setAttribute("aria-pressed", "false"); 4053 this.resetAll(false); 4054 await this.checkAll(); 4055 } else { 4056 localStorage.setItem("jooa11y-remember-labels", "On"); 4057 $jooa11yLabelsCheck.textContent = Lang._('ON'); 4058 $jooa11yLabelsCheck.setAttribute("aria-pressed", "true"); 4059 this.resetAll(false); 4060 await this.checkAll(); 4061 } 4062 }; //Toggle: Links (Advanced) 4063 4064 4065 const $jooa11yChangeRequestCheck = document.getElementById("jooa11y-links-advanced-toggle"); 4066 4067 $jooa11yChangeRequestCheck.onclick = async () => { 4068 if (localStorage.getItem("jooa11y-remember-links-advanced") === "On") { 4069 localStorage.setItem("jooa11y-remember-links-advanced", "Off"); 4070 $jooa11yChangeRequestCheck.textContent = Lang._('OFF'); 4071 $jooa11yChangeRequestCheck.setAttribute("aria-pressed", "false"); 4072 this.resetAll(false); 4073 await this.checkAll(); 4074 } else { 4075 localStorage.setItem("jooa11y-remember-links-advanced", "On"); 4076 $jooa11yChangeRequestCheck.textContent = Lang._('ON'); 4077 $jooa11yChangeRequestCheck.setAttribute("aria-pressed", "true"); 4078 this.resetAll(false); 4079 await this.checkAll(); 4080 } 4081 }; //Toggle: Readability 4082 4083 4084 const $jooa11yReadabilityCheck = document.getElementById("jooa11y-readability-toggle"); 4085 4086 $jooa11yReadabilityCheck.onclick = async () => { 4087 if (localStorage.getItem("jooa11y-remember-readability") === "On") { 4088 localStorage.setItem("jooa11y-remember-readability", "Off"); 4089 $jooa11yReadabilityCheck.textContent = Lang._('OFF'); 4090 $jooa11yReadabilityCheck.setAttribute("aria-pressed", "false"); 4091 document.getElementById("jooa11y-readability-panel").classList.remove("jooa11y-active"); 4092 this.resetAll(false); 4093 await this.checkAll(); 4094 } else { 4095 localStorage.setItem("jooa11y-remember-readability", "On"); 4096 $jooa11yReadabilityCheck.textContent = Lang._('ON'); 4097 $jooa11yReadabilityCheck.setAttribute("aria-pressed", "true"); 4098 document.getElementById("jooa11y-readability-panel").classList.add("jooa11y-active"); 4099 this.resetAll(false); 4100 await this.checkAll(); 4101 } 4102 }; 4103 4104 if (localStorage.getItem("jooa11y-remember-readability") === "On") { 4105 document.getElementById("jooa11y-readability-panel").classList.add("jooa11y-active"); 4106 } //Toggle: Dark mode. (Credits: https://derekkedziora.com/blog/dark-mode-revisited) 4107 4108 4109 let systemInitiatedDark = window.matchMedia("(prefers-color-scheme: dark)"); 4110 const $jooa11yTheme = document.getElementById("jooa11y-theme-toggle"); 4111 const html = document.querySelector("html"); 4112 const theme = localStorage.getItem("jooa11y-remember-theme"); 4113 4114 if (systemInitiatedDark.matches) { 4115 $jooa11yTheme.textContent = Lang._('ON'); 4116 $jooa11yTheme.setAttribute("aria-pressed", "true"); 4117 } else { 4118 $jooa11yTheme.textContent = Lang._('OFF'); 4119 $jooa11yTheme.setAttribute("aria-pressed", "false"); 4120 } 4121 4122 function prefersColorTest(systemInitiatedDark) { 4123 if (systemInitiatedDark.matches) { 4124 html.setAttribute("data-jooa11y-theme", "dark"); 4125 $jooa11yTheme.textContent = Lang._('ON'); 4126 $jooa11yTheme.setAttribute("aria-pressed", "true"); 4127 localStorage.setItem("jooa11y-remember-theme", ""); 4128 } else { 4129 html.setAttribute("data-jooa11y-theme", "light"); 4130 $jooa11yTheme.textContent = Lang._('OFF'); 4131 $jooa11yTheme.setAttribute("aria-pressed", "false"); 4132 localStorage.setItem("jooa11y-remember-theme", ""); 4133 } 4134 } 4135 4136 systemInitiatedDark.addEventListener('change', prefersColorTest); 4137 4138 $jooa11yTheme.onclick = async () => { 4139 const theme = localStorage.getItem("jooa11y-remember-theme"); 4140 4141 if (theme === "dark") { 4142 html.setAttribute("data-jooa11y-theme", "light"); 4143 localStorage.setItem("jooa11y-remember-theme", "light"); 4144 $jooa11yTheme.textContent = Lang._('OFF'); 4145 $jooa11yTheme.setAttribute("aria-pressed", "false"); 4146 } else if (theme === "light") { 4147 html.setAttribute("data-jooa11y-theme", "dark"); 4148 localStorage.setItem("jooa11y-remember-theme", "dark"); 4149 $jooa11yTheme.textContent = Lang._('ON'); 4150 $jooa11yTheme.setAttribute("aria-pressed", "true"); 4151 } else if (systemInitiatedDark.matches) { 4152 html.setAttribute("data-jooa11y-theme", "light"); 4153 localStorage.setItem("jooa11y-remember-theme", "light"); 4154 $jooa11yTheme.textContent = Lang._('OFF'); 4155 $jooa11yTheme.setAttribute("aria-pressed", "false"); 4156 } else { 4157 html.setAttribute("data-jooa11y-theme", "dark"); 4158 localStorage.setItem("jooa11y-remember-theme", "dark"); 4159 $jooa11yTheme.textContent = Lang._('OFF'); 4160 $jooa11yTheme.setAttribute("aria-pressed", "true"); 4161 } 4162 }; 4163 4164 if (theme === "dark") { 4165 html.setAttribute("data-jooa11y-theme", "dark"); 4166 localStorage.setItem("jooa11y-remember-theme", "dark"); 4167 $jooa11yTheme.textContent = Lang._('ON'); 4168 $jooa11yTheme.setAttribute("aria-pressed", "true"); 4169 } else if (theme === "light") { 4170 html.setAttribute("data-jooa11y-theme", "light"); 4171 localStorage.setItem("jooa11y-remember-theme", "light"); 4172 $jooa11yTheme.textContent = Lang._('OFF'); 4173 $jooa11yTheme.setAttribute("aria-pressed", "false"); 4174 } 4175 } //---------------------------------------------------------------------- 4176 // Tooltip for Jump-to-Issue button. 4177 //---------------------------------------------------------------------- 4178 4179 4180 initializeJumpToIssueTooltip() { 4181 tippy('#jooa11y-cycle-toggle', { 4182 content: `<div style="text-align:center">$Lang._('SHORTCUT_TOOLTIP')} »<br><span class="jooa11y-shortcut-icon"></span></div>`, 4183 allowHTML: true, 4184 delay: [900, 0], 4185 trigger: "mouseenter focusin", 4186 arrow: true, 4187 placement: 'top', 4188 theme: "jooa11y-theme", 4189 aria: { 4190 content: null, 4191 expanded: false 4192 }, 4193 appendTo: document.body 4194 }); 4195 } // ---------------------------------------------------------------------- 4196 // Do Initial check 4197 // ---------------------------------------------------------------------- 4198 4199 4200 doInitialCheck() { 4201 if (localStorage.getItem("jooa11y-remember-panel") === "Closed" || !localStorage.getItem("jooa11y-remember-panel")) { 4202 this.panelActive = true; // Prevent panel popping up after initial check 4203 4204 this.checkAll(); 4205 } 4206 } // ---------------------------------------------------------------------- 4207 // Check all 4208 // ---------------------------------------------------------------------- 4209 4210 4211 // ============================================================ 4212 // Reset all 4213 // ============================================================ 4214 resetAll(restartPanel = true) { 4215 this.panelActive = false; 4216 this.clearEverything(); //Remove eventListeners on the Show Outline and Show Panel toggles. 4217 4218 const $outlineToggle = document.getElementById("jooa11y-outline-toggle"); 4219 const resetOutline = $outlineToggle.cloneNode(true); 4220 $outlineToggle.parentNode.replaceChild(resetOutline, $outlineToggle); 4221 const $settingsToggle = document.getElementById("jooa11y-settings-toggle"); 4222 const resetSettings = $settingsToggle.cloneNode(true); 4223 $settingsToggle.parentNode.replaceChild(resetSettings, $settingsToggle); //Errors 4224 4225 document.querySelectorAll('.jooa11y-error-border').forEach(el => el.classList.remove('jooa11y-error-border')); 4226 document.querySelectorAll('.jooa11y-error-text').forEach(el => el.classList.remove('jooa11y-error-text')); //Warnings 4227 4228 document.querySelectorAll('.jooa11y-warning-border').forEach(el => el.classList.remove('jooa11y-warning-border')); 4229 document.querySelectorAll('.jooa11y-warning-text').forEach(el => el.classList.remove('jooa11y-warning-text')); 4230 document.querySelectorAll('p').forEach(el => el.classList.remove('jooa11y-fake-list')); 4231 let allcaps = document.querySelectorAll('.jooa11y-warning-uppercase'); 4232 allcaps.forEach(el => el.outerHTML = el.innerHTML); //Good 4233 4234 document.querySelectorAll('.jooa11y-good-border').forEach(el => el.classList.remove('jooa11y-good-border')); 4235 document.querySelectorAll('.jooa11y-good-text').forEach(el => el.classList.remove('jooa11y-good-text')); //Remove 4236 4237 document.querySelectorAll(` 4238 .jooa11y-instance, 4239 .jooa11y-instance-inline, 4240 .jooa11y-heading-label, 4241 #jooa11y-outline-list li, 4242 .jooa11y-readability-period, 4243 #jooa11y-readability-info span, 4244 #jooa11y-readability-details li, 4245 .jooa11y-clone-image-text 4246 `).forEach(el => el.parentNode.removeChild(el)); //Etc 4247 4248 document.querySelectorAll('.jooa11y-overflow').forEach(el => el.classList.remove('jooa11y-overflow')); 4249 document.querySelectorAll('.jooa11y-fake-heading').forEach(el => el.classList.remove('jooa11y-fake-heading')); 4250 document.querySelectorAll('.jooa11y-pulse-border').forEach(el => el.classList.remove('jooa11y-pulse-border')); 4251 document.querySelector('#jooa11y-panel-alert').classList.remove("jooa11y-active"); 4252 var empty = document.querySelector('#jooa11y-panel-alert-text'); 4253 4254 while (empty.firstChild) empty.removeChild(empty.firstChild); 4255 4256 var clearStatus = document.querySelector('#jooa11y-status'); 4257 4258 while (clearStatus.firstChild) clearStatus.removeChild(clearStatus.firstChild); 4259 4260 if (restartPanel) { 4261 document.querySelector('#jooa11y-panel').classList.remove("jooa11y-active"); 4262 } 4263 } 4264 4265 clearEverything() {} 4266 4267 // ============================================================ 4268 // Initialize tooltips for error/warning/pass buttons: (Tippy.js) 4269 // Although you can also swap this with Bootstrap's tooltip library for example. 4270 // ============================================================ 4271 initializeTooltips() { 4272 tippy(".jooa11y-btn", { 4273 interactive: true, 4274 trigger: "mouseenter click focusin", 4275 //Focusin trigger to ensure "Jump to issue" button displays tooltip. 4276 arrow: true, 4277 delay: [200, 0], 4278 //Slight delay to ensure mouse doesn't quickly trigger and hide tooltip. 4279 theme: "jooa11y-theme", 4280 placement: 'bottom', 4281 allowHTML: true, 4282 aria: { 4283 content: 'describedby' 4284 }, 4285 appendTo: document.body 4286 }); 4287 } // ============================================================ 4288 // Detect parent containers that have hidden overflow. 4289 // ============================================================ 4290 4291 4292 detectOverflow() { 4293 const findParentWithOverflow = ($el, property, value) => { 4294 while ($el !== null) { 4295 const style = window.getComputedStyle($el); 4296 const propValue = style.getPropertyValue(property); 4297 4298 if (propValue === value) { 4299 return $el; 4300 } 4301 4302 $el = $el.parentElement; 4303 } 4304 4305 return null; 4306 }; 4307 4308 const $findButtons = document.querySelectorAll('.jooa11y-btn'); 4309 $findButtons.forEach(function ($el) { 4310 const overflowing = findParentWithOverflow($el, 'overflow', 'hidden'); 4311 4312 if (overflowing !== null) { 4313 overflowing.classList.add('jooa11y-overflow'); 4314 } 4315 }); 4316 } // ============================================================ 4317 // Nudge buttons if they overlap. 4318 // ============================================================ 4319 4320 4321 // ============================================================ 4322 // Update iOS style notification badge on icon. 4323 // ============================================================ 4324 updateBadge() { 4325 let totalCount = this.errorCount + this.warningCount; 4326 const notifBadge = document.getElementById("jooa11y-notification-badge"); 4327 4328 if (totalCount === 0) { 4329 notifBadge.style.display = "none"; 4330 } else { 4331 notifBadge.style.display = "flex"; 4332 document.getElementById('jooa11y-notification-count').innerHTML = Lang.sprintf('PANEL_STATUS_ICON', totalCount); 4333 } 4334 } // ---------------------------------------------------------------------- 4335 // Main panel: Display and update panel. 4336 // ---------------------------------------------------------------------- 4337 4338 4339 updatePanel() { 4340 this.panelActive = true; 4341 this.errorCount + this.warningCount; 4342 this.buildPanel(); 4343 this.skipToIssue(); 4344 const $jooa11ySkipBtn = document.getElementById("jooa11y-cycle-toggle"); 4345 $jooa11ySkipBtn.disabled = false; 4346 $jooa11ySkipBtn.setAttribute("style", "cursor: pointer !important;"); 4347 const $jooa11yPanel = document.getElementById("jooa11y-panel"); 4348 $jooa11yPanel.classList.add("jooa11y-active"); 4349 const $panelContent = document.getElementById("jooa11y-panel-content"); 4350 const $jooa11yStatus = document.getElementById("jooa11y-status"); 4351 const $findButtons = document.querySelectorAll('.jooa11y-btn'); 4352 4353 if (this.errorCount > 0 && this.warningCount > 0) { 4354 $panelContent.setAttribute("class", "jooa11y-errors"); 4355 $jooa11yStatus.textContent = Lang.sprintf('PANEL_STATUS_BOTH', this.errorCount, this.warningCount); 4356 } else if (this.errorCount > 0) { 4357 $panelContent.setAttribute("class", "jooa11y-errors"); 4358 $jooa11yStatus.textContent = Lang.sprintf('PANEL_STATUS_ERRORS', this.errorCount); 4359 } else if (this.warningCount > 0) { 4360 $panelContent.setAttribute("class", "jooa11y-warnings"); 4361 $jooa11yStatus.textContent = Lang.sprintf('PANEL_STATUS_WARNINGS', this.warningCount); 4362 } else { 4363 $panelContent.setAttribute("class", "jooa11y-good"); 4364 $jooa11yStatus.textContent = Lang._('PANEL_STATUS_NONE'); 4365 4366 if ($findButtons.length === 0) { 4367 $jooa11ySkipBtn.disabled = true; 4368 $jooa11ySkipBtn.setAttribute("style", "cursor: default !important;"); 4369 } 4370 } 4371 } 4372 4373 // ============================================================ 4374 // Finds all elements and caches them 4375 // ============================================================ 4376 findElements() { 4377 const allHeadings = Array.from(this.$root.querySelectorAll("h1, h2, h3, h4, h5, h6, [role='heading'][aria-level]")); 4378 const allPs = Array.from(this.$root.querySelectorAll("p")); 4379 this.$containerExclusions = Array.from(document.querySelectorAll(this.containerIgnore)); 4380 this.$h = allHeadings.filter(heading => !this.$containerExclusions.includes(heading)); 4381 this.$p = allPs.filter(p => !this.$containerExclusions.includes(p)); 4382 } 4383 4384 // ============================================================ 4385 // Rulesets: Check Headings 4386 // ============================================================ 4387 checkHeaders() { 4388 let prevLevel; 4389 this.$h.forEach((el, i) => { 4390 let text = this.computeTextNodeWithImage(el); 4391 let htext = escapeHTML(text); 4392 let level; 4393 4394 if (el.getAttribute("aria-level")) { 4395 level = +el.getAttribute("aria-level"); 4396 } else { 4397 level = +el.tagName.slice(1); 4398 } 4399 4400 let headingLength = el.textContent.trim().length; 4401 let error = null; 4402 let warning = null; 4403 4404 if (level - prevLevel > 1 && i !== 0) { 4405 error = Lang.sprintf('HEADING_NON_CONSECUTIVE_LEVEL', prevLevel, level); 4406 } else if (el.textContent.trim().length === 0) { 4407 if (el.querySelectorAll("img").length) { 4408 const imgalt = el.querySelector("img").getAttribute("alt"); 4409 4410 if (imgalt === undefined || imgalt === " " || imgalt === "") { 4411 error = Lang.sprintf('HEADING_EMPTY_WITH_IMAGE', level); 4412 el.classList.add("jooa11y-error-text"); 4413 } 4414 } else { 4415 error = Lang.sprintf('HEADING_EMPTY', level); 4416 el.classList.add("jooa11y-error-text"); 4417 } 4418 } else if (i === 0 && level !== 1 && level !== 2) { 4419 error = Lang._('HEADING_FIRST'); 4420 } else if (el.textContent.trim().length > 170) { 4421 warning = `$Lang._('HEADING_LONG')} . $Lang.sprintf('HEADING_LONG_INFO', headingLength)}`; 4422 } 4423 4424 prevLevel = level; 4425 let li = `<li class='jooa11y-outline-$level}'> 4426 <span class='jooa11y-badge'>$level}</span> 4427 <span class='jooa11y-outline-list-item'>$htext}</span> 4428 </li>`; 4429 let liError = `<li class='jooa11y-outline-$level}'> 4430 <span class='jooa11y-badge jooa11y-error-badge'> 4431 <span aria-hidden='true'>✗</span> 4432 <span class='jooa11y-visually-hidden'>$Lang._('ERROR')}</span> $level}</span> 4433 <span class='jooa11y-outline-list-item jooa11y-red-text jooa11y-bold'>$htext}</span> 4434 </li>`; 4435 let liWarning = `<li class='jooa11y-outline-$level}'> 4436 <span class='jooa11y-badge jooa11y-warning-badge'> 4437 <span aria-hidden='true'>?</span> 4438 <span class='jooa11y-visually-hidden'>$Lang._('WARNING')}</span> $level}</span> 4439 <span class='jooa11y-outline-list-item jooa11y-yellow-text jooa11y-bold'>$htext}</span> 4440 </li>`; 4441 let ignoreArray = []; 4442 4443 if (this.options.outlineIgnore) { 4444 ignoreArray = Array.from(document.querySelectorAll(this.options.outlineIgnore)); 4445 } 4446 4447 if (!ignoreArray.includes(el)) { 4448 //Append heading labels. 4449 el.insertAdjacentHTML("beforeend", `<span class='jooa11y-heading-label'>H$level}</span>`); //Heading errors 4450 4451 if (error != null && el.closest("a")) { 4452 this.errorCount++; 4453 el.classList.add("jooa11y-error-border"); 4454 el.closest("a").insertAdjacentHTML("afterend", this.annotate(Lang._('ERROR'), error, true)); 4455 document.querySelector("#jooa11y-outline-list").insertAdjacentHTML("beforeend", liError); 4456 } else if (error != null) { 4457 this.errorCount++; 4458 el.classList.add("jooa11y-error-border"); 4459 el.insertAdjacentHTML("beforebegin", this.annotate(Lang._('ERROR'), error)); 4460 document.querySelector("#jooa11y-outline-list").insertAdjacentHTML("beforeend", liError); 4461 } //Heading warnings 4462 else if (warning != null && el.closest("a")) { 4463 this.warningCount++; 4464 el.closest("a").insertAdjacentHTML("afterend", this.annotate(Lang._('WARNING'), warning)); 4465 document.querySelector("#jooa11y-outline-list").insertAdjacentHTML("beforeend", liWarning); 4466 } else if (warning != null) { 4467 el.insertAdjacentHTML("beforebegin", this.annotate(Lang._('WARNING'), warning)); 4468 document.querySelector("#jooa11y-outline-list").insertAdjacentHTML("beforeend", liWarning); 4469 } //Not an error or warning 4470 else if (error == null || warning == null) { 4471 document.querySelector("#jooa11y-outline-list").insertAdjacentHTML("beforeend", li); 4472 } 4473 } 4474 }); //Check to see there is at least one H1 on the page. 4475 4476 const $h1 = Array.from(this.$root.querySelectorAll('h1, [role="heading"][aria-level="1"]')).filter($h => !this.$containerExclusions.includes($h)); 4477 4478 if ($h1.length === 0) { 4479 this.errorCount++; 4480 document.querySelector('#jooa11y-outline-header').insertAdjacentHTML('afterend', `<div class='jooa11y-instance jooa11y-missing-h1'> 4481 <span class='jooa11y-badge jooa11y-error-badge'><span aria-hidden='true'>✗</span><span class='jooa11y-visually-hidden'>$Lang._('ERROR')}</span></span> 4482 <span class='jooa11y-red-text jooa11y-bold'>$Lang._('PANEL_HEADING_MISSING_ONE')}</span> 4483 </div>`); 4484 document.querySelector("#jooa11y-container").insertAdjacentHTML('afterend', this.annotateBanner(Lang._('ERROR'), Lang._('HEADING_MISSING_ONE'))); 4485 } 4486 } 4487 4488 // ============================================================ 4489 // Rulesets: Link text 4490 // ============================================================ 4491 checkLinkText() { 4492 const containsLinkTextStopWords = textContent => { 4493 let urlText = ["http", ".asp", ".htm", ".php", ".edu/", ".com/", ".net/", ".org/", ".us/", ".ca/", ".de/", ".icu/", ".uk/", ".ru/", ".info/", ".top/", ".xyz/", ".tk/", ".cn/", ".ga/", ".cf/", ".nl/", ".io/"]; 4494 let hit = [null, null, null]; // Flag partial stop words. 4495 4496 this.options.partialAltStopWords.forEach(word => { 4497 if (textContent.length === word.length && textContent.toLowerCase().indexOf(word) >= 0) { 4498 hit[0] = word; 4499 return false; 4500 } 4501 }); // Other warnings we want to add. 4502 4503 this.options.warningAltWords.forEach(word => { 4504 if (textContent.toLowerCase().indexOf(word) >= 0) { 4505 hit[1] = word; 4506 return false; 4507 } 4508 }); // Flag link text containing URLs. 4509 4510 urlText.forEach(word => { 4511 if (textContent.toLowerCase().indexOf(word) >= 0) { 4512 hit[2] = word; 4513 return false; 4514 } 4515 }); 4516 return hit; 4517 }; 4518 /* Mini function if you need to exclude any text contained with a span. We created this function to ignore automatically appended sr-only text for external links and document filetypes. 4519 $.fn.ignore = function(sel){ 4520 return this.clone().find(sel||">*").remove().end(); 4521 }; 4522 $el.ignore("span.sr-only").text().trim(); 4523 Example: <a href="#">learn more <span class="sr-only">(external)</span></a> 4524 This function will ignore the text "(external)", and correctly flag this link as an error for non descript link text. */ 4525 4526 4527 const fnIgnore = (element, selector) => { 4528 const $clone = element.cloneNode(true); 4529 const $excluded = Array.from(selector ? $clone.querySelectorAll(selector) : $clone.children); 4530 $excluded.forEach($c => { 4531 $c.parentElement.removeChild($c); 4532 }); 4533 return $clone; 4534 }; 4535 4536 const $linkIgnore = Array.from(this.$root.querySelectorAll(this.linkIgnore)); 4537 const $links = Array.from(this.$root.querySelectorAll('a[href]')).filter($a => !$linkIgnore.includes($a)); 4538 $links.forEach(el => { 4539 let linkText = this.computeAriaLabel(el); 4540 let hasAriaLabelledBy = el.getAttribute('aria-labelledby'); 4541 let hasAriaLabel = el.getAttribute('aria-label'); 4542 let hasTitle = el.getAttribute('title'); 4543 let childAriaLabelledBy = null; 4544 let childAriaLabel = null; 4545 let childTitle = null; 4546 4547 if (el.children.length) { 4548 let $firstChild = el.children[0]; 4549 childAriaLabelledBy = $firstChild.getAttribute('aria-labelledby'); 4550 childAriaLabel = $firstChild.getAttribute('aria-label'); 4551 childTitle = $firstChild.getAttribute('title'); 4552 } 4553 4554 let error = containsLinkTextStopWords(fnIgnore(el, this.options.linkIgnoreSpan).textContent.trim()); 4555 4556 if (linkText === 'noAria') { 4557 linkText = el.textContent; 4558 } //Flag empty hyperlinks 4559 4560 4561 if (el.getAttribute('href') && !el.textContent.trim()) { 4562 if (el.querySelectorAll('img').length) ;else if (hasAriaLabelledBy || hasAriaLabel) { 4563 el.classList.add("jooa11y-good-border"); 4564 el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), Lang.sprintf('LINK_LABEL', linkText), true)); 4565 } else if (hasTitle) { 4566 let linkText = hasTitle; 4567 el.classList.add("jooa11y-good-border"); 4568 el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), Lang.sprintf('LINK_LABEL', linkText), true)); 4569 } else if (el.children.length) { 4570 if (childAriaLabelledBy || childAriaLabel || childTitle) { 4571 el.classList.add("jooa11y-good-border"); 4572 el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), Lang.sprintf('LINK_LABEL', linkText), true)); 4573 } else { 4574 this.errorCount++; 4575 el.classList.add("jooa11y-error-border"); 4576 el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), Lang.sprintf('LINK_EMPTY_LINK_NO_LABEL'), true)); 4577 } 4578 } else { 4579 this.errorCount++; 4580 el.classList.add("jooa11y-error-border"); 4581 el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), Lang._('LINK_EMPTY'), true)); 4582 } 4583 } else if (error[0] !== null) { 4584 if (hasAriaLabelledBy) { 4585 el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), Lang.sprintf('LINK_LABEL', linkText), true)); 4586 } else if (hasAriaLabel) { 4587 el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), Lang.sprintf('LINK_LABEL', hasAriaLabel), true)); 4588 } else if (el.getAttribute('aria-hidden') === 'true' && el.getAttribute('tabindex') === '-1') ;else { 4589 this.errorCount++; 4590 el.classList.add("jooa11y-error-text"); 4591 el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), `$Lang.sprintf('LINK_STOPWORD', error[0])} <hr aria-hidden="true"> $Lang._('LINK_STOPWORD_TIP')}`, true)); 4592 } 4593 } else if (error[1] !== null) { 4594 this.warningCount++; 4595 el.classList.add("jooa11y-warning-text"); 4596 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang.sprintf('LINK_BEST_PRACTICES', error[1])} <hr aria-hidden="true"> $Lang._('LINK_BEST_PRACTICES_DETAILS')}`, true)); 4597 } else if (error[2] != null) { 4598 if (linkText.length > 40) { 4599 this.warningCount++; 4600 el.classList.add("jooa11y-warning-text"); 4601 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('LINK_URL')} <hr aria-hidden="true"> $Lang._('LINK_URL_TIP')}`, true)); 4602 } 4603 } 4604 }); 4605 } 4606 4607 // ============================================================ 4608 // Rulesets: Links (Advanced) 4609 // ============================================================ 4610 checkLinksAdvanced() { 4611 const $linkIgnore = Array.from(this.$root.querySelectorAll(this.linkIgnore + ', #jooa11y-container a, .jooa11y-exclude')); 4612 const $linksTargetBlank = Array.from(this.$root.querySelectorAll('a[href]')).filter($a => !$linkIgnore.includes($a)); 4613 let seen = {}; 4614 $linksTargetBlank.forEach(el => { 4615 let linkText = this.computeAriaLabel(el); 4616 4617 if (linkText === 'noAria') { 4618 linkText = el.textContent; 4619 } 4620 4621 const fileTypeMatch = el.matches(` 4622 a[href$='.pdf'], 4623 a[href$='.doc'], 4624 a[href$='.zip'], 4625 a[href$='.mp3'], 4626 a[href$='.txt'], 4627 a[href$='.exe'], 4628 a[href$='.dmg'], 4629 a[href$='.rtf'], 4630 a[href$='.pptx'], 4631 a[href$='.ppt'], 4632 a[href$='.xls'], 4633 a[href$='.xlsx'], 4634 a[href$='.csv'], 4635 a[href$='.mp4'], 4636 a[href$='.mov'], 4637 a[href$='.avi'] 4638 `); //Links with identical accessible names have equivalent purpose. 4639 //If link has an image, process alt attribute, 4640 //To-do: Kinda hacky. Doesn't return accessible name of link in correct order. 4641 4642 const $img = el.querySelector('img'); 4643 let alt = $img ? $img.getAttribute('alt') || '' : ''; //Return link text and image's alt text. 4644 4645 let linkTextTrimmed = linkText.trim().toLowerCase() + " " + alt; 4646 let href = el.getAttribute("href"); 4647 4648 if (linkText.length !== 0) { 4649 if (seen[linkTextTrimmed] && linkTextTrimmed.length !== 0) { 4650 if (seen[href]) ;else { 4651 this.warningCount++; 4652 el.classList.add("jooa11y-warning-text"); 4653 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('LINK_IDENTICAL_NAME')} <hr aria-hidden="true"> $Lang.sprintf('LINK_IDENTICAL_NAME_TIP', linkText)}`, true)); 4654 } 4655 } else { 4656 seen[linkTextTrimmed] = true; 4657 seen[href] = true; 4658 } 4659 } //New tab or new window. 4660 4661 4662 const containsNewWindowPhrases = this.options.newWindowPhrases.some(function (pass) { 4663 return linkText.toLowerCase().indexOf(pass) >= 0; 4664 }); //Link that points to a file type indicates that it does. 4665 4666 const containsFileTypePhrases = this.options.fileTypePhrases.some(function (pass) { 4667 return linkText.toLowerCase().indexOf(pass) >= 0; 4668 }); 4669 4670 if (el.getAttribute("target") === "_blank" && !fileTypeMatch && !containsNewWindowPhrases) { 4671 this.warningCount++; 4672 el.classList.add("jooa11y-warning-text"); 4673 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('NEW_TAB_WARNING')} <hr aria-hidden="true"> $Lang._('NEW_TAB_WARNING_TIP')}`, true)); 4674 } 4675 4676 if (fileTypeMatch && !containsFileTypePhrases) { 4677 this.warningCount++; 4678 el.classList.add("jooa11y-warning-text"); 4679 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('FILE_TYPE_WARNING')} <hr aria-hidden="true"> $Lang._('FILE_TYPE_WARNING_TIP')}`, true)); 4680 } 4681 }); 4682 } // ============================================================ 4683 // Ruleset: Underlined text 4684 // ============================================================ 4685 // check text for <u> tags 4686 4687 4688 checkUnderline() { 4689 const underline = Array.from(this.$root.querySelectorAll('u')); 4690 underline.forEach($el => { 4691 this.warningCount++; 4692 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('TEXT_UNDERLINE_WARNING')} <hr aria-hidden="true"> $Lang._('TEXT_UNDERLINE_WARNING_TIP')}`, true)); 4693 }); // check for text-decoration-line: underline 4694 4695 const computed = Array.from(this.$root.querySelectorAll('h1, h2, h3, h4, h5, h6, p, div, span, li, blockquote')); 4696 computed.forEach($el => { 4697 let style = getComputedStyle($el), 4698 decoration = style.textDecorationLine; 4699 4700 if (decoration === 'underline') { 4701 this.warningCount++; 4702 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('TEXT_UNDERLINE_WARNING')} <hr aria-hidden="true"> $Lang._('TEXT_UNDERLINE_WARNING_TIP')}`, true)); 4703 } 4704 }); 4705 } // ============================================================ 4706 // Ruleset: Alternative text 4707 // ============================================================ 4708 4709 4710 checkAltText() { 4711 const containsAltTextStopWords = alt => { 4712 const altUrl = [".png", ".jpg", ".jpeg", ".gif", ".tiff", ".svg"]; 4713 let hit = [null, null, null]; 4714 altUrl.forEach(word => { 4715 if (alt.toLowerCase().indexOf(word) >= 0) { 4716 hit[0] = word; 4717 } 4718 }); 4719 this.options.suspiciousAltWords.forEach(word => { 4720 if (alt.toLowerCase().indexOf(word) >= 0) { 4721 hit[1] = word; 4722 } 4723 }); 4724 this.options.placeholderAltStopWords.forEach(word => { 4725 if (alt.length === word.length && alt.toLowerCase().indexOf(word) >= 0) { 4726 hit[2] = word; 4727 } 4728 }); 4729 return hit; 4730 }; // Stores the corresponding issue text to alternative text 4731 4732 4733 const images = Array.from(this.$root.querySelectorAll("img")); 4734 const excludeimages = Array.from(this.$root.querySelectorAll(this.imageIgnore)); 4735 const $img = images.filter($el => !excludeimages.includes($el)); 4736 $img.forEach($el => { 4737 let alt = $el.getAttribute("alt"); 4738 4739 if (alt === null) { 4740 if ($el.closest('a[href]')) { 4741 if ($el.closest('a[href]').textContent.trim().length > 1) { 4742 $el.classList.add("jooa11y-error-border"); 4743 $el.closest('a[href]').insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('MISSING_ALT_LINK_BUT_HAS_TEXT_MESSAGE'), false, true)); 4744 } else if ($el.closest('a[href]').textContent.trim().length === 0) { 4745 $el.classList.add("jooa11y-error-border"); 4746 $el.closest('a[href]').insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('MISSING_ALT_LINK_MESSAGE'), false, true)); 4747 } 4748 } // General failure message if image is missing alt. 4749 else { 4750 $el.classList.add("jooa11y-error-border"); 4751 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('MISSING_ALT_MESSAGE'), false, true)); 4752 } 4753 } // If alt attribute is present, further tests are done. 4754 else { 4755 let altText = escapeHTML(alt); //Prevent tooltip from breaking. 4756 4757 let error = containsAltTextStopWords(altText); 4758 let altLength = alt.length; // Image fails if a stop word was found. 4759 4760 if (error[0] != null && $el.closest("a[href]")) { 4761 this.errorCount++; 4762 $el.classList.add("jooa11y-error-border"); 4763 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), `$Lang.sprintf('LINK_IMAGE_BAD_ALT_MESSAGE', altText, error[0])} <hr aria-hidden="true"> $Lang._('LINK_IMAGE_BAD_ALT_MESSAGE_INFO')}`, false)); 4764 } else if (error[2] != null && $el.closest("a[href]")) { 4765 this.errorCount++; 4766 $el.classList.add("jooa11y-error-border"); 4767 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang.sprintf('LINK_IMAGE_PLACEHOLDER_ALT_MESSAGE', altText), false, true)); 4768 } else if (error[1] != null && $el.closest("a[href]")) { 4769 this.warningCount++; 4770 $el.classList.add("jooa11y-warning-border"); 4771 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang.sprintf('LINK_IMAGE_SUS_ALT_MESSAGE', altText, error[1])} <hr aria-hidden="true"> $Lang.sprintf('LINK_IMAGE_SUS_ALT_MESSAGE_INFO', altText)}`, false)); 4772 } else if (error[0] != null) { 4773 this.errorCount++; 4774 $el.classList.add("jooa11y-error-border"); 4775 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), `$Lang._('LINK_ALT_HAS_BAD_WORD_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LINK_ALT_HAS_BAD_WORD_MESSAGE_INFO', error[0], altText)}`, false)); 4776 } else if (error[2] != null) { 4777 this.errorCount++; 4778 $el.classList.add("jooa11y-error-border"); 4779 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang.sprintf('LINK_ALT_PLACEHOLDER_MESSAGE', altText), false)); 4780 } else if (error[1] != null) { 4781 this.warningCount++; 4782 $el.classList.add("jooa11y-warning-border"); 4783 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang.sprintf('LINK_ALT_HAS_SUS_WORD_MESSAGE', altText, error[1])} <hr aria-hidden="true"> $Lang.sprintf('LINK_ALT_HAS_SUS_WORD_MESSAGE_INFO', altText)}`, false)); 4784 } else if ((alt === "" || alt === " ") && $el.closest("a[href]")) { 4785 if ($el.closest("a[href]").getAttribute("tabindex") === "-1" && $el.closest("a[href]").getAttribute("aria-hidden") === "true") ;else if ($el.closest("a[href]").getAttribute("aria-hidden") === "true") { 4786 this.errorCount++; 4787 $el.classList.add("jooa11y-error-border"); 4788 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('LINK_HYPERLINKED_IMAGE_ARIA_HIDDEN'), false, true)); 4789 } else if ($el.closest("a[href]").textContent.trim().length === 0) { 4790 this.errorCount++; 4791 $el.classList.add("jooa11y-error-border"); 4792 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('LINK_IMAGE_LINK_NULL_ALT_NO_TEXT_MESSAGE'), false, true)); 4793 } else { 4794 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), Lang._('LINK_LINK_HAS_ALT_MESSAGE'), false, true)); 4795 } 4796 } //Link and contains alt text. 4797 else if (alt.length > 250 && $el.closest("a[href]")) { 4798 this.warningCount++; 4799 $el.classList.add("jooa11y-warning-border"); 4800 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('HYPERLINK_ALT_LENGTH_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('HYPERLINK_ALT_LENGTH_MESSAGE_INFO', altText, altLength)}`, false)); 4801 } //Link and contains an alt text. 4802 else if (alt !== "" && $el.closest("a[href]") && $el.closest("a[href]").textContent.trim().length === 0) { 4803 this.warningCount++; 4804 $el.classList.add("jooa11y-warning-border"); 4805 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('LINK_IMAGE_LINK_ALT_TEXT_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LINK_IMAGE_LINK_ALT_TEXT_MESSAGE_INFO', altText)}`, false)); 4806 } //Contains alt text & surrounding link text. 4807 else if (alt !== "" && $el.closest("a[href]") && $el.closest("a[href]").textContent.trim().length > 1) { 4808 this.warningCount++; 4809 $el.classList.add("jooa11y-warning-border"); 4810 $el.closest("a[href]").insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('LINK_ANCHOR_LINK_AND_ALT_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LINK_ANCHOR_LINK_AND_ALT_MESSAGE_INFO', altText)}`, false)); 4811 } //Decorative alt and not a link. 4812 else if (alt === "" || alt === " ") { 4813 if ($el.closest("figure")) { 4814 const figcaption = $el.closest("figure").querySelector("figcaption"); 4815 4816 if (figcaption !== null && figcaption.textContent.trim().length >= 1) { 4817 this.warningCount++; 4818 $el.classList.add("jooa11y-warning-border"); 4819 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('IMAGE_FIGURE_DECORATIVE')} <hr aria-hidden="true"> $Lang._('IMAGE_FIGURE_DECORATIVE_INFO')}`, false, true)); 4820 } 4821 } else { 4822 this.warningCount++; 4823 $el.classList.add("jooa11y-warning-border"); 4824 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), Lang._('LINK_DECORATIVE_MESSAGE'), false, true)); 4825 } 4826 } else if (alt.length > 250) { 4827 this.warningCount++; 4828 $el.classList.add("jooa11y-warning-border"); 4829 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('LINK_ALT_TOO_LONG_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LINK_ALT_TOO_LONG_MESSAGE_INFO', altText, altLength)}`, false)); 4830 } else if (alt !== "") { 4831 //Figure element has same alt and caption text. 4832 if ($el.closest("figure")) { 4833 const figcaption = $el.closest("figure").querySelector("figcaption"); 4834 4835 if (figcaption !== null && figcaption.textContent.trim().toLowerCase === altText.trim().toLowerCase) { 4836 this.warningCount++; 4837 $el.classList.add("jooa11y-warning-border"); 4838 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang.sprintf('IMAGE_FIGURE_DUPLICATE_ALT', altText)} <hr aria-hidden="true"> $Lang._('IMAGE_FIGURE_DECORATIVE_INFO')}`, false, true)); 4839 } 4840 } //If image has alt text - pass! 4841 else { 4842 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('GOOD'), `$Lang.sprintf('LINK_PASS_ALT', altText)}`, false, true)); 4843 } 4844 } 4845 } 4846 }); 4847 } 4848 4849 // ============================================================ 4850 // Rulesets: Labels 4851 // ============================================================ 4852 checkLabels() { 4853 const $inputs = Array.from(this.$root.querySelectorAll('input, select, textarea')).filter($i => { 4854 return !this.$containerExclusions.includes($i) && !isElementHidden($i); 4855 }); 4856 $inputs.forEach(el => { 4857 let ariaLabel = this.computeAriaLabel(el); 4858 const type = el.getAttribute('type'); //If button type is submit or button: pass 4859 4860 if (type === "submit" || type === "button" || type === "hidden") ; //Inputs where type="image". 4861 else if (type === "image") { 4862 let imgalt = el.getAttribute("alt"); 4863 4864 if (!imgalt || imgalt === ' ') { 4865 if (el.getAttribute("aria-label")) ;else { 4866 this.errorCount++; 4867 el.classList.add("jooa11y-error-border"); 4868 el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), Lang._('LABELS_MISSING_IMAGE_INPUT_MESSAGE'), true)); 4869 } 4870 } 4871 } //Recommendation to remove reset buttons. 4872 else if (type === "reset") { 4873 this.warningCount++; 4874 el.classList.add("jooa11y-warning-border"); 4875 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('LABELS_INPUT_RESET_MESSAGE')} <hr aria-hidden="true"> $Lang._('LABELS_INPUT_RESET_MESSAGE_TIP')}`, true)); 4876 } //Uses ARIA. Warn them to ensure there's a visible label. 4877 else if (el.getAttribute("aria-label") || el.getAttribute("aria-labelledby") || el.getAttribute("title")) { 4878 if (el.getAttribute("title")) { 4879 let ariaLabel = el.getAttribute("title"); 4880 this.warningCount++; 4881 el.classList.add("jooa11y-warning-border"); 4882 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('LABELS_ARIA_LABEL_INPUT_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LABELS_ARIA_LABEL_INPUT_MESSAGE_INFO', ariaLabel)}`, true)); 4883 } else { 4884 this.warningCount++; 4885 el.classList.add("jooa11y-warning-border"); 4886 el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), `$Lang._('LABELS_ARIA_LABEL_INPUT_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LABELS_ARIA_LABEL_INPUT_MESSAGE_INFO', ariaLabel)}`, true)); 4887 } 4888 } //Implicit labels. 4889 else if (el.closest('label') && el.closest('label').textContent.trim()) ; //Has an ID but doesn't have a matching FOR attribute. 4890 else if (el.getAttribute("id") && Array.from(el.parentElement.children).filter($c => $c.nodeName === 'LABEL').length) { 4891 const $labels = Array.from(el.parentElement.children).filter($c => $c.nodeName === 'LABEL'); 4892 let hasFor = false; 4893 $labels.forEach($l => { 4894 if (hasFor) return; 4895 4896 if ($l.getAttribute('for') === el.getAttribute('id')) { 4897 hasFor = true; 4898 } 4899 }); 4900 4901 if (!hasFor) { 4902 this.errorCount++; 4903 el.classList.add("jooa11y-error-border"); 4904 el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), `$Lang._('LABELS_NO_FOR_ATTRIBUTE_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('LABELS_NO_FOR_ATTRIBUTE_MESSAGE_INFO', el.getAttribute('id'))}`, true)); 4905 } 4906 } else { 4907 this.errorCount++; 4908 el.classList.add("jooa11y-error-border"); 4909 el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), Lang._('LABELS_MISSING_LABEL_MESSAGE'), true)); 4910 } 4911 }); 4912 } 4913 4914 // ============================================================ 4915 // Rulesets: Embedded content. 4916 // ============================================================ 4917 checkEmbeddedContent() { 4918 const $findiframes = Array.from(this.$root.querySelectorAll("iframe, audio, video")); 4919 const $iframes = $findiframes.filter($el => !this.$containerExclusions.includes($el)); //Warning: Video content. 4920 4921 const $videos = $iframes.filter($el => $el.matches(this.options.videoContent)); 4922 $videos.forEach($el => { 4923 let track = $el.getElementsByTagName('TRACK'); 4924 if ($el.tagName === "VIDEO" && track.length) ;else { 4925 this.warningCount++; 4926 $el.classList.add("jooa11y-warning-border"); 4927 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), Lang._('EMBED_VIDEO'))); 4928 } 4929 }); //Warning: Audio content. 4930 4931 const $audio = $iframes.filter($el => $el.matches(this.options.audioContent)); 4932 $audio.forEach($el => { 4933 this.warningCount++; 4934 $el.classList.add("jooa11y-warning-border"); 4935 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), Lang._('EMBED_AUDIO'))); 4936 }); //Error: iFrame is missing accessible name. 4937 4938 $iframes.forEach($el => { 4939 if ($el.tagName === "VIDEO" || $el.tagName === "AUDIO" || $el.getAttribute("aria-hidden") === "true" || $el.getAttribute("hidden") !== null || $el.style.display === 'none' || $el.getAttribute("role") === "presentation") ;else if ($el.getAttribute("title") === null || $el.getAttribute("title") === '') { 4940 if ($el.getAttribute("aria-label") === null || $el.getAttribute("aria-label") === '') { 4941 if ($el.getAttribute("aria-labelledby") === null) { 4942 //Make sure red error border takes precedence 4943 if ($el.classList.contains("jooa11y-warning-border")) { 4944 $el.classList.remove("jooa11y-warning-border"); 4945 } 4946 4947 this.errorCount++; 4948 $el.classList.add("jooa11y-error-border"); 4949 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('EMBED_MISSING_TITLE'))); 4950 } 4951 } 4952 } else ; 4953 }); 4954 const $embeddedcontent = $iframes.filter($el => !$el.matches(this.options.embeddedContent)); 4955 $embeddedcontent.forEach($el => { 4956 if ($el.tagName === "VIDEO" || $el.tagName === "AUDIO" || $el.getAttribute("aria-hidden") === "true" || $el.getAttribute("hidden") !== null || $el.style.display === 'none' || $el.getAttribute("role") === "presentation" || $el.getAttribute("tabindex") === "-1") ;else { 4957 this.warningCount++; 4958 $el.classList.add("jooa11y-warning-border"); 4959 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), Lang._('EMBED_GENERAL_WARNING'))); 4960 } 4961 }); 4962 } // ============================================================ 4963 // Rulesets: QA 4964 // ============================================================ 4965 4966 4967 checkQA() { 4968 //Error: Find all links pointing to development environment. 4969 const $findbadDevLinks = this.options.linksToFlag ? Array.from(this.$root.querySelectorAll(this.options.linksToFlag)) : []; 4970 const $badDevLinks = $findbadDevLinks.filter($el => !this.$containerExclusions.includes($el)); 4971 $badDevLinks.forEach($el => { 4972 this.errorCount++; 4973 $el.classList.add("jooa11y-error-text"); 4974 $el.insertAdjacentHTML('afterend', this.annotate(Lang._('ERROR'), Lang.sprintf('QA_BAD_LINK', $el.getAttribute('href')), true)); 4975 }); //Warning: Find all PDFs. Although only append warning icon to first PDF on page. 4976 4977 let checkPDF = Array.from(this.$root.querySelectorAll('a[href$=".pdf"]')).filter(p => !this.$containerExclusions.includes(p)); 4978 let firstPDF = checkPDF[0]; 4979 let pdfCount = checkPDF.length; 4980 4981 if (checkPDF.length > 0) { 4982 this.warningCount++; 4983 checkPDF.forEach($pdf => { 4984 $pdf.classList.add('jooa11y-warning-text'); 4985 4986 if ($pdf.querySelector('img')) { 4987 $pdf.classList.remove('jooa11y-warning-text'); 4988 } 4989 }); 4990 firstPDF.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), Lang.sprintf('QA_PDF_COUNT', pdfCount), true)); 4991 } //Warning: Detect uppercase. 4992 4993 4994 const $findallcaps = Array.from(this.$root.querySelectorAll("h1, h2, h3, h4, h5, h6, p, li:not([class^='jooa11y']), blockquote")); 4995 const $allcaps = $findallcaps.filter($el => !this.$containerExclusions.includes($el)); 4996 $allcaps.forEach(function ($el) { 4997 let uppercasePattern = /(?!<a[^>]*?>)(\b[A-Z][',!:A-Z\s]{15,}|\b[A-Z]{15,}\b)(?![^<]*?<\/a>)/g; 4998 let html = $el.innerHTML; 4999 $el.innerHTML = html.replace(uppercasePattern, "<span class='jooa11y-warning-uppercase'>$1</span>"); 5000 }); 5001 const $warningUppercase = document.querySelectorAll(".jooa11y-warning-uppercase"); 5002 $warningUppercase.forEach($el => { 5003 $el.insertAdjacentHTML('afterend', this.annotate(Lang._('WARNING'), Lang._('QA_UPPERCASE_WARNING'), true)); 5004 }); 5005 5006 if ($warningUppercase.length > 0) { 5007 this.warningCount++; 5008 } //Tables check. 5009 5010 5011 const $findtables = Array.from(this.$root.querySelectorAll("table:not([role='presentation'])")); 5012 const $tables = $findtables.filter($el => !this.$containerExclusions.includes($el)); 5013 $tables.forEach($el => { 5014 let findTHeaders = $el.querySelectorAll("th"); 5015 let findHeadingTags = $el.querySelectorAll("h1, h2, h3, h4, h5, h6"); 5016 5017 if (findTHeaders.length === 0) { 5018 this.errorCount++; 5019 $el.classList.add("jooa11y-error-border"); 5020 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), Lang._('TABLES_MISSING_HEADINGS'))); 5021 } 5022 5023 if (findHeadingTags.length > 0) { 5024 this.errorCount++; 5025 findHeadingTags.forEach($el => { 5026 $el.classList.add("jooa11y-error-border"); 5027 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), `$Lang._('TABLES_SEMANTIC_HEADING')} <hr aria-hidden="true"> $Lang._('TABLES_SEMANTIC_HEADING_INFO')}`)); 5028 }); 5029 } 5030 5031 findTHeaders.forEach($el => { 5032 if ($el.textContent.trim().length === 0) { 5033 this.errorCount++; 5034 $el.classList.add("jooa11y-error-border"); 5035 $el.innerHTML = this.annotate(Lang._('ERROR'), `$Lang._('TABLES_EMPTY_HEADING')} <hr aria-hidden="true"> $Lang._('TABLES_EMPTY_HEADING_INFO')}`); 5036 } 5037 }); 5038 }); //Error: Missing language tag. Lang should be at least 2 characters. 5039 5040 const lang = document.querySelector("html").getAttribute("lang"); 5041 5042 if (!lang || lang.length < 2) { 5043 this.errorCount++; 5044 const jooa11yContainer = document.getElementById("jooa11y-container"); 5045 jooa11yContainer.insertAdjacentHTML('afterend', this.annotateBanner(Lang._('ERROR'), Lang._('QA_PAGE_LANGUAGE_MESSAGE'))); 5046 } //Excessive bolding or italics. 5047 5048 5049 const $findstrongitalics = Array.from(this.$root.querySelectorAll("strong, em")); 5050 const $strongitalics = $findstrongitalics.filter($el => !this.$containerExclusions.includes($el)); 5051 $strongitalics.forEach($el => { 5052 if ($el.textContent.trim().length > 200) { 5053 this.warningCount++; 5054 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), Lang._('QA_BAD_ITALICS'))); 5055 } 5056 }); //Find blockquotes used as headers. 5057 5058 const $findblockquotes = Array.from(this.$root.querySelectorAll("blockquote")); 5059 const $blockquotes = $findblockquotes.filter($el => !this.$containerExclusions.includes($el)); 5060 $blockquotes.forEach($el => { 5061 let bqHeadingText = $el.textContent; 5062 5063 if (bqHeadingText.trim().length < 25) { 5064 this.warningCount++; 5065 $el.classList.add("jooa11y-warning-border"); 5066 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang.sprintf('QA_BLOCKQUOTE_MESSAGE', bqHeadingText)} <hr aria-hidden="true"> $Lang._('QA_BLOCKQUOTE_MESSAGE_TIP')}`)); 5067 } 5068 }); // Warning: Detect fake headings. 5069 5070 this.$p.forEach($el => { 5071 let brAfter = $el.innerHTML.indexOf("</strong><br>"); 5072 let brBefore = $el.innerHTML.indexOf("<br></strong>"); //Check paragraphs greater than x characters. 5073 5074 if ($el && $el.textContent.trim().length >= 300) { 5075 let firstChild = $el.firstChild; //If paragraph starts with <strong> tag and ends with <br>. 5076 5077 if (firstChild.tagName === "STRONG" && (brBefore !== -1 || brAfter !== -1)) { 5078 let boldtext = firstChild.textContent; 5079 5080 if (!$el.closest("table") && boldtext.length <= 120) { 5081 firstChild.classList.add("jooa11y-fake-heading", "jooa11y-warning-border"); 5082 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang.sprintf('QA_FAKE_HEADING', boldtext)} <hr aria-hidden="true"> $Lang._('QA_FAKE_HEADING_INFO')}`)); 5083 } 5084 } 5085 } // If paragraph only contains <p><strong>...</strong></p>. 5086 5087 5088 if (/^<(strong)>.+<\/\1>$/.test($el.innerHTML.trim())) { 5089 //Although only flag if it: 5090 // 1) Has less than 120 characters (typical heading length). 5091 // 2) The previous element is not a heading. 5092 const prevElement = $el.previousElementSibling; 5093 let tagName = ""; 5094 5095 if (prevElement !== null) { 5096 tagName = prevElement.tagName; 5097 } 5098 5099 if (!$el.closest("table") && $el.textContent.length <= 120 && tagName.charAt(0) !== "H") { 5100 let boldtext = $el.textContent; 5101 $el.classList.add("jooa11y-fake-heading", "jooa11y-warning-border"); 5102 $el.firstChild.insertAdjacentHTML("afterend", this.annotate(Lang._('WARNING'), `$Lang.sprintf('QA_FAKE_HEADING', boldtext)} <hr aria-hidden="true"> $Lang._('QA_FAKE_HEADING_INFO')}`)); 5103 } 5104 } 5105 }); 5106 5107 if (this.$root.querySelectorAll(".jooa11y-fake-heading").length > 0) { 5108 this.warningCount++; 5109 } // Check duplicate ID 5110 5111 5112 const ids = this.$root.querySelectorAll('[id]'); 5113 let allIds = {}; 5114 ids.forEach($el => { 5115 let id = $el.id; 5116 5117 if (id) { 5118 if (allIds[id] === undefined) { 5119 allIds[id] = 1; 5120 } else { 5121 $el.classList.add("sa11y-error-border"); 5122 $el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('QA_DUPLICATE_ID')} 5123 <hr aria-hidden="true"> 5124 $Lang.sprintf('QA_DUPLICATE_ID_TIP', id)}`, true)); 5125 } 5126 } 5127 }); 5128 /* Thanks to John Jameson from PrincetonU for this ruleset! */ 5129 // Detect paragraphs that should be lists. 5130 5131 let activeMatch = ""; 5132 let prefixDecrement = { 5133 b: "a", 5134 B: "A", 5135 2: "1" 5136 }; 5137 let prefixMatch = /a\.|a\)|A\.|A\)|1\.|1\)|\*\s|-\s|--|•\s|→\s|✓\s|✔\s|✗\s|✖\s|✘\s|❯\s|›\s|»\s/; 5138 5139 let decrement = function (el) { 5140 return el.replace(/^b|^B|^2/, function (match) { 5141 return prefixDecrement[match]; 5142 }); 5143 }; 5144 5145 this.$p.forEach(el => { 5146 let hit = false; // Grab first two characters. 5147 5148 let firstPrefix = el.textContent.substring(0, 2); 5149 5150 if (firstPrefix.trim().length > 0 && firstPrefix !== activeMatch && firstPrefix.match(prefixMatch)) { 5151 // We have a prefix and a possible hit 5152 // Split p by carriage return if present and compare. 5153 let hasBreak = el.innerHTML.indexOf("<br>"); 5154 5155 if (hasBreak !== -1) { 5156 let subParagraph = el.innerHTML.substring(hasBreak + 4).trim(); 5157 let subPrefix = subParagraph.substring(0, 2); 5158 5159 if (firstPrefix === decrement(subPrefix)) { 5160 hit = true; 5161 } 5162 } // Decrement the second p prefix and compare . 5163 5164 5165 if (!hit) { 5166 let $second = el.nextElementSibling.nodeName === 'P' ? el.nextElementSibling : null; 5167 5168 if ($second) { 5169 let secondPrefix = decrement(el.nextElementSibling.textContent.substring(0, 2)); 5170 5171 if (firstPrefix === secondPrefix) { 5172 hit = true; 5173 } 5174 } 5175 } 5176 5177 if (hit) { 5178 this.warningCount++; 5179 el.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang.sprintf('QA_SHOULD_BE_LIST', firstPrefix)} <hr aria-hidden="true"> $Lang._('QA_SHOULD_BE_LIST_TIP')}`)); 5180 el.classList.add("jooa11y-fake-list"); 5181 activeMatch = firstPrefix; 5182 } else { 5183 activeMatch = ""; 5184 } 5185 } else { 5186 activeMatch = ""; 5187 } 5188 }); 5189 5190 if (this.$root.querySelectorAll('.jooa11y-fake-list').length > 0) { 5191 this.warningCount++; 5192 } 5193 } 5194 5195 // ============================================================ 5196 // Rulesets: Contrast 5197 // Color contrast plugin by jasonday: https://github.com/jasonday/color-contrast 5198 // ============================================================ 5199 checkContrast() { 5200 const $findcontrast = Array.from(this.$root.querySelectorAll("* > :not(.jooa11y-heading-label)")); 5201 const $contrast = $findcontrast.filter($el => !this.$containerExclusions.includes($el)); 5202 var contrastErrors = { 5203 errors: [], 5204 warnings: [] 5205 }; 5206 let elements = $contrast; 5207 let contrast = { 5208 // Parse rgb(r, g, b) and rgba(r, g, b, a) strings into an array. 5209 // Adapted from https://github.com/gka/chroma.js 5210 parseRgb: function (css) { 5211 let i, m, rgb, _i, _j; 5212 5213 if (m = css.match(/rgb\(\s*(\-?\d+),\s*(\-?\d+)\s*,\s*(\-?\d+)\s*\)/)) { 5214 rgb = m.slice(1, 4); 5215 5216 for (i = _i = 0; _i <= 2; i = ++_i) { 5217 rgb[i] = +rgb[i]; 5218 } 5219 5220 rgb[3] = 1; 5221 } else if (m = css.match(/rgba\(\s*(\-?\d+),\s*(\-?\d+)\s*,\s*(\-?\d+)\s*,\s*([01]|[01]?\.\d+)\)/)) { 5222 rgb = m.slice(1, 5); 5223 5224 for (i = _j = 0; _j <= 3; i = ++_j) { 5225 rgb[i] = +rgb[i]; 5226 } 5227 } 5228 5229 return rgb; 5230 }, 5231 // Based on http://www.w3.org/TR/WCAG20/#relativeluminancedef 5232 relativeLuminance: function (c) { 5233 let lum = []; 5234 5235 for (let i = 0; i < 3; i++) { 5236 let v = c[i] / 255; 5237 lum.push(v < 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4)); 5238 } 5239 5240 return 0.2126 * lum[0] + 0.7152 * lum[1] + 0.0722 * lum[2]; 5241 }, 5242 // Based on http://www.w3.org/TR/WCAG20/#contrast-ratiodef 5243 contrastRatio: function (x, y) { 5244 let l1 = contrast.relativeLuminance(contrast.parseRgb(x)); 5245 let l2 = contrast.relativeLuminance(contrast.parseRgb(y)); 5246 return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); 5247 }, 5248 getBackground: function (el) { 5249 let styles = getComputedStyle(el), 5250 bgColor = styles.backgroundColor, 5251 bgImage = styles.backgroundImage, 5252 rgb = contrast.parseRgb(bgColor) + '', 5253 alpha = rgb.split(','); // if background has alpha transparency, flag manual check 5254 5255 if (alpha[3] < 1 && alpha[3] > 0) { 5256 return "alpha"; 5257 } // if element has no background image, or transparent background (alpha == 0) return bgColor 5258 5259 5260 if (bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' && bgImage === "none" && alpha[3] !== '0') { 5261 return bgColor; 5262 } else if (bgImage !== "none") { 5263 return "image"; 5264 } // retest if not returned above 5265 5266 5267 if (el.tagName === 'HTML') { 5268 return 'rgb(255, 255, 255)'; 5269 } else { 5270 return contrast.getBackground(el.parentNode); 5271 } 5272 }, 5273 // check visibility - based on jQuery method 5274 // isVisible: function (el) { 5275 // return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length); 5276 // }, 5277 check: function () { 5278 // resets results 5279 contrastErrors = { 5280 errors: [], 5281 warnings: [] 5282 }; 5283 5284 for (let i = 0; i < elements.length; i++) { 5285 (function (elem) { 5286 // Test if visible. Although we want invisible too. 5287 if (contrast 5288 /* .isVisible(elem) */ 5289 ) { 5290 let style = getComputedStyle(elem), 5291 color = style.color, 5292 fill = style.fill, 5293 fontSize = parseInt(style.fontSize), 5294 pointSize = fontSize * 3 / 4, 5295 fontWeight = style.fontWeight, 5296 htmlTag = elem.tagName, 5297 background = contrast.getBackground(elem), 5298 textString = [].reduce.call(elem.childNodes, function (a, b) { 5299 return a + (b.nodeType === 3 ? b.textContent : ''); 5300 }, ''), 5301 text = textString.trim(), 5302 ratio, 5303 error, 5304 warning; 5305 5306 if (htmlTag === "SVG") { 5307 ratio = Math.round(contrast.contrastRatio(fill, background) * 100) / 100; 5308 5309 if (ratio < 3) { 5310 error = { 5311 elem: elem, 5312 ratio: ratio + ':1' 5313 }; 5314 contrastErrors.errors.push(error); 5315 } 5316 } else if (text.length || htmlTag === "INPUT" || htmlTag === "SELECT" || htmlTag === "TEXTAREA") { 5317 // does element have a background image - needs to be manually reviewed 5318 if (background === "image") { 5319 warning = { 5320 elem: elem 5321 }; 5322 contrastErrors.warnings.push(warning); 5323 } else if (background === "alpha") { 5324 warning = { 5325 elem: elem 5326 }; 5327 contrastErrors.warnings.push(warning); 5328 } else { 5329 ratio = Math.round(contrast.contrastRatio(color, background) * 100) / 100; 5330 5331 if (pointSize >= 18 || pointSize >= 14 && fontWeight >= 700) { 5332 if (ratio < 3) { 5333 error = { 5334 elem: elem, 5335 ratio: ratio + ':1' 5336 }; 5337 contrastErrors.errors.push(error); 5338 } 5339 } else { 5340 if (ratio < 4.5) { 5341 error = { 5342 elem: elem, 5343 ratio: ratio + ':1' 5344 }; 5345 contrastErrors.errors.push(error); 5346 } 5347 } 5348 } 5349 } 5350 } 5351 })(elements[i]); 5352 } 5353 5354 return contrastErrors; 5355 } 5356 }; 5357 contrast.check(); //const {errorMessage, warningMessage} = jooa11yIM["contrast"]; 5358 5359 contrastErrors.errors.forEach(item => { 5360 let name = item.elem; 5361 let cratio = item.ratio; 5362 let clone = name.cloneNode(true); 5363 let removeJooa11yHeadingLabel = clone.querySelectorAll('.jooa11y-heading-label'); 5364 5365 for (let i = 0; i < removeJooa11yHeadingLabel.length; i++) { 5366 clone.removeChild(removeJooa11yHeadingLabel[i]); 5367 } 5368 5369 let nodetext = clone.textContent; 5370 this.errorCount++; 5371 5372 if (name.tagName === "INPUT") { 5373 name.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), `$Lang._('CONTRAST_ERROR_INPUT_MESSAGE')} 5374 <hr aria-hidden="true"> 5375 $Lang.sprintf('CONTRAST_ERROR_INPUT_MESSAGE_INFO', cratio)}`, true)); 5376 } else { 5377 name.insertAdjacentHTML('beforebegin', this.annotate(Lang._('ERROR'), `$Lang.sprintf('CONTRAST_ERROR_MESSAGE', cratio, nodetext)} 5378 <hr aria-hidden="true"> 5379 $Lang.sprintf('CONTRAST_ERROR_MESSAGE_INFO', cratio, nodetext)}`, true)); 5380 } 5381 }); 5382 contrastErrors.warnings.forEach(item => { 5383 let name = item.elem; 5384 let clone = name.cloneNode(true); 5385 let removeJooa11yHeadingLabel = clone.querySelectorAll('.jooa11y-heading-label'); 5386 5387 for (let i = 0; i < removeJooa11yHeadingLabel.length; i++) { 5388 clone.removeChild(removeJooa11yHeadingLabel[i]); 5389 } 5390 5391 let nodetext = clone.textContent; 5392 this.warningCount++; 5393 name.insertAdjacentHTML('beforebegin', this.annotate(Lang._('WARNING'), `$Lang._('CONTRAST_WARNING_MESSAGE')} <hr aria-hidden="true"> $Lang.sprintf('CONTRAST_WARNING_MESSAGE_INFO', nodetext)}`, true)); 5394 }); 5395 } 5396 5397 // ============================================================ 5398 // Rulesets: Readability 5399 // Adapted from Greg Kraus' readability script: https://accessibility.oit.ncsu.edu/it-accessibility-at-nc-state/developers/tools/readability-bookmarklet/ 5400 // ============================================================ 5401 checkReadability() { 5402 const container = document.querySelector(this.options.readabilityRoot); 5403 const $findreadability = Array.from(container.querySelectorAll("p, li")); 5404 const $readability = $findreadability.filter($el => !this.$containerExclusions.includes($el)); //Crude hack to add a period to the end of list items to make a complete sentence. 5405 5406 $readability.forEach($el => { 5407 let listText = $el.textContent; 5408 5409 if (listText.length >= 120) { 5410 if (listText.charAt(listText.length - 1) !== ".") { 5411 $el.insertAdjacentHTML("beforeend", "<span class='jooa11y-readability-period jooa11y-visually-hidden'>.</span>"); 5412 } 5413 } 5414 }); // Compute syllables: http://stackoverflow.com/questions/5686483/how-to-compute-number-of-syllables-in-a-word-in-javascript 5415 5416 function number_of_syllables(wordCheck) { 5417 wordCheck = wordCheck.toLowerCase().replace('.', '').replace('\n', ''); 5418 5419 if (wordCheck.length <= 3) { 5420 return 1; 5421 } 5422 5423 wordCheck = wordCheck.replace(/(?:[^laeiouy]es|ed|[^laeiouy]e)$/, ''); 5424 wordCheck = wordCheck.replace(/^y/, ''); 5425 let syllable_string = wordCheck.match(/[aeiouy]{1,2}/g); 5426 let syllables = 0; 5427 5428 if (!!syllable_string) { 5429 syllables = syllable_string.length; 5430 } 5431 5432 return syllables; 5433 } 5434 5435 let readabilityarray = []; 5436 5437 for (let i = 0; i < $readability.length; i++) { 5438 var current = $readability[i]; 5439 5440 if (current.textContent.replace(/ |\n/g, '') !== '') { 5441 readabilityarray.push(current.textContent); 5442 } 5443 } 5444 5445 let paragraphtext = readabilityarray.join(' ').trim().toString(); 5446 let words_raw = paragraphtext.replace(/[.!?-]+/g, ' ').split(' '); 5447 let words = 0; 5448 5449 for (let i = 0; i < words_raw.length; i++) { 5450 if (words_raw[i] != 0) { 5451 words = words + 1; 5452 } 5453 } 5454 5455 let sentences_raw = paragraphtext.split(/[.!?]+/); 5456 let sentences = 0; 5457 5458 for (let i = 0; i < sentences_raw.length; i++) { 5459 if (sentences_raw[i] !== '') { 5460 sentences = sentences + 1; 5461 } 5462 } 5463 5464 let total_syllables = 0; 5465 let syllables1 = 0; 5466 let syllables2 = 0; 5467 5468 for (let i = 0; i < words_raw.length; i++) { 5469 if (words_raw[i] != 0) { 5470 var syllable_count = number_of_syllables(words_raw[i]); 5471 5472 if (syllable_count === 1) { 5473 syllables1 = syllables1 + 1; 5474 } 5475 5476 if (syllable_count === 2) { 5477 syllables2 = syllables2 + 1; 5478 } 5479 5480 total_syllables = total_syllables + syllable_count; 5481 } 5482 } //var characters = paragraphtext.replace(/[.!?|\s]+/g, '').length; 5483 //Reference: https://core.ac.uk/download/pdf/6552422.pdf 5484 //Reference: https://github.com/Yoast/YoastSEO.js/issues/267 5485 5486 5487 let flesch_reading_ease; 5488 5489 if (this.options.readabilityLang === 'en') { 5490 flesch_reading_ease = 206.835 - 1.015 * words / sentences - 84.6 * total_syllables / words; 5491 } else if (this.options.readabilityLang === 'fr') { 5492 //French (Kandel & Moles) 5493 flesch_reading_ease = 207 - 1.015 * words / sentences - 73.6 * total_syllables / words; 5494 } else if (this.options.readabilityLang === 'es') { 5495 flesch_reading_ease = 206.84 - 1.02 * words / sentences - 0.60 * (100 * total_syllables / words); 5496 } 5497 5498 if (flesch_reading_ease > 100) { 5499 flesch_reading_ease = 100; 5500 } else if (flesch_reading_ease < 0) { 5501 flesch_reading_ease = 0; 5502 } 5503 5504 const $readabilityinfo = document.getElementById("jooa11y-readability-info"); 5505 5506 if (paragraphtext.length === 0) { 5507 $readabilityinfo.innerHTML = Lang._('READABILITY_NO_P_OR_LI_MESSAGE'); 5508 } else if (words > 30) { 5509 let fleschScore = flesch_reading_ease.toFixed(1); 5510 let avgWordsPerSentence = (words / sentences).toFixed(1); 5511 let complexWords = Math.round(100 * ((words - (syllables1 + syllables2)) / words)); //WCAG AAA pass if greater than 60 5512 5513 if (fleschScore >= 0 && fleschScore < 30) { 5514 $readabilityinfo.innerHTML = `<span>$fleschScore}</span> <span class="jooa11y-readability-score">$Lang._('VERY_DIFFICULT_READABILITY')}</span>`; 5515 } else if (fleschScore > 31 && fleschScore < 49) { 5516 $readabilityinfo.innerHTML = `<span>$fleschScore}</span> <span class="jooa11y-readability-score">$Lang._('DIFFICULT_READABILITY')}</span>`; 5517 } else if (fleschScore > 50 && fleschScore < 60) { 5518 $readabilityinfo.innerHTML = `<span>$fleschScore}</span> <span class="jooa11y-readability-score">$Lang._('FAIRLY_DIFFICULT_READABILITY')}</span>`; 5519 } else { 5520 $readabilityinfo.innerHTML = `<span>$fleschScore}</span> <span class="jooa11y-readability-score">$Lang._('GOOD_READABILITY')}</span>`; 5521 } 5522 5523 document.getElementById("jooa11y-readability-details").innerHTML = `<li><span class='jooa11y-bold'>$Lang._('AVG_WORD_PER_SENTENCE')}</span> $avgWordsPerSentence}</li> 5524 <li><span class='jooa11y-bold'>$Lang._('COMPLEX_WORDS')}</span> $complexWords}%</li> 5525 <li><span class='jooa11y-bold'>$Lang._('TOTAL_WORDS')}</span> $words}</li>`; 5526 } else { 5527 $readabilityinfo.textContent = Lang._('READABILITY_NOT_ENOUGH_CONTENT_MESSAGE'); 5528 } 5529 } 5530 5531 //---------------------------------------------------------------------- 5532 // Templating for Error, Warning and Pass buttons. 5533 //---------------------------------------------------------------------- 5534 annotate(type, content, inline = false) { 5535 const validTypes = [Lang._('ERROR'), Lang._('WARNING'), Lang._('GOOD')]; 5536 5537 if (validTypes.indexOf(type) === -1) { 5538 throw Error(`Invalid type [$type}] for annotation`); 5539 } 5540 5541 const CSSName = { 5542 [validTypes[0]]: "error", 5543 [validTypes[1]]: "warning", 5544 [validTypes[2]]: "good" 5545 }; // Check if content is a function 5546 5547 if (content && {}.toString.call(content) === "[object Function]") { 5548 // if it is, call it and get the value. 5549 content = content(); 5550 } // Escape content, it is need because it used inside data-tippy-content="" 5551 5552 5553 content = escapeHTML(content); 5554 return ` 5555 <div class=$inline ? "jooa11y-instance-inline" : "jooa11y-instance"}> 5556 <button 5557 type="button" 5558 aria-label="${[type]}" 5559 class="jooa11y-btn jooa11y-$CSSName[type]}-btn$inline ? "-text" : ""}" 5560 data-tippy-content="<div lang='$this.options.langCode}'> 5561 <div class='jooa11y-header-text'>${[type]}</div> 5562 $content} 5563 </div> 5564 "> 5565 </button> 5566 </div>`; 5567 } 5568 5569 //---------------------------------------------------------------------- 5570 // Templating for full-width banners. 5571 //---------------------------------------------------------------------- 5572 annotateBanner(type, content) { 5573 const validTypes = [Lang._('ERROR'), Lang._('WARNING'), Lang._('GOOD')]; 5574 5575 if (validTypes.indexOf(type) === -1) { 5576 throw Error(`Invalid type [$type}] for annotation`); 5577 } 5578 5579 const CSSName = { 5580 [validTypes[0]]: "error", 5581 [validTypes[1]]: "warning", 5582 [validTypes[2]]: "good" 5583 }; // Check if content is a function 5584 5585 if (content && {}.toString.call(content) === "[object Function]") { 5586 // if it is, call it and get the value. 5587 content = content(); 5588 } 5589 5590 return `<div class="jooa11y-instance jooa11y-$CSSName[type]}-message-container"> 5591 <div role="region" aria-label="${[type]}" class="jooa11y-$CSSName[type]}-message" lang="$this.options.langCode}"> 5592 $content} 5593 </div> 5594 </div>`; 5595 } 5596 5597 } 5598 5599 if (!Joomla) { 5600 throw new Error('Joomla API is not properly initialised'); 5601 } 5602 5603 const stringPrefix = 'PLG_SYSTEM_JOOA11Y_'; 5604 5605 Lang.translate = string => Joomla.Text._(stringPrefix + string, string); 5606 5607 const options = Joomla.getOptions('jooa11yOptions'); 5608 window.addEventListener('load', () => { 5609 // Instantiate 5610 const checker = new Jooa11y(options); 5611 checker.doInitialCheck(); 5612 });
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |