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