Home Reference Source

lib/dom.js

import { camelCase } from './strings';
import { isPromise } from './general';

/**
 * Returns the results of evaluating the supplied
 * xpath query using an optional context `contextElement`, defaults
 * to document, as XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
 * @param {string} xpathQuery - The xpath query to be evaluated
 * @param {SomeElement|Document} [contextElement] - Optional
 * element to be used as the context of the evaluation
 * @return {XPathResult} - The results of the xpath query evaluation
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate
 */
export function xpathSnapShot(xpathQuery, contextElement) {
  return document.evaluate(
    xpathQuery,
    contextElement || document,
    null,
    XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
    null
  );
}

/**
 * Provides the same functionality of the chrome console utility `$x`
 * but likely less performant
 * @param {string} xpathQuery - The xpath query to be evaluated
 * @param {SomeElement|Document} [contextElement] - Optional
 * element to be used as the context of the evaluation
 * @return {Array<SomeElement>} - The results of the xpath query evaluation
 * @see https://developer.mozilla.org/en-US/docs/Web/API/Document/evaluate
 * @see https://developers.google.com/web/tools/chrome-devtools/console/utilities
 */
export function xpathSnapShotArray(xpathQuery, contextElement) {
  const snapShot = xpathSnapShot(xpathQuery, contextElement);
  const elements = [];
  const len = snapShot.snapshotLength;
  for (var i = 0; i < len; i++) {
    elements.push(snapShot.snapshotItem(i));
  }
  return elements;
}

/**
 * Ensures that if the value of the chrome console utility $x
 * is not the actual utility (jquery is on the page) the returned
 * function behaves exactly like it.
 * @param {function(...args: *): Array<SomeElement>} cliXPG
 * @return {function(...args: *): Array<SomeElement>}
 */
export function maybePolyfillXPG(cliXPG) {
  if (
    typeof cliXPG === 'function' &&
    cliXPG.toString().includes('[Command Line API]')
  ) {
    return cliXPG;
  }
  return xpathSnapShotArray;
}

/**
 * Utility function for `(document||element).querySelector(selector)`
 * @param {string} selector - the selector to be use
 * @param {SomeElement|Document} [context] - element to use rather than document for the querySelector call
 * @return {?SomeElement}
 */
export function qs(selector, context) {
  if (context != null) return context.querySelector(selector);
  return document.querySelector(selector);
}

/**
 * Utility function for `(document||element).querySelector(selector)`
 * @param {string} selector - the selector to be use
 * @param {function(elem: SomeElement): boolean} filterFn
 * @param {SomeElement|Document} [context] - element to use rather than document for the querySelector call
 * @return {?SomeElement}
 */
export function filteredQs(selector, filterFn, context) {
  const elem = qs(selector, context);
  if (elem == null) return null;
  if (elem && filterFn(elem)) return elem;
  return null;
}

/**
 * Utility function for `document.querySelectorAll(selector)`
 * @param {string} selector - the selector to be use
 * @param {SomeElement|Document} [context] - element to use rather than document for the querySelector call
 * @return {NodeList<SomeElement>}
 */
export function qsa(selector, context) {
  if (context != null) return context.querySelectorAll(selector);
  return document.querySelectorAll(selector);
}

/**
 * Utility function for `document.getElementById(id)`
 * @param {string} eid - The id of the element to get
 * @param {SomeElement|Document} [context] - Optional document element to use rather than
 * the current JS context's
 * @return {?SomeElement}
 */
export function id(eid, context) {
  if (context != null) return context.getElementById(eid);
  return document.getElementById(eid);
}

/**
 * Removes the element selected by the supplied querySelector, if it exits,
 * returning true to indicate the element was removed and false otherwise
 * @param {string} selector - the selector to be use
 * @param {SomeElement|Document} [context] - element to use rather than document for the querySelector call
 * @return {boolean}
 */
export function maybeRemoveElem(selector, context) {
  const elem = qs(selector, context);
  if (elem) {
    elem.remove();
    return true;
  }
  return false;
}

/**
 * Removes the element with the supplied id, if it exits, returning
 * true to indicate the element was removed and false otherwise
 * @param {string} eid - The id of the element to remove
 * @param {SomeElement|Document} [context] - Optional document element to use rather than
 * the current JS context's
 * @return {boolean}
 */
export function maybeRemoveElemById(eid, context) {
  const elem = id(eid, context);
  if (elem) {
    elem.remove();
    return true;
  }
  return false;
}

/**
 * Returns true if the supplied elements `offsetTop === 0`
 * @param {SomeElement} elem - The element to check its
 * offsetTop
 * @return {boolean}
 */
export function elemOffsetTopZero(elem) {
  return elem.offsetTop === 0;
}

/**
 * Marks the supplied element as visited by adding the marker
 * to its classList.
 * @param {SomeElement} elem - The element to mark
 * as visited
 * @param {string} [marker = 'wrvistited'] - Optional marker to use
 * defaults to `wrvistited`
 */
export function markElemAsVisited(elem, marker = 'wrvistited') {
  if (elem != null) {
    elem.classList.add(marker);
  }
}

/**
 * Creates a style tag if one was not created before and adds
 * the supplied `styleDef` to it. If a style tag was created before
 * this function is a no-op
 * @param {string} styleDef - The CSS rules to add
 * @return {Object} - An object containing the selectors as key value pairs
 * where the keys are the selectors in camelcase and the value is the raw selector
 */
export function addBehaviorStyle(styleDef) {
  let style = document.getElementById('$wrStyle$');
  if (style == null) {
    style = document.createElement('style');
    style.id = '$wrStyle$';
    document.head.appendChild(style);
  }
  style.textContent = styleDef;
  const rules = style.sheet.rules;
  let ruleIdx = rules.length;
  let selector;
  const classes = {};
  while (ruleIdx--) {
    selector = rules[ruleIdx].selectorText.replace('.', '');
    classes[camelCase(selector)] = selector;
  }
  return classes;
}

/**
 * Determines if the supplied iframe is accessible from this
 * origin. Test is that access the window object of the iframes contentWindow
 * does not throw an exception and the contentDocument is not falsy.
 * @param {HTMLIFrameElement} iframe - The iframe to determine accessibility
 * @return {boolean} - True if the iframe is accessible and false otherwise
 */
export function canAccessIf(iframe) {
  if (iframe == null) return false;
  try {
    iframe.contentWindow.window;
  } catch (e) {
    return false;
  }
  return iframe.contentDocument != null;
}

/**
 * Determines if the element the supplied selector selects exists
 * @param {string} selector - The querySelector to use for testing if
 * the element it selects exists
 * @param {Document|SomeElement} [cntx] - Optional element to use rather
 * than the current JS context's document object
 * @return {boolean} - True if the element exists, false otherwise
 */
export function selectorExists(selector, cntx) {
  return qs(selector, cntx) != null;
}

/**
 * Determines if the element the supplied id identifies exists
 * @param {string} eid - The id of the element
 * @param {Document} [cntx] - Optional document object to use rather
 * than the current JS context's document object
 * @return {boolean} - True if the element exists, false otherwise
 */
export function idExists(eid, cntx) {
  return id(eid, cntx) != null;
}

/**
 * Attempts to find a tag using the supplied function that accepts
 * an xpath query and an optional starting element and returns
 * the element the supplied predicate function returns a truthy value for
 * @param {function(query: string, node: ?Node): Node[]} xpg - xpath execution function
 * @param {string} tag - The tag to be found
 * @param {function(elem: SomeElement): boolean} predicate - Element selecting predicate function
 * @param {Document|SomeElement} [cntx] - Optional starting element, defaults to `document`
 * @return {SomeElement} - The desired element if it was found
 */
export function findTag(xpg, tag, predicate, cntx) {
  const tags = xpg(`//${tag}`, cntx || document);
  for (var i = 0; i < tags.length; ++i) {
    if (predicate(tags[i])) return tags[i];
  }
  return null;
}

/**
 * Retrieves the value of an elements attribute if it exists
 * @param {SomeElement} elem - The element to retrieve an attribute from
 * @param {string} attr - The name of the attribute to be retrieved
 * @return {*} - The value of the retrieved attribute if it exists
 */
export function attr(elem, attr) {
  if (elem) return elem.getAttribute(attr);
  return null;
}

/**
 * Tests to determine if the value of elements attribute equals
 * the supplied value using loose equality
 * @param {SomeElement} elem - The element to retrieve an attribute from
 * @param {string} attr - The name of the attribute to be retrieved
 * @param {*} shouldEq - The value the attributes value should equal
 * @return {boolean} - T/F indicating if the attribute equals. Note
 * false can indicate the attribute does not equal expected or
 * the element was null/undefined
 */
export function attrEq(elem, attr, shouldEq) {
  if (elem) return elem.getAttribute(attr) == shouldEq;
  return false;
}

/**
 * Returns the Nth child node of the supplied element (indexing assumes start is 1)
 * @param {SomeElement|Document} elem - The element to retrieve the nth child of
 * @param {number} nth - The number of the nth child
 * @return {?SomeElement} - The nth child if it exists
 */
export function nthChildNodeOf(elem, nth) {
  if (elem && elem.children && elem.children.length >= nth) {
    return elem.childNodes[nth - 1];
  }
  return null;
}

/**
 * Chains {@link nthChildElemOf} for each nth child in `nths`
 * @param {SomeElement} startingElem - The starting parent element
 * @param {...number} nths - The consecutive nth child node
 * @return {?SomeElement}
 * @example
 * // child is the parent elements 3rd child node's 4th child node's 5th child node
 * const child = chainNthChildNodeOf(parentElement, 3, 4, 5);
 */
export function chainNthChildNodeOf(startingElem, ...nths) {
  if (elemHasChildren(startingElem)) {
    let child = startingElem;
    for (var i = 0; i < nths.length; ++i) {
      child = nthChildNodeOf(child, nths[i]);
      if (child == null) break;
    }
    return child;
  }
  return null;
}

/**
 * Returns the first child element of the supplied elements parent element.
 * If the supplied element or the elements parent is null/undefined null is returned
 * @param {?SomeElement} elem - The element who's parents first child is desired
 * @return {?SomeElement}
 */
export function firstChildElemOfParent(elem) {
  if (elem == null || elem.parentElement == null) return null;
  return elem.parentElement.firstElementChild;
}

/**
 * Returns the supplied elements first element child. If the element is null
 * null is returned
 * @param {?SomeElement} elem - The element who's first child is desired
 * @returns {?SomeElement}
 */
export function firstChildElementOf(elem) {
  if (elem != null) return elem.firstElementChild;
  return null;
}

/**
 * Returns the first child element of the element matching the supplied selector
 * @param {string} selector - The selector to match the parent element of the desired child
 * @param {Document|SomeElement} [cntx] - Optional starting element, defaults to `document`
 * @returns {?SomeElement}
 */
export function firstChildElementOfSelector(selector, cntx) {
  return firstChildElementOf(qs(selector, cntx));
}

/**
 * Returns the nth previous sibling of the supplied element if it is not null/undefined or the supplied element is not null/undefined
 * @param {?SomeElement} elem - The element who's nth previous sibling is desired
 * @param {number} nth - The nth previous sibling
 * @return {SomeElement}
 */
export function nthPreviousSibling(elem, nth) {
  let prevSibling = elem;
  for (let i = 0; i < nth; i++) {
    if (!prevSibling) break;
    prevSibling = prevSibling.previousElementSibling;
  }
  return prevSibling;
}

/**
 * Returns the Nth child element of the supplied element (indexing assumes start is 1)
 * @param {SomeElement|Document} elem - The element to retrieve the nth child element of
 * @param {number} nth - The number of the nth child element
 * @return {?SomeElement} - The nth child if it exists
 */
export function nthChildElementOf(elem, nth) {
  if (!elem || !elem.firstElementChild) return null;
  let child = elem.firstElementChild;
  for (let i = 1; i < nth; i++) {
    child = child.nextElementSibling;
    if (!child) break;
  }
  return child;
}

/**
 * Chains {@link nthChildElementOf} for each nth child element in `nths`
 * @param {SomeElement} elem - The starting parent element
 * @param {...number} nths - The consecutive nth child node
 * @return {?SomeElement}
 * @example
 * // child is the parent elements 3rd child element's 4th child element's 5th child element
 * const child = chainNthChildElementOf(parentElement, 3, 4, 5);
 */
export function chainNthChildElementOf(elem, ...nths) {
  let child = elem;
  for (let i = 0; i < nths.length; i++) {
    child = nthChildElementOf(child, nths[i]);
    if (!child) break;
  }
  return child;
}

/**
 * Chains {@link firstChildElementOf} on the supplied element n `times`
 * @param {SomeElement} elem - The starting element
 * @param {number} times - How many times to call {@link firstChildElementOf}
 * @returns {?SomeElement}
 */
export function chainFistChildElemOf(elem, times) {
  if (elem == null) return null;
  let child = elem;
  for (var i = 0; i < times; ++i) {
    child = firstChildElementOf(child);
    if (child == null) break;
  }
  return child;
}

/**
 * Returns the last child element of the supplied element
 * @param {?SomeElement} elem - The element who's last child element is desired
 * @returns {?SomeElement}
 */
export function lastChildElementOf(elem) {
  if (elem != null) return elem.lastElementChild;
  return null;
}

/**
 * Returns the last child element of the element the selector selects
 * @param {string} selector - The selector to be used to select the element who's last child element is desired
 * @param {Document|SomeElement} [cntx] - Optional starting element, defaults to `document`
 * @returns {?SomeElement}
 */
export function lastChildElementOfSelector(selector, cntx) {
  return lastChildElementOf(qs(selector, cntx));
}

/**
 * Chains {@link lastChildElementOf} n `times` starting with the supplied element
 * @param {SomeElement} elem - The starting parent element who
 * @param {number} times - How many times should the
 * @returns {?SomeElement}
 */
export function chainLastChildElemOf(elem, times) {
  let child = elem;
  if (elem != null && elem.children && elem.children.length) {
    for (var i = 0; i < times; ++i) {
      child = lastChildElementOf(child);
      if (child == null) break;
    }
  }
  return child;
}

/**
 * Returns the number of child elements the supplied element has if it is not
 * null/undefined otherwise -1
 * @param {SomeElement} elem - The element who's child element count is deired
 * @return {number}
 */
export function numElemChildren(elem) {
  if (elem != null) {
    return elem.childElementCount;
  }
  return -1;
}

/**
 * Consecutively calls querySelector(selector) on the element returned by the previous invocation
 * @param {SomeElement|Document} startingSelectFrom - The first element to perform querySelector(startingSelector) on
 * @param {string} startingSelector - The first selector
 * @param {...string} selectors - Additional selections
 * @return {?SomeElement} - Final selected element if it exists
 */
export function chainQs(startingSelectFrom, startingSelector, ...selectors) {
  let selected = qs(startingSelector, startingSelectFrom);
  if (selected != null) {
    const len = selectors.length;
    for (var i = 0; i < len; ++i) {
      selected = qs(selectors[i], selected);
      if (selected == null) return null;
    }
  }
  return selected;
}

/**
 * Adds a CSS classname to the supplied element
 * @param {?SomeElement} elem - The element to add the classname to
 * @param {string} clazz - The classname to be added
 */
export function addClass(elem, clazz) {
  if (elem) {
    elem.classList.add(clazz);
  }
}

/**
 * Removes a CSS classname to the supplied element
 * @param {?SomeElement} elem - The element to remove the classname to
 * @param {string} clazz - The classname to be removed
 */
export function removeClass(elem, clazz) {
  if (elem) {
    elem.classList.remove(clazz);
  }
}

/**
 * Tests to see if the supplied element has a css class
 * @param {?SomeElement} elem - The element to be tested if it has the class
 * @param {string} clazz - The classname
 * @return {boolean} - T/F indicating if the element has the class
 */
export function hasClass(elem, clazz) {
  if (elem) return elem.classList.contains(clazz);
  return false;
}

/**
 * Tests to see if the supplied element has the supplied css classes
 * @param {?SomeElement} elem - The element to be tested if it has the classes
 * @param {...string} classes - The classes the element must have
 * @return {boolean} - T/F indicating if the element has the classes
 */
export function hasClasses(elem, ...classes) {
  if (!elem) return false;
  for (let i = 0; i < classes.length; i++) {
    if (!elem.classList.contains(classes[i])) return false;
  }
  return true;
}

/**
 * Tests to see if the supplied element has any of the supplied css classes
 * @param {?SomeElement} elem - The element to be tested if it has any of the classes
 * @param {...string} classes - The classes the element can have
 * @return {boolean} - T/F indicating if the element has any of the classes
 */
export function hasAnyClass(elem, ...classes) {
  if (!elem) return false;
  for (let i = 0; i < classes.length; i++) {
    if (elem.classList.contains(classes[i])) return true;
  }
  return false;
}

/**
 * Returns T/F indicating if the element matches the supplied selector
 * @param {SomeElement} elem
 * @param {string} selector
 * @return {boolean}
 */
export function elemMatchesSelector(elem, selector) {
  if (!elem) return false;
  return elem.matches(selector);
}

/**
 * Returns T/F indicating if the supplied element has not CSS classes
 * @param {SomeElement} elem
 * @return {boolean}
 */
export function isClasslessElem(elem) {
  return elem.classList.length === 0;
}

/**
 * Returns the supplied elements next element sibling
 * @param {SomeElement} elem - The element to receive its sibling
 * @return {?SomeElement} - The elements sibling if it exists
 */
export function getElemSibling(elem) {
  if (!elem) return null;
  return elem.nextElementSibling;
}

/**
 * Returns the next element sibling of the parent element of the
 * supplied element
 * @param {SomeElement} elem - The element to receive its sibling
 * @return {?SomeElement} - The elements sibling if it exists
 */
export function getElemsParentsSibling(elem) {
  if (!elem) return null;
  return getElemSibling(elem.parentElement);
}

/**
 * Returns T/F indicating if the supplied element has an sibling element
 * @param {SomeElement} elem
 * @return {boolean}
 */
export function elemHasSibling(elem) {
  return getElemSibling(elem) != null;
}

/**
 * Returns the supplied elements next element sibling and removes the
 * supplied element
 * @param {SomeElement} elem - The element to receive its sibling
 * @return {?SomeElement} - The elements sibling if it exists
 */
export function getElemSiblingAndRemoveElem(elem) {
  const sibling = getElemSibling(elem);
  elem.remove();
  return sibling;
}

/**
 * Determines if the supplied elements bounding client rect's
 * x,y,width,height,top,left properties all equal zero.
 * Note this function returns true if the element is null/undefined;
 * @param {SomeElement} elem - The element to be tested
 * @return {boolean} - T/F indicating if all zero or not.
 */
export function elemHasZeroBoundingRect(elem) {
  if (elem == null) return true;
  const rect = elem.getBoundingClientRect();
  return (
    rect.x === 0 &&
    rect.y === 0 &&
    rect.width === 0 &&
    rect.height === 0 &&
    rect.top === 0 &&
    rect.left === 0
  );
}

/**
 * Returns T/F indicating if the supplied element is visible.
 *
 * If the supplied element is falsy the return value is false.
 *
 * The test checks the computed style of the supplied element
 * to determine if it's css display property is not null and
 * the visibility of the element is visible.
 *
 * @param {SomeElement} elem
 * @return {boolean}
 */
export function isElemVisible(elem) {
  if (elem == null) return false;
  const computedStyle = window.getComputedStyle(elem);
  if (computedStyle.display === 'none') return false;
  return computedStyle.visibility === 'visible';
}

/**
 * Returns the Nth parent element of the supplied element (indexing assumes start is 1)
 * @param {SomeElement} elem - The element to retrieve the nth parent element of
 * @param {number} nth - The number of the nth parent
 * @return {?SomeElement} - The nth parent element if it exists
 */
export function getNthParentElement(elem, nth) {
  if (elem != null && elem.parentElement != null && nth >= 1) {
    let counter = nth - 1;
    let parent = elem.parentElement;
    while (counter > 0 && parent != null) {
      parent = parent.parentElement;
      counter--;
    }
    return parent;
  }
  return null;
}

/**
 * Returns T/F indicating if the supplied element's textContents contains the supplied string
 * @param {SomeElement} elem
 * @param {string} needle - The string that the elements textContents should contain
 * @param {boolean} [caseInsensitive] - Should the compairison be case insensitive
 * @returns {boolean}
 */
export function elementTextContains(elem, needle, caseInsensitive) {
  if (elem != null && elem.textContent != null) {
    const tc = elem.textContent;
    return (caseInsensitive ? tc.toLowerCase() : tc).includes(needle);
  }
  return false;
}

/**
 * Returns T/F indicating if the supplied element's textContents equals the supplied string
 * @param {SomeElement} elem
 * @param {string} shouldEqual - The string that the elements textContents should be equal to
 * @param {boolean} [caseInsensitive] - Should the compairison be case insensitive
 * @returns {boolean}
 */
export function elementTextEqs(elem, shouldEqual, caseInsensitive) {
  if (elem != null) {
    const tc = elem.textContent;
    return (caseInsensitive ? tc.toLowerCase() : tc) == shouldEqual;
  }
  return false;
}

/**
 * Returns T/F indicating if the supplied element' textContent starts and ends with the supplied start and end strings
 * @param {SomeElement} elem
 * @param {string} start - The string the element's textContent should start with
 * @param {string} end - The string the element's textContent should end with
 * @returns {boolean}
 */
export function elementTextStartsWithAndEndsWith(elem, start, end) {
  return elementTextStartsWith(elem, start) && elementTextEndsWith(elem, end);
}

/**
 * Returns T/F indicating if the supplied element's textContent starts with the supplied string
 * @param {SomeElement} elem
 * @param {string} start - The string the element's textContent should start with
 * @returns {boolean}
 */
export function elementTextStartsWith(elem, start) {
  if (elem != null && elem.textContent != null) {
    return elem.textContent.startsWith(start);
  }
  return false;
}

/**
 * Returns T/F indicating if the supplied element's textContent ends with the supplied string
 * @param {SomeElement} elem
 * @param {string} end - The string the element's textContent should end with
 * @returns {boolean}
 */
export function elementTextEndsWith(elem, end) {
  if (elem != null && elem.textContent != null) {
    return elem.textContent.endsWith(end);
  }
  return false;
}

/**
 * Returns the results of splitting the supplied elements innerText using the supplied splitter
 * @param {SomeElement} elem
 * @param {string|RegExp} splitter
 * @returns {?Array<string>}
 */
export function splitElemInnerText(elem, splitter) {
  if (elem != null && elem.innerText != null) {
    return elem.innerText.split(splitter);
  }
  return null;
}

/**
 * Returns the results of splitting the supplied elements textContent using the supplied splitter
 * @param {SomeElement} elem
 * @param {string|RegExp} splitter
 * @returns {?Array<string>}
 */
export function splitElemTextContents(elem, splitter) {
  if (elem != null && elem.textContent != null) {
    return elem.textContent.split(splitter);
  }
  return null;
}

/**
 * Returns the supplied elements innerText
 * @param {?SomeElement} elem
 * @param {boolean} [trim] - Should the innerText be trimmed
 * @returns {?string}
 */
export function elemInnerText(elem, trim) {
  if (elem != null && elem.innerText != null) {
    return trim ? elem.innerText.trim() : elem.innerText;
  }
  return null;
}

/**
 * Returns T/F if the supplied elements innerText matches the supplied regex
 * @param {SomeElement} elem
 * @param {RegExp} regex
 * @return {boolean}
 */
export function elemInnerTextMatchesRegex(elem, regex) {
  if (elem == null) return false;
  return regex.test(elem.innerText);
}

/**
 * Returns the inner text of the element the supplied selectors matches
 * @param {string} selector - the selector to be use
 * @param {SomeElement|Document} [cntx] - element to use rather than document for the querySelector call
 * @return {?string}
 */
export function innerTextOfSelected(selector, cntx) {
  return elemInnerText(qs(selector, cntx));
}

/**
 * Returns T/F if the supplied elements innerText equals the supplied string case sensitive
 * @param {SomeElement} elem
 * @param {string} shouldEqual - The string the elements inner text should equal
 * @param {boolean} [trim = false] - Should the innerText be trimmed before comparison
 * @return {boolean}
 */
export function elemInnerTextEqs(elem, shouldEqual, trim = false) {
  if (elem == null || !elem.innerText) return false;
  const innerText = trim ? elem.innerText.trim() : elem.innerText;
  return innerText === shouldEqual;
}

/**
 * Returns T/F if the supplied elements innerText equals the supplied string case in-sensitive
 * @param {SomeElement} elem
 * @param {string} shouldEqual
 * @param {boolean} [trim = false]
 * @return {boolean}
 */
export function elemInnerTextEqsInsensitive(elem, shouldEqual, trim = false) {
  if (elem == null || !elem.innerText) return false;
  const innerText = trim ? elem.innerText.trim() : elem.innerText;
  return innerText.toLowerCase() === shouldEqual;
}

/**
 * Returns T/F if the supplied elements innerText equals one of the supplied string case sensitive
 * @param {SomeElement} elem
 * @param {...string} shouldEquals
 * @return {boolean}
 */
export function elemInnerTextEqsOneOf(elem, ...shouldEquals) {
  if (elem != null && elem.innerText != null) return false;
  const innertText = elem.innerText;
  for (var i = 0; i < shouldEquals.length; ++i) {
    if (innertText === shouldEquals[i]) return true;
  }
  return false;
}

/**
 * Returns the textContent of the supplied element
 * @param {SomeElement} elem
 * @returns {?string}
 */
export function elemTextContent(elem) {
  if (elem != null && elem.textContent != null) return elem.textContent;
  return null;
}

/**
 * Returns the scrollTop and scrollLeft values of the supplied document if one
 * was supplied other wise the values are from the current document
 * @param {?Document} [doc]
 * @return {{scrollTop: number, scrollLeft: number}}
 */
export function documentScrollPosition(doc) {
  const documentElem = doc != null ? doc : document;
  const elem = documentElem.body
    ? documentElem.body
    : documentElem.documentElement;
  return {
    scrollTop: elem.scrollTop,
    scrollLeft: elem.scrollLeft,
  };
}

/**
 * Helper function to get the actual x and y position,
 * page x and y position (scroll) and the height, width of a given element
 * @param {Element} element
 * @param {Document} [doc]
 * @return {?{y: number, pageY: number, x: number, pageX: number, w: number, h: number}}
 */
export function getElementPositionWidthHeight(element, doc) {
  if (element == null) return null;
  const rect = element.getBoundingClientRect();
  const scrollPos = documentScrollPosition(doc);
  return {
    y: rect.top,
    x: rect.left,
    pageY: rect.top + scrollPos.scrollTop,
    pageX: rect.left + scrollPos.scrollLeft,
    w: rect.width,
    h: rect.height,
  };
}

/**
 * Get the position of the passed DOM element
 * @param {SomeElement} element
 * @param {Object} options
 * @return {{clientY: number, clientX: number, pageY: number, pageX: number}}
 */
export function getElementClientPagePosition(element, options) {
  const opts = Object.assign({ x: 1, y: 1, floor: false }, options);
  const cords = getElementPositionWidthHeight(element, opts.doc);
  const clientX = cords.x + (cords.w / 100) * opts.x;
  const clientY = cords.y + (cords.h / 100) * opts.y;
  const pageX = cords.pageX + (cords.w / 100) * opts.x;
  const pageY = cords.pageY + (cords.h / 100) * opts.y;
  return {
    clientX: opts.floor ? Math.floor(clientX) : clientX,
    clientY: opts.floor ? Math.floor(clientY) : clientY,
    pageX: opts.floor ? Math.floor(pageX) : pageX,
    pageY: opts.floor ? Math.floor(pageY) : pageY,
  };
}

/**
 * Get the center of the passed DOM element
 * @param {SomeElement} element
 * @param {{floor: boolean}} [options]
 * @return {?{clientY: number, clientX: number, pageY: number, pageX: number}}
 */
export function getElementClientPageCenter(element, options) {
  if (element == null) return null;
  const opts = Object.assign({ floor: false }, options);
  const cords = getElementPositionWidthHeight(element, opts.doc);
  const clientX = cords.x + cords.w / 2;
  const clientY = cords.y + cords.h / 2;
  const pageX = cords.pageX + cords.w / 2;
  const pageY = cords.pageY + cords.h / 2;
  return {
    clientX: opts.floor ? Math.floor(clientX) : clientX,
    clientY: opts.floor ? Math.floor(clientY) : clientY,
    pageX: opts.floor ? Math.floor(pageX) : pageX,
    pageY: opts.floor ? Math.floor(pageY) : pageY,
  };
}

/**
 * Determines if the element the supplied selector selects exists.
 *
 * If one of the supplied selectors matches an existing element the idx property
 * of the returned object is set to the index of the selector in the array and
 * the success property is set to true.
 *
 * Otherwise idx = -1 and success = false.
 * @param {Array<string>} selectors - The query selectors to use for testing if
 * the elements it selects exist
 * @param {SomeElement} [cntx] - Optional element to use rather
 * than the current JS context's document object
 * @return {{idx: number, success: boolean}} - The results of the selectors
 * existence check
 */
export function anySelectorExists(selectors, cntx) {
  const numSelectors = selectors.length;
  for (var i = 0; i < numSelectors; ++i) {
    if (selectorExists(selectors[i], cntx)) {
      return { idx: i, success: true };
    }
  }
  return { idx: -1, success: false };
}

/**
 * Returns T/F indicating if the elements name (localName) is equal to the supplied name
 * @param {SomeElement} elem - The element to check if its name equals the supplied name
 * @param {string} name - The name of the desired element
 * @return {boolean}
 */
export function elementsNameEquals(elem, name) {
  if (!elem) return false;
  return elem.localName === name;
}

/**
 * Returns T/F indicating if the nodes name (nodeName) is equal to the supplied name
 * @param {SomeElement} node - The node to check if its name equals the supplied name
 * @param {string} name - The name of the desired Node
 * @return {boolean}
 */
export function nodesNameEquals(node, name) {
  if (!node) return false;
  return node.nodeName === name;
}

/**
 * @typedef {Object} XPathOnOfOpts
 * @property {Array<string>} queries
 * @property {*} xpg
 * @property {*} [context]
 */

/**
 * Returns the results of evaluating one of the supplied xpath queries if
 * one of the queries yields results otherwise null/undefined
 * @param {XPathOnOfOpts} options
 * @return {?Array<SomeElement>}
 */
export function xpathOneOf({ queries, xpg, context }) {
  let results = null;
  for (var i = 0; i < queries.length; i++) {
    results = xpg(queries[i], context);
    if (results.length || results.snapshotLength) return results;
  }
  return results;
}

/**
 * Returns the results of querySelector using one of the supplied selectors
 * one if one of the selectors yields results otherwise null/undefined
 * @param {{selectors: Array<string>, context: *}} options
 * @return {?SomeElement}
 */
export function qsOneOf({ selectors, context }) {
  if (selectors == null) return null;
  let results = null;
  for (var i = 0; i < selectors.length; i++) {
    results = qs(selectors[i], context);
    if (results) return results;
  }
  return results;
}

/**
 * Returns the results of querySelectorAll using one of the supplied
 * selectors if one of the selectors yields results otherwise null/undefined
 * @param {{selectors: Array<string>, context: *}} options - Optional document object to use rather than
 * defaulting to the current execution contexts document object
 * @return {?NodeList<SomeElement>}
 */
export function qsaOneOf({ selectors, context }) {
  if (selectors == null) return null;
  let results = null;
  for (var i = 0; i < selectors.length; i++) {
    results = qsa(selectors[i], context);
    if (results.length) return results;
  }
  return results;
}

/**
 * Returns the next element sibling of the supplied selector IFF
 * the selector returns a results and the next element sibling exists
 * otherwise null/undefined
 * @param {string} selector - The selector for the element who's next sibling is
 * to be returned
 @param {SomeElement|Document} [context] - element to use rather than document for the querySelector call
 * @return {?SomeElement}
 */
export function selectedNextElementSibling(selector, context) {
  const maybeSelected = qs(selector, context);
  if (maybeSelected && maybeSelected.nextElementSibling) {
    return maybeSelected.nextElementSibling;
  }
  return null;
}

/**
 * Returns T/F indicating if the documents baseURI ends with the supplied string
 * @param {string} shouldEndWith - What the documents base URI should end with
 * @param {Document} [cntxDoc] - Optional document object to use rather than
 * defaulting to the current execution contexts document object
 * @return {boolean}
 */
export function docBaseURIEndsWith(shouldEndWith, cntxDoc) {
  if (!shouldEndWith) return false;
  return (cntxDoc || document).baseURI.endsWith(shouldEndWith);
}

/**
 * Returns T/F indicating if the documents baseURI equals the supplied string
 * @param {string} shouldEqual - What the documents base URI should be equal to
 * @param {Document} [cntxDoc] - Optional document object to use rather than
 * defaulting to the current execution contexts document object
 * @return {boolean}
 */
export function docBaseURIEquals(shouldEqual, cntxDoc) {
  if (!shouldEqual) return false;
  return (cntxDoc || document).baseURI === shouldEqual;
}

/**
 * Repeatably performs the supplied xpath query yielding the results
 * of the each query. If an empty result set is encountered and generateMoreElements
 * function was supplied it is called and the query repeated. If the second query
 * try yields another empty set the iterator ends
 * @param {string} query - The xpath query to be repeated until it returns no more elements
 * @param [cntx] - Optional element to execute the xpath query from (defaults to document)
 * @param {function(): void} [generateMoreElements] - Optional function used to generate more elements that may match the supplied xpath
 * @return {IterableIterator<SomeElement>}
 */
export function* repeatedXpathQueryIterator(query, cntx, generateMoreElements) {
  let snapShot = xpathSnapShot(query, cntx);
  const haveGenMore = typeof generateMoreElements === 'function';
  while (snapShot.snapshotLength > 0) {
    for (let i = 0; i < snapShot.snapshotLength; i++) {
      yield snapShot.snapshotItem(i);
    }
    snapShot = xpathSnapShot(query, cntx);
    if (snapShot.snapshotLength === 0) {
      if (haveGenMore) generateMoreElements();
      snapShot = xpathSnapShot(query, cntx);
    }
  }
}

/**
 * Repeatably performs the supplied xpath query yielding the results
 * of the each query. If an empty result set is encountered and generateMoreElements
 * function was supplied it is called and the query repeated. If the second query
 * try yields another empty set the iterator ends
 * @param {string} query - The xpath query to be repeated until it returns no more elements
 * @param [cntx] - Optional element to execute the xpath query from (defaults to document)
 * @param {function(): *} [generateMoreElements] - Optional function used to generate more elements that may match the supplied xpath
 * @return {AsyncIterableIterator<SomeElement>}
 */
export async function* repeatedXpathQueryIteratorAsync(
  query,
  cntx,
  generateMoreElements
) {
  let snapShot = xpathSnapShot(query, cntx);
  let i;
  const haveGenMore = typeof generateMoreElements === 'function';
  while (snapShot.snapshotLength > 0) {
    for (i = 0; i < snapShot.snapshotLength; i++) {
      yield snapShot.snapshotItem(i);
    }
    snapShot = xpathSnapShot(query, cntx);
    if (snapShot.snapshotLength === 0) {
      if (haveGenMore) {
        const result = generateMoreElements();
        if (isPromise(result)) await result;
      }
      snapShot = xpathSnapShot(query, cntx);
    }
  }
}

/**
 * Returns the value of the elements data attribute if it exists
 * If the element is null/undefined or does not have data attributes null is returned.
 * @param {HTMLElement|SVGElement} elem - The element that should have the data attribute
 * @param {string} dataKey - The name of the data value to be retrieved
 * @return {?string}
 */
export function elemDataValue(elem, dataKey) {
  if (!elem) return null;
  if (!elem.dataset) return null;
  return elem.dataset[dataKey];
}

/**
 * Returns T/F indicating if the element has data attribute equal to the supplied value
 * @param {HTMLElement|SVGElement} elem - The element that should have the data attribute
 * @param dataKey - The name of the data value to be retrieved
 * @param dataValue - The expected value of the elements data attribute
 * @return {boolean}
 */
export function elemDataValueEqs(elem, dataKey, dataValue) {
  if (!elem || !elem.dataset) return false;
  return elem.dataset[dataKey] === dataValue;
}

/**
 * Returns T/F indicating if the supplied element has children.
 * If the supplied element is null/undefined false is returned.
 * @param {Element?} elem - The element to be checked for children
 * @return {boolean}
 */
export function elemHasChildren(elem) {
  if (elem == null) return false;
  if (typeof elem.hasChildNodes === 'function') {
    return elem.hasChildNodes();
  }
  return elem.children.length > 0;
}

/**
 * Returns an iterator over the supplied parent element's child elements that
 * ends once the current child has no next element sibling.
 *
 * If the parent element is null/undefined then the returned iterator yields nothing.
 * @param {SomeElement?} parentElement - The parent element who's child elements will be iterated over
 * @return {IterableIterator<SomeElement?>}
 */
export function* childElementIterator(parentElement) {
  if (parentElement == null) return;
  let child = parentElement.firstElementChild;
  while (child != null) {
    yield child;
    child = child.nextElementSibling;
  }
}

/**
 * Returns an iterator over the supplied parent element's child nodes that
 * ends once the current child has no next sibling.
 *
 * If the parent element is null/undefined then the returned iterator yields nothing.
 * @param {SomeElement?} parentElement - The parent element who's child nodes will be iterated over
 * @return {IterableIterator<Node?>}
 */
export function* childNodeIterator(parentElement) {
  if (parentElement == null) return;
  let child = parentElement.firstChild;
  while (child != null) {
    yield child;
    child = child.nextSibling;
  }
}

/**
 * Applies the supplied `predicate` to every child element of the supplied
 * parent element and returns the element that the `predicate` returns true for
 * otherwise returns null.
 *
 * If the parent element is null then null is returned.
 * @param {SomeElement} parentElement - The parent element who's child element will be searched
 * @param {function(elem: SomeElement): boolean} predicate - The predicate function used to select a child
 * @return {?SomeElement}
 */
export function findDirectChildElement(parentElement, predicate) {
  if (parentElement == null) return null;
  for (let i = 0; i < parentElement.children.length; i++) {
    if (predicate(parentElement.children[i])) return parentElement.children[i];
  }
  return null;
}

/**
 * @typedef {HTMLAnchorElement|HTMLElement|HTMLAppletElement|HTMLAreaElement|HTMLAudioElement|HTMLBaseElement|HTMLBaseFontElement|HTMLQuoteElement|HTMLBodyElement|HTMLBRElement|HTMLButtonElement|HTMLCanvasElement|HTMLTableCaptionElement|HTMLTableColElement|HTMLDataElement|HTMLDataListElement|HTMLModElement|HTMLDetailsElement|HTMLDialogElement|HTMLDirectoryElement|HTMLDivElement|HTMLDListElement|HTMLEmbedElement|HTMLFieldSetElement|HTMLFontElement|HTMLFormElement|HTMLFrameElement|HTMLFrameSetElement|HTMLHeadingElement|HTMLHeadElement|HTMLHRElement|HTMLHtmlElement|HTMLIFrameElement|HTMLImageElement|HTMLInputElement|HTMLLabelElement|HTMLLegendElement|HTMLLIElement|HTMLLinkElement|HTMLMapElement|HTMLMarqueeElement|HTMLMenuElement|HTMLMetaElement|HTMLMeterElement|HTMLObjectElement|HTMLOListElement|HTMLOptGroupElement|HTMLOptionElement|HTMLOutputElement|HTMLParagraphElement|HTMLParamElement|HTMLPictureElement|HTMLPreElement|HTMLProgressElement|HTMLScriptElement|HTMLSelectElement|HTMLSlotElement|HTMLSourceElement|HTMLSpanElement|HTMLStyleElement|HTMLTableElement|HTMLTableSectionElement|HTMLTableDataCellElement|HTMLTemplateElement|HTMLTextAreaElement|HTMLTableHeaderCellElement|HTMLTimeElement|HTMLTitleElement|HTMLTableRowElement|HTMLTrackElement|HTMLUListElement|HTMLVideoElement|Element|Node|SVGAElement|SVGCircleElement|SVGClipPathElement|SVGDefsElement|SVGDescElement|SVGEllipseElement|SVGFEBlendElement|SVGFEColorMatrixElement|SVGFEComponentTransferElement|SVGFECompositeElement|SVGFEConvolveMatrixElement|SVGFEDiffuseLightingElement|SVGFEDisplacementMapElement|SVGFEDistantLightElement|SVGFEFloodElement|SVGFEFuncAElement|SVGFEFuncBElement|SVGFEFuncGElement|SVGFEFuncRElement|SVGFEGaussianBlurElement|SVGFEImageElement|SVGFEMergeElement|SVGFEMergeNodeElement|SVGFEMorphologyElement|SVGFEOffsetElement|SVGFEPointLightElement|SVGFESpecularLightingElement|SVGFESpotLightElement|SVGFETileElement|SVGFETurbulenceElement|SVGFilterElement|SVGForeignObjectElement|SVGGElement|SVGImageElement|SVGLineElement|SVGLinearGradientElement|SVGMarkerElement|SVGMaskElement|SVGMetadataElement|SVGPathElement|SVGPatternElement|SVGPolygonElement|SVGPolylineElement|SVGRadialGradientElement|SVGRectElement|SVGScriptElement|SVGStopElement|SVGStyleElement|SVGSVGElement|SVGSwitchElement|SVGSymbolElement|SVGTextElement|SVGTextPathElement|SVGTitleElement|SVGTSpanElement|SVGUseElement|SVGViewElement} SomeElement
 */