@gitlab/ui
Version:
GitLab UI Components
299 lines (257 loc) • 11.2 kB
JavaScript
import { WINDOW, DOCUMENT } from '../constants/env';
import { Element } from '../constants/safe-types';
import { from } from './array';
import { isNull, isFunction } from './inspect';
import { toFloat } from './number';
import { toString } from './string';
// --- Constants ---
const ELEMENT_PROTO = Element.prototype;
const TABABLE_SELECTOR = ['button', '[href]:not(.disabled)', 'input', 'select', 'textarea', '[tabindex]', '[contenteditable]'].map(s => `${s}:not(:disabled):not([disabled])`).join(', ');
// --- Normalization utils ---
// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/matches#Polyfill
/* istanbul ignore next */
const matchesEl = ELEMENT_PROTO.matches || ELEMENT_PROTO.msMatchesSelector || ELEMENT_PROTO.webkitMatchesSelector;
// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
/* istanbul ignore next */
const closestEl = ELEMENT_PROTO.closest || function (sel) {
let el = this;
do {
// Use our "patched" matches function
if (matches(el, sel)) {
return el;
}
el = el.parentElement || el.parentNode;
} while (!isNull(el) && el.nodeType === Node.ELEMENT_NODE);
return null;
};
// `requestAnimationFrame()` convenience method
/* istanbul ignore next: JSDOM always returns the first option */
const requestAF = (WINDOW.requestAnimationFrame || WINDOW.webkitRequestAnimationFrame || WINDOW.mozRequestAnimationFrame || WINDOW.msRequestAnimationFrame || WINDOW.oRequestAnimationFrame || (
// Fallback, but not a true polyfill
// Only needed for Opera Mini
/* istanbul ignore next */
cb => setTimeout(cb, 16))).bind(WINDOW);
const MutationObs = WINDOW.MutationObserver || WINDOW.WebKitMutationObserver || WINDOW.MozMutationObserver || null;
// --- Utils ---
// Remove a node from DOM
const removeNode = el => el && el.parentNode && el.parentNode.removeChild(el);
// Determine if an element is an HTML element
const isElement = el => !!(el && el.nodeType === Node.ELEMENT_NODE);
// Get the currently active HTML element
const getActiveElement = function () {
let excludes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
const {
activeElement
} = DOCUMENT;
return activeElement && !excludes.some(el => el === activeElement) ? activeElement : null;
};
// Returns `true` if a tag's name equals `name`
const isTag = (tag, name) => toString(tag).toLowerCase() === toString(name).toLowerCase();
// Determine if an HTML element is the currently active element
const isActiveElement = el => isElement(el) && el === getActiveElement();
// Determine if an HTML element is visible - Faster than CSS check
const isVisible = el => {
if (!isElement(el) || !el.parentNode || !contains(DOCUMENT.body, el)) {
// Note this can fail for shadow dom elements since they
// are not a direct descendant of document.body
return false;
}
if (getStyle(el, 'display') === 'none') {
// We do this check to help with vue-test-utils when using v-show
/* istanbul ignore next */
return false;
}
// All browsers support getBoundingClientRect(), except JSDOM as it returns all 0's for values :(
// So any tests that need isVisible will fail in JSDOM
// Except when we override the getBCR prototype in some tests
const bcr = getBCR(el);
return !!(bcr && bcr.height > 0 && bcr.width > 0);
};
// Determine if an element is disabled
const isDisabled = el => !isElement(el) || el.disabled || hasAttr(el, 'disabled') || hasClass(el, 'disabled');
// Cause/wait-for an element to reflow its content (adjusting its height/width)
const reflow = el => {
// Requesting an elements offsetHight will trigger a reflow of the element content
/* istanbul ignore next: reflow doesn't happen in JSDOM */
return isElement(el) && el.offsetHeight;
};
// Select all elements matching selector. Returns `[]` if none found
const selectAll = (selector, root) => from((isElement(root) ? root : DOCUMENT).querySelectorAll(selector));
// Select a single element, returns `null` if not found
const select = (selector, root) => (isElement(root) ? root : DOCUMENT).querySelector(selector) || null;
// Determine if an element matches a selector
const matches = (el, selector) => isElement(el) ? matchesEl.call(el, selector) : false;
// Finds closest element matching selector. Returns `null` if not found
const closest = function (selector, root) {
let includeRoot = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
if (!isElement(root)) {
return null;
}
const el = closestEl.call(root, selector);
// Native closest behaviour when `includeRoot` is truthy,
// else emulate jQuery closest and return `null` if match is
// the passed in root element when `includeRoot` is falsey
return includeRoot ? el : el === root ? null : el;
};
// Returns true if the parent element contains the child element
const contains = (parent, child) => parent && isFunction(parent.contains) ? parent.contains(child) : false;
// Get an element given an ID
const getById = id => DOCUMENT.getElementById(/^#/.test(id) ? id.slice(1) : id) || null;
// Add a class to an element
const addClass = (el, className) => {
// We are checking for `el.classList` existence here since IE 11
// returns `undefined` for some elements (e.g. SVG elements)
// See https://github.com/bootstrap-vue/bootstrap-vue/issues/2713
if (className && isElement(el) && el.classList) {
el.classList.add(className);
}
};
// Remove a class from an element
const removeClass = (el, className) => {
// We are checking for `el.classList` existence here since IE 11
// returns `undefined` for some elements (e.g. SVG elements)
// See https://github.com/bootstrap-vue/bootstrap-vue/issues/2713
if (className && isElement(el) && el.classList) {
el.classList.remove(className);
}
};
// Test if an element has a class
const hasClass = (el, className) => {
// We are checking for `el.classList` existence here since IE 11
// returns `undefined` for some elements (e.g. SVG elements)
// See https://github.com/bootstrap-vue/bootstrap-vue/issues/2713
if (className && isElement(el) && el.classList) {
return el.classList.contains(className);
}
return false;
};
// Set an attribute on an element
const setAttr = (el, attr, value) => {
if (attr && isElement(el)) {
el.setAttribute(attr, value);
}
};
// Remove an attribute from an element
const removeAttr = (el, attr) => {
if (attr && isElement(el)) {
el.removeAttribute(attr);
}
};
// Get an attribute value from an element
// Returns `null` if not found
const getAttr = (el, attr) => attr && isElement(el) ? el.getAttribute(attr) : null;
// Determine if an attribute exists on an element
// Returns `true` or `false`, or `null` if element not found
const hasAttr = (el, attr) => attr && isElement(el) ? el.hasAttribute(attr) : null;
// Set an style property on an element
const setStyle = (el, prop, value) => {
if (prop && isElement(el)) {
el.style[prop] = value;
}
};
// Remove an style property from an element
const removeStyle = (el, prop) => {
if (prop && isElement(el)) {
el.style[prop] = '';
}
};
// Get an style property value from an element
// Returns `null` if not found
const getStyle = (el, prop) => prop && isElement(el) ? el.style[prop] || null : null;
// Return the Bounding Client Rect of an element
// Returns `null` if not an element
/* istanbul ignore next: getBoundingClientRect() doesn't work in JSDOM */
const getBCR = el => isElement(el) ? el.getBoundingClientRect() : null;
// Get computed style object for an element
/* istanbul ignore next: getComputedStyle() doesn't work in JSDOM */
const getCS = el => {
const {
getComputedStyle
} = WINDOW;
return getComputedStyle && isElement(el) ? getComputedStyle(el) : {};
};
// Returns a `Selection` object representing the range of text selected
// Returns `null` if no window support is given
/* istanbul ignore next: getSelection() doesn't work in JSDOM */
const getSel = () => {
const {
getSelection
} = WINDOW;
return getSelection ? WINDOW.getSelection() : null;
};
// Return an element's offset with respect to document element
// https://j11y.io/jquery/#v=git&fn=jQuery.fn.offset
const offset = el => /* istanbul ignore next: getBoundingClientRect(), getClientRects() doesn't work in JSDOM */{
const _offset = {
top: 0,
left: 0
};
if (!isElement(el) || el.getClientRects().length === 0) {
return _offset;
}
const bcr = getBCR(el);
if (bcr) {
const win = el.ownerDocument.defaultView;
_offset.top = bcr.top + win.pageYOffset;
_offset.left = bcr.left + win.pageXOffset;
}
return _offset;
};
// Return an element's offset with respect to to its offsetParent
// https://j11y.io/jquery/#v=git&fn=jQuery.fn.position
const position = el => /* istanbul ignore next: getBoundingClientRect() doesn't work in JSDOM */{
let _offset = {
top: 0,
left: 0
};
if (!isElement(el)) {
return _offset;
}
let parentOffset = {
top: 0,
left: 0
};
const elStyles = getCS(el);
if (elStyles.position === 'fixed') {
_offset = getBCR(el) || _offset;
} else {
_offset = offset(el);
const doc = el.ownerDocument;
let offsetParent = el.offsetParent || doc.documentElement;
while (offsetParent && (offsetParent === doc.body || offsetParent === doc.documentElement) && getCS(offsetParent).position === 'static') {
offsetParent = offsetParent.parentNode;
}
if (offsetParent && offsetParent !== el && offsetParent.nodeType === Node.ELEMENT_NODE) {
parentOffset = offset(offsetParent);
const offsetParentStyles = getCS(offsetParent);
parentOffset.top += toFloat(offsetParentStyles.borderTopWidth, 0);
parentOffset.left += toFloat(offsetParentStyles.borderLeftWidth, 0);
}
}
return {
top: _offset.top - parentOffset.top - toFloat(elStyles.marginTop, 0),
left: _offset.left - parentOffset.left - toFloat(elStyles.marginLeft, 0)
};
};
// Find all tabable elements in the given element
// Assumes users have not used `tabindex` > `0` on elements
const getTabables = function () {
let rootEl = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;
return selectAll(TABABLE_SELECTOR, rootEl).filter(isVisible).filter(el => el.tabIndex > -1 && !el.disabled);
};
// Attempt to focus an element, and return `true` if successful
const attemptFocus = function (el) {
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
try {
el.focus(options);
} catch {}
return isActiveElement(el);
};
// Attempt to blur an element, and return `true` if successful
const attemptBlur = el => {
try {
el.blur();
} catch {}
return !isActiveElement(el);
};
export { MutationObs, addClass, attemptBlur, attemptFocus, closest, closestEl, contains, getActiveElement, getAttr, getBCR, getById, getCS, getSel, getStyle, getTabables, hasAttr, hasClass, isActiveElement, isDisabled, isElement, isTag, isVisible, matches, matchesEl, offset, position, reflow, removeAttr, removeClass, removeNode, removeStyle, requestAF, select, selectAll, setAttr, setStyle };