268 lines
7.9 KiB
JavaScript
268 lines
7.9 KiB
JavaScript
/**
|
|
* @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 {bool} [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);
|