PageSourceSearch

https://zaitcev.ru/wp-content/plugins/h5p/h5p-php-library/js/h5p-tooltip.js?ver=1.17.3

js zaitcev.ru collected 2026-10-03 18:13:59 UTC 12,431 bytes, 377 lines download raw bytes

1/* global H5P */
2H5P.Tooltip = (function () {
3  // Position (allowed and default)
4  const Position = {
5    allowed: ['top', 'bottom', 'left', 'right'],
6    default: 'top',
7  };
8
9  /** {number} DELAY_SHOW_MS Delay before tooltip is shown */
10  const DELAY_SHOW_MS = 500;
11
12  /** {number} DELAY_HIDE_MS Delay before tooltip is hidden */
13  const DELAY_HIDE_MS = 500;
14
15  /**
16   * Strips html tags and converts special characters.
17   * Example: "<div>Me &amp; you</div>" is converted to "Me & you".
18   *
19   * @param {String} text The text to be parsed
20   * @returns {String} The parsed text
21   */
22  function parseString(text) {
23    if (text === null || text === undefined) {
24      return '';
25    }
26    const div = document.createElement('div');
27    div.innerHTML = text;
28    return div.textContent;
29  }
30
31  /**
32   * Keep track of whether the user is using their mouse or keyboard to
33   * navigate. Will determine whether tooltip should be shown on focus.
34   */
35  let usingMouse;
36
37  function debounce(callback, delay) {
38    let timeout = null;
39
40    return function (...args) {
41      clearTimeout(timeout);
42      timeout = setTimeout(() => {
43        callback(...args);
44      }, delay);
45    };
46  }
47
48  // The mousemove listener is debounced for performance reasons
49  document.addEventListener('mousemove', debounce(() => usingMouse = true, 100));
50  document.addEventListener('mousedown', () => usingMouse = true);
51  document.addEventListener('keydown', () => usingMouse = false);
52
53  /**
54   * Create an accessible tooltip
55   *
56   * @param {HTMLElement} triggeringElement The element that should trigger the tooltip
57   * @param {Object} options Options for tooltip
58   * @param {String} options.text The text to be displayed in the tooltip
59   *  If not set, will attempt to set text = options.tooltipSource of triggeringElement
60   * @param {String[]} options.classes Extra css classes for the tooltip
61   * @param {Boolean} options.ariaHidden Whether the hover should be read by screen readers or not (default: true)
62   * @param {String} options.position Where the tooltip should appear in relation to the
63   *  triggeringElement. Accepted positions are "top" (default), "left", "right" and "bottom"
64   * @param {String} options.tooltipSource
65   *
66   * @returns {object} returns all the public functions
67   *
68   * @constructor
69   */
70
71  function Tooltip(triggeringElement, options) {
72    // Make sure tooltips have unique id
73    H5P.Tooltip.uniqueId += 1;
74    const tooltipId = `h5p-tooltip-${H5P.Tooltip.uniqueId}`;
75
76    // Default options
77    options = options || {};
78    options.classes = options.classes || [];
79    options.ariaHidden = options.ariaHidden || true;
80    options.tooltipSource = options.tooltipSource || 'aria-label';
81    options.position = (options.position && Position.allowed.includes(options.position))
82      ? options.position
83      : Position.default;
84
85    // Add our internal classes
86    options.classes.push('h5p-tooltip');
87    if (options.position === 'left' || options.position === 'right') {
88      options.classes.push('h5p-tooltip-narrow');
89    }
90
91    // Initiate state
92    let hover = false;
93    let focus = false;
94
95    // Function used by the escape listener
96    const hideOnEscape = function (event) {
97      if (event.key === 'Escape') {
98        tooltip.classList.remove('h5p-tooltip-visible');
99      }
100    };
101
102    // Create element
103    const tooltip = document.createElement('div');
104    tooltip.id = tooltipId;
105    tooltip.role = 'tooltip';
106    tooltip.textContent = parseString(options.text || triggeringElement.getAttribute(options.tooltipSource) || '');
107    tooltip.setAttribute('aria-hidden', options.ariaHidden);
108    tooltip.classList.add(...options.classes);
109
110    document.body.appendChild(tooltip);
111
112    // Aria-describedby will override aria-hidden
113    if (!options.ariaHidden) {
114      triggeringElement.setAttribute('aria-describedby', tooltipId);
115    }
116
117    // Use a mutation observer to listen for options.tooltipSource being
118    // changed for the triggering element. If so, update the tooltip.
119    // Mutation observer will be used even if the original elements
120    // doesn't have any options.tooltipSource.
121    this.observer = new MutationObserver((mutations) => {
122      const updatedText = mutations[0].target.getAttribute(options.tooltipSource);
123
124      if (tooltip.parentNode === null) {
125        triggeringElement.appendChild(tooltip);
126      }
127
128      tooltip.textContent = parseString(options.text || updatedText);
129
130      if (tooltip.textContent.trim().length === 0 && tooltip.classList.contains('h5p-tooltip-visible')) {
131        tooltip.classList.remove('h5p-tooltip-visible');
132      }
133    });
134    this.observer.observe(triggeringElement, {
135      attributes: true,
136      attributeFilter: [options.tooltipSource, 'class'],
137    });
138
139    // A reference to the H5P container (if any). If null, it means
140    // this tooltip is not whithin an H5P.
141    let h5pContainer;
142
143    // Timer responsible for displaying the tooltip x ms after it has been
144    // triggered (either by mouseenter or focusin)
145    let showTooltipTimer;
146
147    // Timer responsible for hiding the tooltip x ms after it has been untriggered
148    let hideTooltipTimer;
149
150    // This timer makes sure the tooltip is not hidden when the mouse
151    // moves from the trigger to the tooltip.
152    let triggerMouseLeaveTimer;
153
154    /**
155     * Makes the tooltip visible and activates it's functionality
156     *
157     * @param {UIEvent} event The triggering event
158     */
159    const showTooltip = function (event, wait = true) {
160      if (!event.target || event.target.disabled || event.target.getAttribute('aria-disabled') === 'true') {
161        return;
162      }
163
164      clearTimeout(hideTooltipTimer); // Prevent from hiding while supposed to show
165
166      if (wait === true) {
167        // We don't want to show the tooltip right away.
168        // Adding a 300 ms waiting period here.
169        clearTimeout(showTooltipTimer);
170        showTooltipTimer = setTimeout(() => {
171          showTooltip(event, false);
172        }, DELAY_SHOW_MS);
173        return;
174      }
175
176      // Don't show tooltip if it is empty
177      if (tooltip.textContent.trim().length === 0) {
178        return;
179      }
180
181      if (event.type === 'mouseenter') {
182        hover = true;
183      }
184      else {
185        focus = true;
186      }
187
188      // Reset placement
189      tooltip.style.left = '';
190      tooltip.style.top = '';
191
192      tooltip.classList.add('h5p-tooltip-visible');
193
194      // Add listener to iframe body, as esc keypress would not be detected otherwise
195      document.body.addEventListener('keydown', hideOnEscape, true);
196
197      // The section below makes sure the tooltip is completely visible
198
199      // H5P.Tooltip can be used both from within an H5P and elsewhere.
200      // The below code is for figuring out the containing element.
201      // h5pContainer has to be looked up the first time we show the tooltip,
202      // since it might not be added to the DOM when H5P.Tooltip is invoked.
203      if (h5pContainer === undefined) {
204        // After the below, h5pContainer is either null or a reference to the
205        // DOM element
206        h5pContainer = triggeringElement.closest('.h5p-container');
207      }
208      const rootRect = h5pContainer ? h5pContainer.getBoundingClientRect() : document.documentElement.getBoundingClientRect();
209      const triggerRect = triggeringElement.getBoundingClientRect();
210      let tooltipRect = tooltip.getBoundingClientRect();
211
212      if (options.position === 'top') {
213        // Places it centered above
214        tooltip.style.left = `${triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2)}px`;
215        tooltip.style.top = `${triggerRect.top - tooltipRect.height}px`;
216      }
217      else if (options.position === 'bottom') {
218        // Places it centered below
219        tooltip.style.left = `${triggerRect.left + (triggerRect.width / 2) - (tooltipRect.width / 2)}px`;
220        tooltip.style.top = `${triggerRect.bottom}px`;
221      }
222      else if (options.position === 'left') {
223        tooltip.style.left = `${triggerRect.left - tooltipRect.width}px`;
224        tooltip.style.top = `${triggerRect.top + (triggerRect.height - tooltipRect.height) / 2}px`;
225        // We trust this option makes the tooltip being shown
226        return;
227      }
228      else if (options.position === 'right') {
229        tooltip.style.left = `${triggerRect.right}px`;
230        tooltip.style.top = `${triggerRect.top + (triggerRect.height - tooltipRect.height) / 2}px`;
231        // We trust this option makes the tooltip being shown
232        return;
233      }
234
235      tooltipRect = tooltip.getBoundingClientRect();
236      const isVisible = tooltipRect.left >= 0
237        && tooltipRect.top >= 0
238        && tooltipRect.right <= rootRect.width
239        && tooltipRect.bottom <= rootRect.height;
240
241      if (!isVisible) {
242        // The tooltip placement needs to be adjusted. This logic will move the
243        // tooltip either left or right if it's placed outside the root element
244        tooltipRect = tooltip.getBoundingClientRect();
245        if (tooltipRect.left < 0) {
246          tooltip.style.left = 0;
247        }
248        else if (tooltipRect.right > rootRect.width) {
249          tooltip.style.left = '';
250          tooltip.style.right = 0;
251        }
252      }
253    };
254
255    /**
256     * Hides the tooltip and removes listeners
257     *
258     * @param {UIEvent} event The triggering event
259     */
260    const hideTooltip = function (event) {
261      let hide = false;
262      let wait = false;
263
264      if (event.type === 'click') {
265        hide = true;
266      }
267      else {
268        if (event.type === 'mouseleave') {
269          wait = true; // Tooltip should not disappear right away
270          hover = false;
271        }
272        else {
273          focus = false;
274        }
275
276        hide = (!hover && !focus);
277      }
278
279      // Only hide tooltip if neither hovered nor focused
280      if (hide) {
281        clearTimeout(showTooltipTimer); // Prevent from showing while supposed to hide
282
283        const cleanupTooltip = () => {
284          tooltip.classList.remove('h5p-tooltip-visible');
285          document.body.removeEventListener('keydown', hideOnEscape, true); // Remove iframe body listener
286        };
287
288        if (wait) {
289          clearTimeout(hideTooltipTimer);
290          hideTooltipTimer = setTimeout(() => {
291            cleanupTooltip();
292          }, DELAY_HIDE_MS);
293        }
294        else {
295          cleanupTooltip();
296        }
297      }
298    };
299
300    // Add event listeners to triggeringElement
301    triggeringElement.addEventListener('mouseenter', showTooltip);
302    triggeringElement.addEventListener('mouseleave', (event) => {
303      triggerMouseLeaveTimer = setTimeout(() => {
304        hideTooltip(event);
305      }, 1);
306    });
307    triggeringElement.addEventListener('focusin', (event) => {
308      if (!usingMouse) {
309        showTooltip(event);
310      }
311    });
312    triggeringElement.addEventListener('focusout', hideTooltip);
313    triggeringElement.addEventListener('click', hideTooltip);
314    tooltip.addEventListener('mouseenter', () => {
315      clearTimeout(triggerMouseLeaveTimer);
316    });
317    tooltip.addEventListener('mouseleave', hideTooltip);
318
319    tooltip.addEventListener('click', (event) => {
320      // Prevent clicks on the tooltip from triggering click
321      // listeners on the triggering element
322      event.stopPropagation();
323      event.preventDefault();
324
325      // Hide the tooltip when it is clicked
326      hideTooltip(event);
327    });
328
329    /**
330     * Change the text displayed by the tooltip
331     *
332     * @param {String} text The new text to be displayed
333     *  Set to null to use options.tooltipSource of triggeringElement instead
334     */
335    this.setText = function (text) {
336      options.text = text;
337      tooltip.textContent = parseString(options.text || triggeringElement.getAttribute(options.tooltipSource) || '');
338    };
339
340    /**
341     * Hide the tooltip
342     */
343    this.hide = function () {
344      hover = focus = false;
345      tooltip.classList.remove('h5p-tooltip-visible');
346    };
347
348    /**
349     * Retrieve tooltip
350     *
351     * @return {HTMLElement}
352     */
353    this.getElement = function () {
354      return tooltip;
355    };
356
357    /**
358     * Remove tooltip
359     */
360    this.remove = function () {
361      this.observer?.disconnect();
362      tooltip.remove();
363    };
364
365    return {
366      setText: this.setText,
367      hide: this.hide,
368      getElement: this.getElement,
369      remove: this.remove,
370      observer: this.observer,
371    };
372  }
373
374  return Tooltip;
375}());
376
377H5P.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.