/** * @file * Manages elements that can offset the size of the viewport. * * Measures and reports viewport offset dimensions from elements like the * toolbar that can potentially displace the positioning of other elements. */ /** * @typedef {object} Drupal~displaceOffset * * @prop {number} top * @prop {number} left * @prop {number} right * @prop {number} bottom */ /** * Triggers when layout of the page changes. * * This is used to position fixed element on the page during page resize and * Toolbar toggling. * * @event drupalViewportOffsetChange */ (function($, Drupal, debounce) { /** * @name Drupal.displace.offsets * * @type {Drupal~displaceOffset} */ let offsets = { top: 0, right: 0, bottom: 0, left: 0, }; /** * Calculates displacement for element based on its dimensions and placement. * * @param {HTMLElement} el * The jQuery element whose dimensions and placement will be measured. * * @param {string} edge * The name of the edge of the viewport that the element is associated * with. * * @return {number} * The viewport displacement distance for the requested edge. */ function getRawOffset(el, edge) { const $el = $(el); const documentElement = document.documentElement; let displacement = 0; const horizontal = edge === 'left' || edge === 'right'; // Get the offset of the element itself. let placement = $el.offset()[horizontal ? 'left' : 'top']; // Subtract scroll distance from placement to get the distance // to the edge of the viewport. placement -= window[`scroll${horizontal ? 'X' : 'Y'}`] || document.documentElement[`scroll${horizontal ? 'Left' : 'Top'}`] || 0; // Find the displacement value according to the edge. switch (edge) { // Left and top elements displace as a sum of their own offset value // plus their size. case 'top': // Total displacement is the sum of the elements placement and size. displacement = placement + $el.outerHeight(); break; case 'left': // Total displacement is the sum of the elements placement and size. displacement = placement + $el.outerWidth(); break; // Right and bottom elements displace according to their left and // top offset. Their size isn't important. case 'bottom': displacement = documentElement.clientHeight - placement; break; case 'right': displacement = documentElement.clientWidth - placement; break; default: displacement = 0; } return displacement; } /** * Gets a specific edge's offset. * * Any element with the attribute data-offset-{edge} e.g. data-offset-top will * be considered in the viewport offset calculations. If the attribute has a * numeric value, that value will be used. If no value is provided, one will * be calculated using the element's dimensions and placement. * * @function Drupal.displace.calculateOffset * * @param {string} edge * The name of the edge to calculate. Can be 'top', 'right', * 'bottom' or 'left'. * * @return {number} * The viewport displacement distance for the requested edge. */ function calculateOffset(edge) { let edgeOffset = 0; const displacingElements = document.querySelectorAll( `[data-offset-${edge}]`, ); const n = displacingElements.length; for (let i = 0; i < n; i++) { const el = displacingElements[i]; // If the element is not visible, do consider its dimensions. if (el.style.display === 'none') { continue; } // If the offset data attribute contains a displacing value, use it. let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10); // If the element's offset data attribute exits // but is not a valid number then get the displacement // dimensions directly from the element. // eslint-disable-next-line no-restricted-globals if (isNaN(displacement)) { displacement = getRawOffset(el, edge); } // If the displacement value is larger than the current value for this // edge, use the displacement value. edgeOffset = Math.max(edgeOffset, displacement); } return edgeOffset; } /** * Determines the viewport offsets. * * @return {Drupal~displaceOffset} * An object whose keys are the for sides an element -- top, right, bottom * and left. The value of each key is the viewport displacement distance for * that edge. */ function calculateOffsets() { return { top: calculateOffset('top'), right: calculateOffset('right'), bottom: calculateOffset('bottom'), left: calculateOffset('left'), }; } /** * Informs listeners of the current offset dimensions. * * @function Drupal.displace * * @prop {Drupal~displaceOffset} offsets * * @param {bool} [broadcast] * When true or undefined, causes the recalculated offsets values to be * broadcast to listeners. * * @return {Drupal~displaceOffset} * An object whose keys are the for sides an element -- top, right, bottom * and left. The value of each key is the viewport displacement distance for * that edge. * * @fires event:drupalViewportOffsetChange */ function displace(broadcast) { offsets = calculateOffsets(); Drupal.displace.offsets = offsets; if (typeof broadcast === 'undefined' || broadcast) { $(document).trigger('drupalViewportOffsetChange', offsets); } return offsets; } /** * Registers a resize handler on the window. * * @type {Drupal~behavior} */ Drupal.behaviors.drupalDisplace = { attach() { // Mark this behavior as processed on the first pass. if (this.displaceProcessed) { return; } this.displaceProcessed = true; $(window).on('resize.drupalDisplace', debounce(displace, 200)); }, }; /** * Assign the displace function to a property of the Drupal global object. * * @ignore */ Drupal.displace = displace; $.extend(Drupal.displace, { /** * Expose offsets to other scripts to avoid having to recalculate offsets. * * @ignore */ offsets, /** * Expose method to compute a single edge offsets. * * @ignore */ calculateOffset, }); })(jQuery, Drupal, Drupal.debounce);