1/*global H5P*/ 2H5P.Tooltip = (function () { 3 'use strict'; 4 5 /** 6 * Create an accessible tooltip 7 * 8 * @param {HTMLElement} triggeringElement The element that should trigger the tooltip 9 * @param {Object} options Options for tooltip 10 * @param {String} options.text The text to be displayed in the tooltip 11 * If not set, will attempt to set text = aria-label of triggeringElement 12 * @param {String[]} options.classes Extra css classes for the tooltip 13 * @param {Boolean} options.ariaHidden Whether the hover should be read by screen readers or not (default: true) 14 * @param {String} options.position Where the tooltip should appear in relation to the 15 * triggeringElement. Accepted positions are "top" (default), "left", "right" and "bottom" 16 * 17 * @constructor 18 */ 19 function Tooltip(triggeringElement, options) { 20 21 // Make sure tooltips have unique id 22 H5P.Tooltip.uniqueId += 1; 23 const tooltipId = 'h5p-tooltip-' + H5P.Tooltip.uniqueId; 24 25 // Default options 26 options = options || {}; 27 options.classes = options.classes || []; 28 options.ariaHidden = options.ariaHidden || true; 29 30 // Initiate state 31 let hover = false; 32 let focus = false; 33 34 // Function used by the escape listener 35 const escapeFunction = function (e) { 36 if (e.key === 'Escape') { 37 tooltip.classList.remove('h5p-tooltip-visible'); 38 } 39 } 40 41 // Create element 42 const tooltip = document.createElement('div'); 43 44 tooltip.classList.add('h5p-tooltip'); 45 tooltip.id = tooltipId; 46 tooltip.role = 'tooltip'; 47 tooltip.innerHTML = options.text || triggeringElement.getAttribute('aria-label') || ''; 48 tooltip.setAttribute('aria-hidden', options.ariaHidden); 49 tooltip.classList.add(...options.classes); 50 51 triggeringElement.appendChild(tooltip); 52 53 // Set the initial position based on options.position 54 switch (options.position) { 55 case 'left': 56 tooltip.classList.add('h5p-tooltip-left'); 57 break; 58 case 'right': 59 tooltip.classList.add('h5p-tooltip-right'); 60 break; 61 case 'bottom': 62 tooltip.classList.add('h5p-tooltip-bottom'); 63 break; 64 default: 65 options.position = 'top'; 66 } 67 68 // Aria-describedby will override aria-hidden 69 if (!options.ariaHidden) { 70 triggeringElement.setAttribute('aria-describedby', tooltipId); 71 } 72 73 // Add event listeners to triggeringElement 74 triggeringElement.addEventListener('mouseenter', function () { 75 showTooltip(true); 76 }); 77 triggeringElement.addEventListener('mouseleave', function () { 78 hideTooltip(true); 79 }); 80 triggeringElement.addEventListener('focusin', function () { 81 showTooltip(false); 82 }); 83 triggeringElement.addEventListener('focusout', function () { 84 hideTooltip(false); 85 }); 86 87 // Prevent clicks on the tooltip from triggering onClick listeners on the triggeringElement 88 tooltip.addEventListener('click', function (event) { 89 event.stopPropagation(); 90 }); 91 92 // Use a mutation observer to listen for aria-label being 93 // changed for the triggering element. If so, update the tooltip. 94 // Mutation observer will be used even if the original elements 95 // doesn't have any aria-label. 96 new MutationObserver(function (mutations) { 97 const ariaLabel = mutations[0].target.getAttribute('aria-label'); 98 if (ariaLabel) { 99 tooltip.innerHTML = options.text || ariaLabel; 100 } 101 }).observe(triggeringElement, { 102 attributes: true, 103 attributeFilter: ['aria-label'], 104 }); 105 106 // Use intersection observer to adjust the tooltip if it is not completely visible 107 new IntersectionObserver(function (entries) { 108 entries.forEach((entry) => { 109 const target = entry.target; 110 const positionClass = 'h5p-tooltip-' + options.position; 111 112 // Stop adjusting when hidden (to prevent a false positive next time) 113 if (entry.intersectionRatio === 0) { 114 ['h5p-tooltip-down', 'h5p-tooltip-left', 'h5p-tooltip-right'] 115 .forEach(function (adjustmentClass) { 116 if (adjustmentClass !== positionClass) { 117 target.classList.remove(adjustmentClass); 118 } 119 }); 120 } 121 // Adjust if not completely visible when meant to be 122 else if (entry.intersectionRatio < 1 && (hover || focus)) { 123 const targetRect = entry.boundingClientRect; 124 const intersectionRect = entry.intersectionRect; 125 126 // Going out of screen on left side 127 if (intersectionRect.left > targetRect.left) { 128 target.classList.add('h5p-tooltip-right'); 129 target.classList.remove(positionClass); 130 } 131 // Going out of screen on right side 132 else if (intersectionRect.right < targetRect.right) { 133 target.classList.add('h5p-tooltip-left'); 134 target.classList.remove(positionClass); 135 } 136 137 // going out of top of screen 138 if (intersectionRect.top > targetRect.top) { 139 target.classList.add('h5p-tooltip-down'); 140 target.classList.remove(positionClass); 141 } 142 // going out of bottom of screen 143 else if (intersectionRect.bottom < targetRect.bottom) { 144 target.classList.add('h5p-tooltip-up'); 145 target.classList.remove(positionClass); 146 } 147 } 148 }); 149 }).observe(tooltip); 150 151 /** 152 * Makes the tooltip visible and activates it's functionality 153 * 154 * @param {Boolean} triggeredByHover True if triggered by mouse, false if triggered by focu
154s 155 */ 156 const showTooltip = function (triggeredByHover) { 157 if (triggeredByHover) { 158 hover = true; 159 } 160 else { 161 focus = true; 162 } 163 164 tooltip.classList.add('h5p-tooltip-visible'); 165 166 // Add listener to iframe body, as esc keypress would not be detected otherwise 167 document.body.addEventListener('keydown', escapeFunction, true); 168 } 169 170 /** 171 * Hides the tooltip and removes listeners 172 * 173 * @param {Boolean} triggeredByHover True if triggered by mouse, false if triggered by focus 174 */ 175 const hideTooltip = function (triggeredByHover) { 176 if (triggeredByHover) { 177 hover = false; 178 } 179 else { 180 focus = false; 181 } 182 183 // Only hide tooltip if neither hovered nor focused 184 if (!hover && !focus) { 185 tooltip.classList.remove('h5p-tooltip-visible'); 186 187 // Remove iframe body listener 188 document.body.removeEventListener('keydown', escapeFunction, true); 189 } 190 } 191 192 /** 193 * Change the text displayed by the tooltip 194 * 195 * @param {String} text The new text to be displayed 196 * Set to null to use aria-label of triggeringElement instead 197 */ 198 this.setText = function (text) { 199 options.text = text; 200 tooltip.innerHTML = options.text || triggeringElement.getAttribute('aria-label') || ''; 201 }; 202 203 /** 204 * Retrieve tooltip 205 * 206 * @return {HTMLElement} 207 */ 208 this.getElement = function () { 209 return tooltip; 210 }; 211 } 212 213 return Tooltip; 214 215})(); 216 217H5P.Tooltip.uniqueId = -1;
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.