3 * Manages elements that can offset the size of the viewport.
5 * Measures and reports viewport offset dimensions from elements like the
6 * toolbar that can potentially displace the positioning of other elements.
10 * @typedef {object} Drupal~displaceOffset
14 * @prop {number} right
15 * @prop {number} bottom
19 * Triggers when layout of the page changes.
21 * This is used to position fixed element on the page during page resize and
24 * @event drupalViewportOffsetChange
27 (function($, Drupal, debounce) {
29 * @name Drupal.displace.offsets
31 * @type {Drupal~displaceOffset}
41 * Calculates displacement for element based on its dimensions and placement.
43 * @param {HTMLElement} el
44 * The jQuery element whose dimensions and placement will be measured.
46 * @param {string} edge
47 * The name of the edge of the viewport that the element is associated
51 * The viewport displacement distance for the requested edge.
53 function getRawOffset(el, edge) {
55 const documentElement = document.documentElement;
57 const horizontal = edge === 'left' || edge === 'right';
58 // Get the offset of the element itself.
59 let placement = $el.offset()[horizontal ? 'left' : 'top'];
60 // Subtract scroll distance from placement to get the distance
61 // to the edge of the viewport.
63 window[`scroll${horizontal ? 'X' : 'Y'}`] ||
64 document.documentElement[`scroll${horizontal ? 'Left' : 'Top'}`] ||
66 // Find the displacement value according to the edge.
68 // Left and top elements displace as a sum of their own offset value
71 // Total displacement is the sum of the elements placement and size.
72 displacement = placement + $el.outerHeight();
76 // Total displacement is the sum of the elements placement and size.
77 displacement = placement + $el.outerWidth();
80 // Right and bottom elements displace according to their left and
81 // top offset. Their size isn't important.
83 displacement = documentElement.clientHeight - placement;
87 displacement = documentElement.clientWidth - placement;
97 * Gets a specific edge's offset.
99 * Any element with the attribute data-offset-{edge} e.g. data-offset-top will
100 * be considered in the viewport offset calculations. If the attribute has a
101 * numeric value, that value will be used. If no value is provided, one will
102 * be calculated using the element's dimensions and placement.
104 * @function Drupal.displace.calculateOffset
106 * @param {string} edge
107 * The name of the edge to calculate. Can be 'top', 'right',
108 * 'bottom' or 'left'.
111 * The viewport displacement distance for the requested edge.
113 function calculateOffset(edge) {
115 const displacingElements = document.querySelectorAll(
116 `[data-offset-${edge}]`,
118 const n = displacingElements.length;
119 for (let i = 0; i < n; i++) {
120 const el = displacingElements[i];
121 // If the element is not visible, do consider its dimensions.
122 if (el.style.display === 'none') {
125 // If the offset data attribute contains a displacing value, use it.
126 let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10);
127 // If the element's offset data attribute exits
128 // but is not a valid number then get the displacement
129 // dimensions directly from the element.
130 // eslint-disable-next-line no-restricted-globals
131 if (isNaN(displacement)) {
132 displacement = getRawOffset(el, edge);
134 // If the displacement value is larger than the current value for this
135 // edge, use the displacement value.
136 edgeOffset = Math.max(edgeOffset, displacement);
143 * Determines the viewport offsets.
145 * @return {Drupal~displaceOffset}
146 * An object whose keys are the for sides an element -- top, right, bottom
147 * and left. The value of each key is the viewport displacement distance for
150 function calculateOffsets() {
152 top: calculateOffset('top'),
153 right: calculateOffset('right'),
154 bottom: calculateOffset('bottom'),
155 left: calculateOffset('left'),
160 * Informs listeners of the current offset dimensions.
162 * @function Drupal.displace
164 * @prop {Drupal~displaceOffset} offsets
166 * @param {bool} [broadcast]
167 * When true or undefined, causes the recalculated offsets values to be
168 * broadcast to listeners.
170 * @return {Drupal~displaceOffset}
171 * An object whose keys are the for sides an element -- top, right, bottom
172 * and left. The value of each key is the viewport displacement distance for
175 * @fires event:drupalViewportOffsetChange
177 function displace(broadcast) {
178 offsets = calculateOffsets();
179 Drupal.displace.offsets = offsets;
180 if (typeof broadcast === 'undefined' || broadcast) {
181 $(document).trigger('drupalViewportOffsetChange', offsets);
187 * Registers a resize handler on the window.
189 * @type {Drupal~behavior}
191 Drupal.behaviors.drupalDisplace = {
193 // Mark this behavior as processed on the first pass.
194 if (this.displaceProcessed) {
197 this.displaceProcessed = true;
199 $(window).on('resize.drupalDisplace', debounce(displace, 200));
204 * Assign the displace function to a property of the Drupal global object.
208 Drupal.displace = displace;
209 $.extend(Drupal.displace, {
211 * Expose offsets to other scripts to avoid having to recalculate offsets.
218 * Expose method to compute a single edge offsets.
224 })(jQuery, Drupal, Drupal.debounce);