blob: 3f63f9072d8d735f3be51ffbc7259598f14571e8 [file] [log] [blame]
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.tether = exports.removeChildElements = exports.moveElements = exports.isRectInsideWindowViewport = exports.isFocusable = exports.getScrollParents = exports.getParentElements = exports.getWindowViewport = undefined;
var _isNan = require('babel-runtime/core-js/number/is-nan');
var _isNan2 = _interopRequireDefault(_isNan);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Remove child element(s)
* element.innerHTNL = '' has a performance penality!
* @see http://jsperf.com/empty-an-element/16
* @see http://jsperf.com/force-reflow
* @param element
* @param forceReflow
*/
var removeChildElements = function removeChildElements(element) {
var forceReflow = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true;
// See: http://jsperf.com/empty-an-element/16
while (element.lastChild) {
element.removeChild(element.lastChild);
}
if (forceReflow) {
// See: http://jsperf.com/force-reflow
var d = element.style.display;
element.style.display = 'none';
element.style.display = d;
}
};
/**
* Moves child elements from a DOM node to another dom node.
* @param source {HTMLElement}
* @param target {HTMLElement} If the target parameter is ommited, a document fragment is created
* @return {HTMLElement} The target node
*
* @example
* // Moves child elements from a DOM node to another dom node.
* moveElements(source, destination);
*
* @example
* // If the second parameter is ommited, a document fragment is created:
* let fragment = moveElements(source);
*
* @See: https://github.com/webmodules/dom-move
*/
var moveElements = function moveElements(source, target) {
if (!target) {
target = source.ownerDocument.createDocumentFragment();
}
while (source.firstChild) {
target.appendChild(source.firstChild);
}
return target;
};
/**
* Get the browser viewport dimensions
* @see http://stackoverflow.com/questions/1248081/get-the-browser-viewport-dimensions-with-javascript
* @return {{windowWidth: number, windowHeight: number}}
*/
var getWindowViewport = function getWindowViewport() {
return {
viewportWidth: Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0),
viewportHeight: Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0)
};
};
/**
* Check whether an element is in the window viewport
* @see http://stackoverflow.com/questions/123999/how-to-tell-if-a-dom-element-is-visible-in-the-current-viewport/
* @param top
* @param left
* @param bottom
* @param right
* @return {boolean} true if rectangle is inside window viewport, otherwise false
*/
var isRectInsideWindowViewport = function isRectInsideWindowViewport(_ref) {
var top = _ref.top,
left = _ref.left,
bottom = _ref.bottom,
right = _ref.right;
var _getWindowViewport = getWindowViewport(),
viewportWidth = _getWindowViewport.viewportWidth,
viewportHeight = _getWindowViewport.viewportHeight;
return top >= 0 && left >= 0 && bottom <= viewportHeight && right <= viewportWidth;
};
/**
* Get a list of parent elements that can possibly scroll
* @param el the element to get parents for
* @returns {Array}
*/
var getScrollParents = function getScrollParents(el) {
var elements = [];
/*
for (el = el.parentNode; el; el = el.parentNode) {
const cs = window.getComputedStyle(el);
if(!(cs.overflowY === 'hidden' && cs.overflowX === 'hidden')) {
elements.unshift(el);
}
if(el === document.body) {
break;
}
}
*/
var element = el.parentNode;
while (element) {
var cs = window.getComputedStyle(element);
if (!(cs.overflowY === 'hidden' && cs.overflowX === 'hidden')) {
elements.unshift(element);
}
if (element === document.body) {
break;
}
element = element.parentNode;
}
return elements;
};
/**
* Get a list of parent elements, from a given element to a given element
* @param {HTMLElement} from
* @param {HTMLElement} to
* @return {Array<HTMLElement>} the parent elements, not including from and to
*/
var getParentElements = function getParentElements(from, to) {
var result = [];
var element = from.parentNode;
while (element) {
if (element === to) {
break;
}
result.unshift(element);
element = element.parentNode;
}
return result;
};
/**
* Position element next to button
*
* Positioning strategy
* 1. element.height > viewport.height
* let element.height = viewport.heigt
* let element.overflow-y = auto
* 2. element.width > viewport.width
* let element.width = viewport.width
* 3. position element below button, align left edge of element with button left
* done if element inside viewport
* 4. position element below button, align right edge of element with button right
* done if element inside viewport
* 5. positions element above button, aligns left edge of element with button left
* done if element inside viewport
* 6. position element above the control element, aligned to its right.
* done if element inside viewport
* 7. position element at button right hand side, aligns element top with button top
* done if element inside viewport
* 8. position element at button left hand side, aligns element top with button top
* done if element inside viewport
* 9. position element inside viewport
* 1. position element at viewport bottom
* 2. position element at button right hand side
* done if element inside viewport
* 3. position element at button left hand side
* done if element inside viewport
* 4. position element at viewport right
* 10. done
*
*/
var tether = function tether(controlledBy, element) {
var controlRect = controlledBy.getBoundingClientRect();
// 1. will element height fit inside window viewport?
var _getWindowViewport2 = getWindowViewport(),
viewportWidth = _getWindowViewport2.viewportWidth,
viewportHeight = _getWindowViewport2.viewportHeight;
element.style.height = 'auto';
//element.style.overflowY = 'hidden';
if (element.offsetHeight > viewportHeight) {
element.style.height = viewportHeight + 'px';
element.style.overflowY = 'auto';
}
// 2. will element width fit inside window viewport?
element.style.width = 'auto';
if (element.offsetWidth > viewportWidth) {
element.style.width = viewportWidth + 'px';
}
var elementRect = element.getBoundingClientRect();
// element to control distance
var dy = controlRect.top - elementRect.top;
var dx = controlRect.left - elementRect.left;
// element rect, window coordinates relative to top,left of control
var top = elementRect.top + dy;
var left = elementRect.left + dx;
var bottom = top + elementRect.height;
var right = left + elementRect.width;
// Position relative to control
var ddy = dy;
var ddx = dx;
if (isRectInsideWindowViewport({
top: top + controlRect.height,
left: left,
bottom: bottom + controlRect.height,
right: right
})) {
// 3 position element below the control element, aligned to its left
ddy = controlRect.height + dy;
//console.log('***** 3');
} else if (isRectInsideWindowViewport({
top: top + controlRect.height,
left: left + controlRect.width - elementRect.width,
bottom: bottom + controlRect.height,
right: left + controlRect.width
})) {
// 4 position element below the control element, aligned to its right
ddy = controlRect.height + dy;
ddx = dx + controlRect.width - elementRect.width;
//console.log('***** 4');
} else if (isRectInsideWindowViewport({
top: top - elementRect.height,
left: left,
bottom: bottom - elementRect.height,
right: right
})) {
// 5. position element above the control element, aligned to its left.
ddy = dy - elementRect.height;
//console.log('***** 5');
} else if (isRectInsideWindowViewport({
top: top - elementRect.height,
left: left + controlRect.width - elementRect.width,
bottom: bottom - elementRect.height,
right: left + controlRect.width
})) {
// 6. position element above the control element, aligned to its right.
ddy = dy - elementRect.height;
ddx = dx + controlRect.width - elementRect.width;
//console.log('***** 6');
} else if (isRectInsideWindowViewport({
top: top,
left: left + controlRect.width,
bottom: bottom,
right: right + controlRect.width
})) {
// 7. position element at button right hand side
ddx = controlRect.width + dx;
//console.log('***** 7');
} else if (isRectInsideWindowViewport({
top: top,
left: left - controlRect.width,
bottom: bottom,
right: right - controlRect.width
})) {
// 8. position element at button left hand side
ddx = dx - elementRect.width;
//console.log('***** 8');
} else {
// 9. position element inside viewport, near controlrect if possible
//console.log('***** 9');
// 9.1 position element near controlrect bottom
ddy = dy - bottom + viewportHeight;
if (top + controlRect.height >= 0 && bottom + controlRect.height <= viewportHeight) {
ddy = controlRect.height + dy;
} else if (top - elementRect.height >= 0 && bottom - elementRect.height <= viewportHeight) {
ddy = dy - elementRect.height;
}
if (left + elementRect.width + controlRect.width <= viewportWidth) {
// 9.2 Position element at button right hand side
ddx = controlRect.width + dx;
//console.log('***** 9.2');
} else if (left - elementRect.width >= 0) {
// 9.3 Position element at button left hand side
ddx = dx - elementRect.width;
//console.log('***** 9.3');
} else {
// 9.4 position element at (near) viewport right
var r = left + elementRect.width - viewportWidth;
ddx = dx - r;
//console.log('***** 9.4');
}
}
// 10. done
element.style.top = element.offsetTop + ddy + 'px';
element.style.left = element.offsetLeft + ddx + 'px';
//console.log('***** 10. done');
};
/**
* Check if the given element can receive focus
* @param {HTMLElement} element the element to check
* @return {boolean} true if the element is focusable, otherwise false
*/
var isFocusable = function isFocusable(element) {
// https://github.com/stephenmathieson/is-focusable/blob/master/index.js
// http://stackoverflow.com/questions/1599660/which-html-elements-can-receive-focus
if (element.hasAttribute('tabindex')) {
var tabindex = element.getAttribute('tabindex');
if (!(0, _isNan2.default)(tabindex)) {
return parseInt(tabindex) > -1;
}
}
if (element.hasAttribute('contenteditable') && element.getAttribute('contenteditable') !== 'false') {
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes#attr-contenteditable
return true;
}
// natively focusable, but only when enabled
var selector = /input|select|textarea|button|details/i;
var name = element.nodeName;
if (selector.test(name)) {
return element.type.toLowerCase() !== 'hidden' && !element.disabled;
}
// anchors and area must have an href
if (name === 'A' || name === 'AREA') {
return !!element.href;
}
if (name === 'IFRAME') {
// Check visible iframe
var cs = window.getComputedStyle(element);
return cs.getPropertyValue('display').toLowerCase() !== 'none';
}
return false;
};
/**
* Get a list of offset parents for given element
* @see https://www.benpickles.com/articles/51-finding-a-dom-nodes-common-ancestor-using-javascript
* @param el the element
* @return {Array} a list of offset parents
*/
/*
const offsetParents = (el) => {
const elements = [];
for (; el; el = el.offsetParent) {
elements.unshift(el);
}
if(!elements.find(e => e === document.body)) {
elements.unshift(document.body);
}
return elements;
};
*/
/**
* Finds the common offset ancestor of two DOM nodes
* @see https://www.benpickles.com/articles/51-finding-a-dom-nodes-common-ancestor-using-javascript
* @see https://gist.github.com/benpickles/4059636
* @param a
* @param b
* @return {Element} The common offset ancestor of a and b
*/
/*
const commonOffsetAncestor = (a, b) => {
const parentsA = offsetParents(a);
const parentsB = offsetParents(b);
for (let i = 0; i < parentsA.length; i++) {
if (parentsA[i] !== parentsB[i]) return parentsA[i-1];
}
};
*/
/**
* Calculate position relative to a target element
* @see http://stackoverflow.com/questions/21064101/understanding-offsetwidth-clientwidth-scrollwidth-and-height-respectively
* @param target
* @param el
* @return {{top: number, left: number}}
*/
/*
const calcPositionRelativeToTarget = (target, el) => {
let top = 0;
let left = 0;
while(el) {
top += (el.offsetTop - el.scrollTop + el.clientTop) || 0;
left += (el.offsetLeft - el.scrollLeft + el.clientLeft) || 0;
el = el.offsetParent;
if(el === target) {
break;
}
}
return { top: top, left: left };
};
*/
exports.getWindowViewport = getWindowViewport;
exports.getParentElements = getParentElements;
exports.getScrollParents = getScrollParents;
exports.isFocusable = isFocusable;
exports.isRectInsideWindowViewport = isRectInsideWindowViewport;
exports.moveElements = moveElements;
exports.removeChildElements = removeChildElements;
exports.tether = tether;