blob: 5aa265849c527b9f3e9e21820652623960ade1da [file] [log] [blame]
Copybara botbe50d492023-11-30 00:16:42 +01001/**
2 * Remove child element(s)
3 * element.innerHTNL = '' has a performance penality!
4 * @see http://jsperf.com/empty-an-element/16
5 * @see http://jsperf.com/force-reflow
6 * @param element
7 * @param forceReflow
8 */
9const removeChildElements = (element, forceReflow = true) => {
10
11 // See: http://jsperf.com/empty-an-element/16
12 while (element.lastChild) {
13 element.removeChild(element.lastChild);
14 }
15 if(forceReflow) {
16 // See: http://jsperf.com/force-reflow
17 const d = element.style.display;
18
19 element.style.display = 'none';
20 element.style.display = d;
21 }
22};
23
24/**
25 * Moves child elements from a DOM node to another dom node.
26 * @param source {HTMLElement}
27 * @param target {HTMLElement} If the target parameter is ommited, a document fragment is created
28 * @return {HTMLElement} The target node
29 *
30 * @example
31 * // Moves child elements from a DOM node to another dom node.
32 * moveElements(source, destination);
33 *
34 * @example
35 * // If the second parameter is ommited, a document fragment is created:
36 * let fragment = moveElements(source);
37 *
38 * @See: https://github.com/webmodules/dom-move
39 */
40const moveElements = (source, target) => {
41 if (!target) {
42 target = source.ownerDocument.createDocumentFragment();
43 }
44 while (source.firstChild) {
45 target.appendChild(source.firstChild);
46 }
47 return target;
48};
49
50
51/**
52 * Get the browser viewport dimensions
53 * @see http://stackoverflow.com/questions/1248081/get-the-browser-viewport-dimensions-with-javascript
54 * @return {{windowWidth: number, windowHeight: number}}
55 */
56const getWindowViewport = () => {
57 return {
58 viewportWidth: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
59 viewportHeight: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
60 };
61};
62
63
64/**
65 * Check whether an element is in the window viewport
66 * @see http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/
67 * @param top
68 * @param left
69 * @param bottom
70 * @param right
71 * @return {boolean} true if rectangle is inside window viewport, otherwise false
72 */
73const isRectInsideWindowViewport = ({ top, left, bottom, right }) => {
74 const { viewportWidth, viewportHeight } = getWindowViewport();
75 return top >= 0 &&
76 left >= 0 &&
77 bottom <= viewportHeight &&
78 right <= viewportWidth;
79};
80
81
82/**
83 * Get a list of parent elements that can possibly scroll
84 * @param el the element to get parents for
85 * @returns {Array}
86 */
87const getScrollParents = el => {
88 const elements = [];
89
90 /*
91 for (el = el.parentNode; el; el = el.parentNode) {
92 const cs = window.getComputedStyle(el);
93 if(!(cs.overflowY === 'hidden' && cs.overflowX === 'hidden')) {
94 elements.unshift(el);
95 }
96 if(el === document.body) {
97 break;
98 }
99 }
100 */
101
102 let element = el.parentNode;
103 while (element) {
104 const cs = window.getComputedStyle(element);
105 if(!(cs.overflowY === 'hidden' && cs.overflowX === 'hidden')) {
106 elements.unshift(element);
107 }
108 if(element === document.body) {
109 break;
110 }
111 element = element.parentNode;
112 }
113
114 return elements;
115};
116
117/**
118 * Get a list of parent elements, from a given element to a given element
119 * @param {HTMLElement} from
120 * @param {HTMLElement} to
121 * @return {Array<HTMLElement>} the parent elements, not including from and to
122 */
123const getParentElements = (from, to) => {
124 const result = [];
125 let element = from.parentNode;
126 while (element) {
127 if(element === to) {
128 break;
129 }
130 result.unshift(element);
131 element = element.parentNode;
132 }
133 return result;
134};
135
136/**
137 * Position element next to button
138 *
139 * Positioning strategy
140 * 1. element.height > viewport.height
141 * let element.height = viewport.heigt
142 * let element.overflow-y = auto
143 * 2. element.width > viewport.width
144 * let element.width = viewport.width
145 * 3. position element below button, align left edge of element with button left
146 * done if element inside viewport
147 * 4. position element below button, align right edge of element with button right
148 * done if element inside viewport
149 * 5. positions element above button, aligns left edge of element with button left
150 * done if element inside viewport
151 * 6. position element above the control element, aligned to its right.
152 * done if element inside viewport
153 * 7. position element at button right hand side, aligns element top with button top
154 * done if element inside viewport
155 * 8. position element at button left hand side, aligns element top with button top
156 * done if element inside viewport
157 * 9. position element inside viewport
158 * 1. position element at viewport bottom
159 * 2. position element at button right hand side
160 * done if element inside viewport
161 * 3. position element at button left hand side
162 * done if element inside viewport
163 * 4. position element at viewport right
164 * 10. done
165 *
166 */
167const tether = (controlledBy, element) => {
168 const controlRect = controlledBy.getBoundingClientRect();
169
170 // 1. will element height fit inside window viewport?
171 const { viewportWidth, viewportHeight } = getWindowViewport();
172
173 element.style.height = 'auto';
174 //element.style.overflowY = 'hidden';
175 if(element.offsetHeight > viewportHeight) {
176 element.style.height = `${viewportHeight}px`;
177 element.style.overflowY = 'auto';
178 }
179
180 // 2. will element width fit inside window viewport?
181 element.style.width = 'auto';
182 if(element.offsetWidth > viewportWidth) {
183 element.style.width = `${viewportWidth}px`;
184 }
185
186 const elementRect = element.getBoundingClientRect();
187
188 // element to control distance
189 const dy = controlRect.top - elementRect.top;
190 const dx = controlRect.left - elementRect.left;
191
192 // element rect, window coordinates relative to top,left of control
193 const top = elementRect.top + dy;
194 const left = elementRect.left + dx;
195 const bottom = top + elementRect.height;
196 const right = left + elementRect.width;
197
198 // Position relative to control
199 let ddy = dy;
200 let ddx = dx;
201
202 if(isRectInsideWindowViewport({
203 top: top + controlRect.height,
204 left: left,
205 bottom: bottom + controlRect.height,
206 right: right
207 })) {
208 // 3 position element below the control element, aligned to its left
209 ddy = controlRect.height + dy;
210 //console.log('***** 3');
211 }
212 else if(isRectInsideWindowViewport({
213 top: top + controlRect.height,
214 left: left + controlRect.width - elementRect.width,
215 bottom: bottom + controlRect.height,
216 right: left + controlRect.width
217 })) {
218 // 4 position element below the control element, aligned to its right
219 ddy = controlRect.height + dy;
220 ddx = dx + controlRect.width - elementRect.width;
221 //console.log('***** 4');
222 }
223 else if(isRectInsideWindowViewport({
224 top: top - elementRect.height,
225 left: left,
226 bottom: bottom - elementRect.height,
227 right: right
228 })) {
229 // 5. position element above the control element, aligned to its left.
230 ddy = dy - elementRect.height;
231 //console.log('***** 5');
232 }
233 else if(isRectInsideWindowViewport({
234 top: top - elementRect.height,
235 left: left + controlRect.width - elementRect.width,
236 bottom: bottom - elementRect.height,
237 right: left + controlRect.width
238 })) {
239 // 6. position element above the control element, aligned to its right.
240 ddy = dy - elementRect.height;
241 ddx = dx + controlRect.width - elementRect.width;
242 //console.log('***** 6');
243 }
244 else if(isRectInsideWindowViewport({
245 top: top,
246 left: left + controlRect.width,
247 bottom: bottom,
248 right: right + controlRect.width
249 })) {
250 // 7. position element at button right hand side
251 ddx = controlRect.width + dx;
252 //console.log('***** 7');
253 }
254 else if(isRectInsideWindowViewport({
255 top: top,
256 left: left - controlRect.width,
257 bottom: bottom,
258 right: right - controlRect.width
259 })) {
260 // 8. position element at button left hand side
261 ddx = dx - elementRect.width;
262 //console.log('***** 8');
263 }
264 else {
265 // 9. position element inside viewport, near controlrect if possible
266 //console.log('***** 9');
267
268 // 9.1 position element near controlrect bottom
269 ddy = dy - bottom + viewportHeight;
270 if(top + controlRect.height >= 0 && bottom + controlRect.height <= viewportHeight) {
271 ddy = controlRect.height + dy;
272 }
273 else if(top - elementRect.height >= 0 && bottom - elementRect.height <= viewportHeight) {
274 ddy = dy - elementRect.height;
275 }
276
277 if(left + elementRect.width + controlRect.width <= viewportWidth) {
278 // 9.2 Position element at button right hand side
279 ddx = controlRect.width + dx;
280 //console.log('***** 9.2');
281 }
282 else if(left - elementRect.width >= 0) {
283 // 9.3 Position element at button left hand side
284 ddx = dx - elementRect.width;
285 //console.log('***** 9.3');
286 }
287 else {
288 // 9.4 position element at (near) viewport right
289 const r = left + elementRect.width - viewportWidth;
290 ddx = dx - r;
291 //console.log('***** 9.4');
292 }
293 }
294
295 // 10. done
296 element.style.top = `${element.offsetTop + ddy}px`;
297 element.style.left = `${element.offsetLeft + ddx}px`;
298 //console.log('***** 10. done');
299};
300
301/**
302 * Check if the given element can receive focus
303 * @param {HTMLElement} element the element to check
304 * @return {boolean} true if the element is focusable, otherwise false
305 */
306const isFocusable = (element) => {
307 // https://github.com/stephenmathieson/is-focusable/blob/master/index.js
308 // http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
309
310 if (element.hasAttribute('tabindex')) {
311 const tabindex = element.getAttribute('tabindex');
312 if (!Number.isNaN(tabindex)) {
313 return parseInt(tabindex) > -1;
314 }
315 }
316
317 if (element.hasAttribute('contenteditable') &&
318 element.getAttribute('contenteditable') !== 'false') {
319 // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-contenteditable
320 return true;
321 }
322
323 // natively focusable, but only when enabled
324 const selector = /input|select|textarea|button|details/i;
325 const name = element.nodeName;
326 if (selector.test(name)) {
327 return element.type.toLowerCase() !== 'hidden' && !element.disabled;
328 }
329
330 // anchors and area must have an href
331 if (name === 'A' || name === 'AREA') {
332 return !!element.href;
333 }
334
335 if (name === 'IFRAME') {
336 // Check visible iframe
337 const cs = window.getComputedStyle(element);
338 return cs.getPropertyValue('display').toLowerCase() !== 'none';
339 }
340
341 return false;
342};
343
344
345/**
346 * Get a list of offset parents for given element
347 * @see https://www.benpickles.com/articles/51-finding-a-dom-nodes-common-ancestor-using-javascript
348 * @param el the element
349 * @return {Array} a list of offset parents
350 */
351/*
352const offsetParents = (el) => {
353 const elements = [];
354 for (; el; el = el.offsetParent) {
355 elements.unshift(el);
356 }
357 if(!elements.find(e => e === document.body)) {
358 elements.unshift(document.body);
359 }
360 return elements;
361};
362*/
363
364/**
365 * Finds the common offset ancestor of two DOM nodes
366 * @see https://www.benpickles.com/articles/51-finding-a-dom-nodes-common-ancestor-using-javascript
367 * @see https://gist.github.com/benpickles/4059636
368 * @param a
369 * @param b
370 * @return {Element} The common offset ancestor of a and b
371 */
372/*
373const commonOffsetAncestor = (a, b) => {
374 const parentsA = offsetParents(a);
375 const parentsB = offsetParents(b);
376
377 for (let i = 0; i < parentsA.length; i++) {
378 if (parentsA[i] !== parentsB[i]) return parentsA[i-1];
379 }
380};
381*/
382
383/**
384 * Calculate position relative to a target element
385 * @see http://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively
386 * @param target
387 * @param el
388 * @return {{top: number, left: number}}
389 */
390/*
391const calcPositionRelativeToTarget = (target, el) => {
392 let top = 0;
393 let left = 0;
394
395 while(el) {
396 top += (el.offsetTop - el.scrollTop + el.clientTop) || 0;
397 left += (el.offsetLeft - el.scrollLeft + el.clientLeft) || 0;
398 el = el.offsetParent;
399
400 if(el === target) {
401 break;
402 }
403 }
404 return { top: top, left: left };
405};
406*/
407
408export {
409 getWindowViewport,
410 getParentElements,
411 getScrollParents,
412 isFocusable,
413 isRectInsideWindowViewport,
414 moveElements,
415 removeChildElements,
416 tether,
417};
418