utils/dom.js

import isArray from 'lodash/isArray';
import isString from 'lodash/isString';
import includes from 'lodash/includes';

/**
 * Utilities for working with the Document Object Model (DOM).
 * @module DOMUtils
 */
const DOMUtils = {
    /**
     * Returns the tag name of the specified element.
     * @static
     * @function tag
     * @param {Element} el - The element to check the tag of.
     * @param {Boolean} [lowercase=true] - Whether to normalize the tag name for comparison as lowercase.
     * @example
     * DOMUtils.tag(document.querySelector('.foo'));
     * @returns {String} - The tag of the specified element.
     */
    tag(el, lowercase = true) {
        return (lowercase) ? el.nodeName.toLowerCase() : el.nodeName;
    },

    /**
     * Returns the element(s) matching the specified selector.
     * @static
     * @function el
     * @param {String} selector - The selector of the element to query.
     * @example
     * DOMUtils.el('.foo');
     * @returns {Element|Array} - The element or array of elements matching the selector.
     */
    el(selector) {
        const els = document.querySelectorAll(selector);

        if (els.length > 1) {
            return els;
        } else {
            return els[0];
        }
    },

    /**
     * Returns the element's parent or the closest ancestor matching the specified selector if provided.
     * @static
     * @function parent
     * @param {String|Element} item - The selector or element to find the parent of.
     * @param {String} [parentSelector] - The selector of the ancestor to find.
     * @example
     * DOMUtils.parent('.foo', '.bar');
     * @returns {Element} - The parent of the item.
     */
    parent(item, parentSelector) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return undefined; }

        if (parentSelector) {
            const allParents = document.querySelectorAll(parentSelector);
            let currentParent = el.parentNode;

            while (currentParent && !includes(allParents, currentParent)) {
                currentParent = currentParent.parentNode;
            }

            return currentParent;
        } else {
            return el.parentNode;
        }
    },

    /**
     * Returns the attribute of the specified element.
     * @static
     * @function attr
     * @param {String|Element} item - The selector or element to query.
     * @param {String} attribute - The attribute to query.
     * @example
     * DOMUtils.attr('.foo', 'data-baz');
     * @returns {String} - The value of the attribute.
     */
    attr(item, attribute) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return; }

        return el.getAttribute(attribute);
    },

    /**
     * Adds a class to the specified element.
     * @static
     * @function addClass
     * @param {String|Element} item - The selector or element to add the class to.
     * @param {String} className - The class to add.
     * @example
     * DOMUtils.addClass(evt.currentTarget, 'foo');
     * @returns {void}
     */
    addClass(item, className) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return; }

        if (el.classList) {
            el.classList.add(className);
        } else {
            el.className += ` ${className}`;
        }
    },

    /**
     * Removes a class from the specified element.
     * @static
     * @function removeClass
     * @param {String|Element} item - The selector or element to remove the class from.
     * @param {String} className - The class to remove.
     * @example
     * DOMUtils.removeClass(evt.currentTarget, 'foo');
     * @returns {void}
     */
    removeClass(item, className) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return; }

        if (el.classList) {
            el.classList.remove(className);
        } else {
            el.className = el.className.replace(new RegExp(`(^|\\b)${className.split(' ').join('|')}(\\b|$)`, 'gi'), ' ');
        }
    },

    /**
     * Checks if an element has a specified class.
     * @static
     * @function hasClass
     * @param {String|Element} item - The selector or element to check.
     * @param {String} className - The class to check for.
     * @example
     * DOMUtils.hasClass(evt.currentTarget, 'foo');
     * @returns {Boolean} - Whether the element has the class or not
     */
    hasClass(item, className) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return false; }

        if (el.classList) {
            return el.classList.contains(className);
        } else {
            return includes(el.className.split(' '), className);
        }
    },

    /**
     * Get the height of the specified element.
     * @static
     * @function height
     * @param {String|Element} item - The selector or element to find the height of.
     * @param {Boolean} [includeMargin=true] - Whether to include the margins in the calculation effectively getting the outerHeight.
     * @param {Boolean} [includePadding=true] - Whether to include the padding in the calculation.
     * @example
     * DOMUtils.height('.foo');
     * @returns {Number} - The height of the element.
     */
    height(item, includeMargin = true, includePadding = true) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return 0; }

        const {
            marginTop,
            marginBottom,
            paddingTop,
            paddingBottom
        } = getComputedStyle(el);
        let height = el.offsetHeight;

        if (includeMargin) {
            height += (parseInt(marginTop, 10) + parseInt(marginBottom, 10));
        }

        if (!includePadding) {
            height -= (parseInt(paddingTop, 10) + parseInt(paddingBottom, 10));
        }

        return height;
    },

    /**
     * Get the width of the specified element.
     * @static
     * @function width
     * @param {String|Element} item - The selector or element to find the width of.
     * @param {Boolean} [includeMargin=true] - Whether to include the margins in the calculation effectively getting the outerWidth.
     * @param {Boolean} [includePadding=true] - Whether to include the padding in the calculation.
     * @example
     * DOMUtils.width('.foo');
     * @returns {Number} - The width of the element.
     */
    width(item, includeMargin = true, includePadding = true) {
        const el = (isString(item))
            ? DOMUtils.el(item)
            : item;

        if (!el) { return 0; }

        const {
            marginLeft,
            marginRight,
            paddingLeft,
            paddingRight
        } = getComputedStyle(el);
        let width = el.offsetWidth;

        if (includeMargin) {
            width += (parseInt(marginLeft, 10) + parseInt(marginRight, 10));
        }

        if (!includePadding) {
            width -= (parseInt(paddingLeft, 10) + parseInt(paddingRight, 10));
        }

        return width;
    },

    /**
     * Removes element(s) from the DOM.
     * @static
     * @function remove
     * @param {String|Element|Array} items - The selector or element(s) to remove from the DOM.
     * @example
     * DOMUtils.remove('.class-to-remove');
     * @returns {void}
     */
    remove(items) {
        const el = (isString(items))
            ? DOMUtils.el(items)
            : items;

        if (!el) { return; }

        if (isArray(el)) {
            el.each(thisEl => {
                thisEl.remove();
            });
        } else {
            el.remove();
        }
    }
};

export default DOMUtils;