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