/** * @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) { /** * * @type {Drupal~displaceOffset} */ const cache = { right: 0, left: 0, bottom: 0, top: 0, }; /** * The prefix used for the css custom variable name. * * @type {string} */ const cssVarPrefix = '--drupal-displace-offset'; const documentStyle = document.documentElement.style; const offsetKeys = Object.keys(cache); /** * The object with accessors that update the CSS variable on value update. * * @type {Drupal~displaceOffset} */ const offsetProps = {}; offsetKeys.forEach((edge) => { offsetProps[edge] = { // Show this property when using Object.keys(). enumerable: true, get() { return cache[edge]; }, set(value) { // Only update the CSS custom variable when the value changed. if (value !== cache[edge]) { documentStyle.setProperty(`${cssVarPrefix}-${edge}`, `${value}px`); } cache[edge] = value; }, }; }); /** * Current value of the size of margins on the page. * * This property is read-only and the object is sealed to prevent key name * modifications since key names are used to dynamically construct CSS custom * variable names. * * @name Drupal.displace.offsets * * @type {Drupal~displaceOffset} */ const offsets = Object.seal(Object.defineProperties({}, offsetProps)); /** * Calculates displacement for element based on its dimensions and placement. * * @param {HTMLElement} el * The 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; } /** * Informs listeners of the current offset dimensions. * * Corresponding CSS custom variables are also updated. * Corresponding CSS custom variables names are: * - `--drupal-displace-offset-top` * - `--drupal-displace-offset-right` * - `--drupal-displace-offset-bottom` * - `--drupal-displace-offset-left` * * @function Drupal.displace * * @prop {Drupal~displaceOffset} offsets * * @param {boolean} [broadcast=true] * When true, causes the recalculated offsets values to be * broadcast to listeners. If none is given, defaults to true. * * @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 = true) { const newOffsets = {}; // Getting the offset and setting the offset needs to be separated because // of performance concerns. Only do DOM/style reading happening here. offsetKeys.forEach((edge) => { newOffsets[edge] = calculateOffset(edge); }); // Once we have all the values, write to the DOM/style. offsetKeys.forEach((edge) => { // Updating the value in place also update Drupal.displace.offsets. offsets[edge] = newOffsets[edge]; }); if (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; /** * Expose offsets to other scripts to avoid having to recalculate offsets. * * @ignore */ Object.defineProperty(Drupal.displace, 'offsets', { value: offsets, // Make sure other scripts don't replace this object. writable: false, }); /** * Expose method to compute a single edge offsets. * * @ignore */ Drupal.displace.calculateOffset = calculateOffset; })(jQuery, Drupal, Drupal.debounce);