displace.es6.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. /**
  2. * @file
  3. * Manages elements that can offset the size of the viewport.
  4. *
  5. * Measures and reports viewport offset dimensions from elements like the
  6. * toolbar that can potentially displace the positioning of other elements.
  7. */
  8. /**
  9. * @typedef {object} Drupal~displaceOffset
  10. *
  11. * @prop {number} top
  12. * @prop {number} left
  13. * @prop {number} right
  14. * @prop {number} bottom
  15. */
  16. /**
  17. * Triggers when layout of the page changes.
  18. *
  19. * This is used to position fixed element on the page during page resize and
  20. * Toolbar toggling.
  21. *
  22. * @event drupalViewportOffsetChange
  23. */
  24. (function($, Drupal, debounce) {
  25. /**
  26. * @name Drupal.displace.offsets
  27. *
  28. * @type {Drupal~displaceOffset}
  29. */
  30. let offsets = {
  31. top: 0,
  32. right: 0,
  33. bottom: 0,
  34. left: 0,
  35. };
  36. /**
  37. * Calculates displacement for element based on its dimensions and placement.
  38. *
  39. * @param {HTMLElement} el
  40. * The jQuery element whose dimensions and placement will be measured.
  41. *
  42. * @param {string} edge
  43. * The name of the edge of the viewport that the element is associated
  44. * with.
  45. *
  46. * @return {number}
  47. * The viewport displacement distance for the requested edge.
  48. */
  49. function getRawOffset(el, edge) {
  50. const $el = $(el);
  51. const documentElement = document.documentElement;
  52. let displacement = 0;
  53. const horizontal = edge === 'left' || edge === 'right';
  54. // Get the offset of the element itself.
  55. let placement = $el.offset()[horizontal ? 'left' : 'top'];
  56. // Subtract scroll distance from placement to get the distance
  57. // to the edge of the viewport.
  58. placement -=
  59. window[`scroll${horizontal ? 'X' : 'Y'}`] ||
  60. document.documentElement[`scroll${horizontal ? 'Left' : 'Top'}`] ||
  61. 0;
  62. // Find the displacement value according to the edge.
  63. switch (edge) {
  64. // Left and top elements displace as a sum of their own offset value
  65. // plus their size.
  66. case 'top':
  67. // Total displacement is the sum of the elements placement and size.
  68. displacement = placement + $el.outerHeight();
  69. break;
  70. case 'left':
  71. // Total displacement is the sum of the elements placement and size.
  72. displacement = placement + $el.outerWidth();
  73. break;
  74. // Right and bottom elements displace according to their left and
  75. // top offset. Their size isn't important.
  76. case 'bottom':
  77. displacement = documentElement.clientHeight - placement;
  78. break;
  79. case 'right':
  80. displacement = documentElement.clientWidth - placement;
  81. break;
  82. default:
  83. displacement = 0;
  84. }
  85. return displacement;
  86. }
  87. /**
  88. * Gets a specific edge's offset.
  89. *
  90. * Any element with the attribute data-offset-{edge} e.g. data-offset-top will
  91. * be considered in the viewport offset calculations. If the attribute has a
  92. * numeric value, that value will be used. If no value is provided, one will
  93. * be calculated using the element's dimensions and placement.
  94. *
  95. * @function Drupal.displace.calculateOffset
  96. *
  97. * @param {string} edge
  98. * The name of the edge to calculate. Can be 'top', 'right',
  99. * 'bottom' or 'left'.
  100. *
  101. * @return {number}
  102. * The viewport displacement distance for the requested edge.
  103. */
  104. function calculateOffset(edge) {
  105. let edgeOffset = 0;
  106. const displacingElements = document.querySelectorAll(
  107. `[data-offset-${edge}]`,
  108. );
  109. const n = displacingElements.length;
  110. for (let i = 0; i < n; i++) {
  111. const el = displacingElements[i];
  112. // If the element is not visible, do consider its dimensions.
  113. if (el.style.display === 'none') {
  114. continue;
  115. }
  116. // If the offset data attribute contains a displacing value, use it.
  117. let displacement = parseInt(el.getAttribute(`data-offset-${edge}`), 10);
  118. // If the element's offset data attribute exits
  119. // but is not a valid number then get the displacement
  120. // dimensions directly from the element.
  121. // eslint-disable-next-line no-restricted-globals
  122. if (isNaN(displacement)) {
  123. displacement = getRawOffset(el, edge);
  124. }
  125. // If the displacement value is larger than the current value for this
  126. // edge, use the displacement value.
  127. edgeOffset = Math.max(edgeOffset, displacement);
  128. }
  129. return edgeOffset;
  130. }
  131. /**
  132. * Determines the viewport offsets.
  133. *
  134. * @return {Drupal~displaceOffset}
  135. * An object whose keys are the for sides an element -- top, right, bottom
  136. * and left. The value of each key is the viewport displacement distance for
  137. * that edge.
  138. */
  139. function calculateOffsets() {
  140. return {
  141. top: calculateOffset('top'),
  142. right: calculateOffset('right'),
  143. bottom: calculateOffset('bottom'),
  144. left: calculateOffset('left'),
  145. };
  146. }
  147. /**
  148. * Informs listeners of the current offset dimensions.
  149. *
  150. * @function Drupal.displace
  151. *
  152. * @prop {Drupal~displaceOffset} offsets
  153. *
  154. * @param {bool} [broadcast]
  155. * When true or undefined, causes the recalculated offsets values to be
  156. * broadcast to listeners.
  157. *
  158. * @return {Drupal~displaceOffset}
  159. * An object whose keys are the for sides an element -- top, right, bottom
  160. * and left. The value of each key is the viewport displacement distance for
  161. * that edge.
  162. *
  163. * @fires event:drupalViewportOffsetChange
  164. */
  165. function displace(broadcast) {
  166. offsets = calculateOffsets();
  167. Drupal.displace.offsets = offsets;
  168. if (typeof broadcast === 'undefined' || broadcast) {
  169. $(document).trigger('drupalViewportOffsetChange', offsets);
  170. }
  171. return offsets;
  172. }
  173. /**
  174. * Registers a resize handler on the window.
  175. *
  176. * @type {Drupal~behavior}
  177. */
  178. Drupal.behaviors.drupalDisplace = {
  179. attach() {
  180. // Mark this behavior as processed on the first pass.
  181. if (this.displaceProcessed) {
  182. return;
  183. }
  184. this.displaceProcessed = true;
  185. $(window).on('resize.drupalDisplace', debounce(displace, 200));
  186. },
  187. };
  188. /**
  189. * Assign the displace function to a property of the Drupal global object.
  190. *
  191. * @ignore
  192. */
  193. Drupal.displace = displace;
  194. $.extend(Drupal.displace, {
  195. /**
  196. * Expose offsets to other scripts to avoid having to recalculate offsets.
  197. *
  198. * @ignore
  199. */
  200. offsets,
  201. /**
  202. * Expose method to compute a single edge offsets.
  203. *
  204. * @ignore
  205. */
  206. calculateOffset,
  207. });
  208. })(jQuery, Drupal, Drupal.debounce);