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