UNPKG

@gitlab/ui

Version:
299 lines (257 loc) • 11.2 kB
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 };