You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
7.7 KiB
262 lines
7.7 KiB
/** |
|
* @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; |
|
document.querySelectorAll(`[data-offset-${edge}]`).forEach((el) => { |
|
// If the element is not visible, do consider its dimensions. |
|
if (el.style.display === 'none') { |
|
return; |
|
} |
|
// 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);
|
|
|