Source: element.js

import extenders from './extenders/index';
import Extender from './extenders/extender';

/**
 * El Class
 * @class
 * @classdesc Creates an HTML element and wraps it with convenience methods.
 */
class El {

    /**
     * El class constructor.
     * @param {String} type - Type of element to create.
     * @param {Object} attrs - Attributes to set.
     * @param {Array} children - Child elements to nest.
     * @returns {Object} El class instance.
     */
    constructor(type, attrs, children) {

        if (El.isArray(attrs)) children = attrs;

        /**
         * @member
         * @type {Object}
         */
        this._attributes = El.isObject(attrs) ? attrs : {};

        /**
         * @member
         * @type {String}
         */
        this.type = type.split('.')[0].split('#')[0].split('$')[0];

        /**
         * @member
         * @type {Array}
         */
        this.events = [];

        /**
         * @member
         * @type {Array}
         */
        this.extenders = extenders.concat(this.customExtenders ? this.customExtenders.map(i => new El.Extender(i.name, i.action)) : []);

        /**
         * @member
         * @type {Object}
         */
        this.element = this._createElement();

        // Set attributes.
        this._setAttributes();

        // Parse the shorthand class/id/$key properties.
        this._parseType(type);

        // Set the unique $key.
        this.setKey(this.$key || this._attributes.$key || this.getAttribute('id') || null);

        /**
         * @member
         * @type {Array}
         */
        this.children = El.isArray(children) ? this._setChildren(children) : [];

        /**
         * Set default animation speed
         */
        this._animationSpeed = this._animationSpeed || 200;

        this.element.EL_INSTANCE = this;

        return this;

    }

    /**
     * @member
     * @static
     * @type {Object}
     */
    static get Extender() {
        return Extender;
    }

    /**
     * Proxy event handler - used to avoid anonymous functions on event bindings
     * @static
     * @private
     * @param {Function} handler - Event function
     * @param {String} [namespace] - Optional namespace
     * @returns {Function} - Proxied event handler
     */
    static _eventHandler(handler, namespace) {
        return function eventHandler(e) {
            e.namespace = namespace;
            handler.call(this, e);
        };
    }

    /**
     * Add custom extenders
     * @static
     * @public
     * @param {Object[]} extenders - Array of extender configurations
     * @param {String} extenders.name - Name of custom extender
     * @param {Function} extenders.action - Action of the custom extender
     */
    static addExtenders(extenders) {
        El.prototype.customExtenders = extenders;
    }

    /**
     * Set default animation speed
     * @static
     * @public
     * @param {Number} animationSpeed - Default animation speed
     */
    static setAnimationSpeed(animationSpeed) {
        El.prototype._animationSpeed = animationSpeed;
    }

    /**
     * Simple array check.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is array.
     */
    static isArray(item) {
        return Array.isArray(item);
    }

    /**
     * Simple object check. Arrays not included.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is object.
     */
    static isObject(item) {
        return typeof item === 'object' && !Array.isArray(item) && item !== null;
    }

    /**
     * Simple string check.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is string.
     */
    static isString(item) {
        return typeof item === 'string' || item instanceof String;
    }

    /**
     * Simple number check.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is number.
     */
    static isNumber(item) {
        return Number.isInteger(item);
    }

    /**
     * Simple boolean check.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is boolean.
     */
    static isBoolean(item) {
        return typeof item === 'boolean';
    }

    /**
     * Simple truthy check - only used for setting attribute values. Eliminates null, undefined and Infinity. Zero's are considered truthy (useful for setting attributes), as well as empty strings (used for setting blank attributes) and false (again, for setting attributes).
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is truthy.
     */
    static isTruthy(item) {
        return item !== undefined && item !== null && item !== Infinity;
    }

    /**
     * Simple function check.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is function.
     */
    static isFunction(item) {
        return typeof item === 'function';
    }

    /**
     * Simple date check.
     * @static
     * @public
     * @param item - Item to check.
     * @returns {Boolean} Is date.
     */
    static isDate(item) {
        return item instanceof Date;
    }

    /**
     * Convert an object to a query string.
     * @public
     * @static
     * @param  {Object} obj - JS object to encode.
     * @returns {String} URL encoded query string.
     */
    static toQueryString(obj) {
        const str = [];
        for (const p in obj) {
            /* istanbul ignore else */
            if (obj.hasOwnProperty(p)) {
                str.push(`${encodeURIComponent(p)}=${encodeURIComponent(obj[p])}`);
            }
        }
        return str.join('&');
    }

    /**
     * Gets all nested child El instances. Recursive.
     * @private
     * @param {String} $key - Element unique $key.
     * @param {String|Array} type - Element type filter.
     * @param {Array} returnedEls - Array of filtered elements (internal, recursive use only).
     * @returns {Array} Child el instances.
     */
    _getChildren($key, type, returnedEls) {
        $key = El.isString($key) ? $key : null;
        type = El.isArray(type) ? type : [];
        returnedEls = returnedEls || [];
        const children = this.children;
        let i = 0;
        for (; i < children.length; i++) {
            const child = children[i];
            if ($key) {
                if ($key === child.$key) returnedEls.push(child);
            } else if (type && type.length) {
                if (type.includes(child.type)) returnedEls.push(child);
            } else {
                returnedEls.push(child);
            }
            if (child.children) this._getChildren.call(child, $key, type, returnedEls);
        }
        return returnedEls;
    }

    /**
     * Parse the shorthand class/id/$key properties.
     * @private
     * @returns {String} Element type.
     */
    _parseType(type) {
        const matchPattern = /([a-zA-Z0-9]+|#[\w-\d]+|\.[\w\d-]+|\$[\w\d-]+)/g;
        const matches = type.match(matchPattern);
        const elementType = matches.shift();
        for (let i = 0; i < matches.length; i++) {
            if (matches[i].includes('#')) this.setAttribute('id', matches[i].slice(1));
            if (matches[i].includes('.')) this.addClass(matches[i].slice(1));
            if (matches[i].includes('$')) this.setKey(matches[i].slice(1));
        }
        return elementType;
    }

    /**
     * Creates dom element.
     * @private
     * @returns {Object} Dom node.
     */
    _createElement() {

        return document.createElement(this.type);

    }

    /**
     * Sets all attributes.
     * @private
     */
    _setAttributes() {

        const attrs = this._attributes;
        const extenderNames = this.extenders.map(i => i.name);

        for (const prop in attrs) {
            if (attrs.hasOwnProperty(prop)) {

                // Check for an existing extender that matches the attribute name
                if (extenderNames.includes(prop)) this.extenders[extenderNames.indexOf(prop)].action.call(this, attrs[prop]);
                else this._setAttribute(prop, attrs);
            }
        }

    }

    /**
     * Sets an attribute.
     * @private
     */
    _setAttribute(prop, attrs) {
        if (El.isTruthy(attrs[prop])) {
            if (prop === '$key') {
                this.$key = attrs[prop];
            } else {
                this.element.setAttribute(prop, El.isArray(attrs[prop]) ? attrs[prop].join(' ') : attrs[prop]);
            }
        }
    }

    /**
     * Set the child elements.
     * @private
     * @returns {Array} Child elements.
     */
    _setChildren(children) {

        const tracker = {};

        for (let i = 0; i < children.length; i++) {
            const child = children[i];
            if (!tracker[child.type]) tracker[child.type] = [];
            tracker[child.type].push(child.type);
            this.setKey.call(child, child.$key || child._attributes.$key || child.getAttribute('id') || `${child.type}${tracker[child.type].length - 1}`);
            this._append(child);
        }

        return children;

    }

    /**
     * Adds a class or classes to an element.
     * @chainable
     * @public
     * @param {String|Array} myClass - ClassName(s) to add. Can be a space-separated string or an array.
     * @returns {Object} El class instance.
     */
    addClass(myClass) {
        if (!myClass) return this;
        if (El.isArray(myClass)) myClass = myClass.join(' ');
        if (myClass.includes(' ')) {
            myClass = myClass.split(' ');
            for (let i = 0; i < myClass.length; i++) {
                if (!(` ${this.element.className} `).includes(` ${myClass[i]} `)) {

                    if (this.element.classList) this.element.classList.add(myClass[i]);
                    else this.element.className += ` ${myClass[i]}`;

                }
            }
        } else if (!(` ${this.element.className} `).includes(` ${myClass} `)) {

            if (this.element.classList) this.element.classList.add(myClass);
            else this.element.className += ` ${myClass}`;

        }
        return this;
    }

    /**
     * Insert an element after another.
     * @chainable
     * @public
     * @param {Object} el - El class instance.
     * @returns {Object} El class instance.
     */
    after(el) {
        const parent = this.parent();
        if (this.element.after) {
            this.element.after(el.element);
        } else {
            parent.insertBefore(el.element, this.element.nextSibling);
        }
        // parent.children.push(el);
        parent.EL_INSTANCE.children.push(el);
        return this;
    }

    /**
     * Show an element with visibility: visible.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    appear() {
        this.setStyles({
            visibility: 'visible',
        });
        return this;
    }

    /**
     * Show an element with appear() for x number of milliseconds.
     * @chainable
     * @public
     * @param {Number} [interval=200] - Interval in milliseconds.
     * @returns {Object} El class instance.
     */
    appearFor(interval) {
        interval = interval || this._animationSpeed;
        this.appear();
        setTimeout(this.disappear.bind(this), interval);
        return this;
    }

    /**
     * Append an element or text node.
     * @chainable
     * @public
     * @param {Object|String} el - Element to append or string to become text node to append.
     * @returns {Object} El class instance.
     */
    append(el) {
        this._append(el);
        if (el && el instanceof El) this.children.push(el);
    }

    /**
     * Append without adding to the children array
     * @private
     */
    _append(el) {
        if (!el) return this;
        if (El.isObject(el)) this.element.appendChild(el.element || el);
        if (El.isString(el)) this.appendHTML(el);
    }

    /**
     * Append HTML
     * @chainable
     * @public
     * @param {String} html - HTML to append.
     * @returns {Object} El class instance.
     */
    appendHTML(html) {
        if (!html) return this;
        this.setHTML(`${this.getHTML()}${html}`);
    }

    /**
     * Append text content to an element.
     * @chainable
     * @public
     * @param {String} text - Text to append.
     * @returns {Object} El class instance.
     */
    appendText(text) {
        if (!El.isTruthy(text)) return this;
        const newText = document.createTextNode(text);
        this.element.appendChild(newText);
        return this;
    }

    /**
     * Blur element (just a wrapper for the .blur DOM method).
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    blur() {
        this.element.blur();
        return this;
    }

    /**
     * Simulate an element click (just a wrapper for the .click DOM method).
     * @public
     * @chainable
     * @returns {Object} El class instance.
     */
    click() {
        this.element.click();
        return this;
    }

    /**
     * Disable an element by attribute and class.
     * @chainable
     * @public
     * @param {Boolean} [shouldDisable=true] - Conditionally disable.
     * @returns {Object} El class instance.
     */
    disable(shouldDisable) {
        shouldDisable = El.isBoolean(shouldDisable) ? shouldDisable : true;
        if (shouldDisable) this.setAttribute('disabled', 'disabled').addClass('disabled');
        return this;
    }

    /**
     * Hide an element with visibility: hidden.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    disappear() {
        this.setStyles({
            visibility: 'hidden',
        });
        return this;
    }

    /**
     * Hide an element with disappear() for x number of milliseconds.
     * @chainable
     * @public
     * @param {Number} [interval=200] - Interval in milliseconds.
     * @returns {Object} El class instance.
     */
    disappearFor(interval = this._animationSpeed) {
        this.disappear();
        setTimeout(this.appear.bind(this), interval);
        return this;
    }

    /**
     * Empty an element's html content.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    empty() {
        this.element.innerHTML = '';
        return this;
    }

    /**
     * Enable an element by attribute and class.
     * @chainable
     * @public
     * @param {Boolean} [shouldEnable=true] - Conditionally enable.
     * @returns {Object} El class instance.
     */
    enable(shouldEnable) {
        shouldEnable = El.isBoolean(shouldEnable) ? shouldEnable : true;
        if (shouldEnable) this.removeAttribute('disabled').removeClass('disabled');
        return this;
    }

    /**
     * Convert field value to URL encoded query string.
     * @public
     * @returns {String} URL encoded query string.
     */
    encode() {
        return El.toQueryString(this.serialize('object'));
    }

    /**
     * Fade in element.
     * @chainable
     * @public
     * @param {Number} [interval=200] - Transition interval (in milliseconds).
     * @param {String} [display=block] - CSS display property to set after element is faded in. Is only necessary if the CSS display property is set to 'none'.
     */
    fadeIn(interval = this._animationSpeed, display = 'block') {
        const el = this.element;
        el.style.opacity = 0;
        if (el.style.display === 'none') el.style.display = display;

        (function fade() {
            const val = parseFloat(el.style.opacity) + 0.1;
            if (val <= 1) {
                el.style.opacity = val;
                setTimeout(fade, interval / 10);
            }
        }());
    }

    /**
     * Show an element with fadeIn() for x number of milliseconds.
     * @chainable
     * @public
     * @param {Number} [speed=2000] - Interval in milliseconds (shown interval).
     * @param {Number} [interval=200] - Interval in milliseconds (animation speed).
     * @returns {Object} El class instance.
     */
    fadeInFor(speed = this._animationSpeed, interval = this._animationSpeed) {
        this.fadeIn(interval);
        setTimeout(this.fadeOut.bind(this, interval), Math.max(interval, speed));
        return this;
    }

    /**
     * Fade out element.
     * @chainable
     * @public
     * @param {Number} [interval=200] - Transition interval (in milliseconds).
     * @param {Boolean} [hide=true] - Set to false to prevent element from being hidden after the animation.
     */
    fadeOut(interval = this._animationSpeed, hide = true) {
        const el = this.element;
        el.style.opacity = 1;

        (function fade() {
            el.style.opacity -= 0.1;
            if (el.style.opacity <= 0) {
                if (hide) el.style.display = 'none';
            } else {
                setTimeout(fade, interval / 10);
            }
        }());
    }

    /**
     * Hide an element with fadeOut() for x number of milliseconds.
     * @chainable
     * @public
     * @param {Number} [speed=2000] - Interval in milliseconds (hidden interval).
     * @param {Number} [interval=200] - Interval in milliseconds (animation speed).
     * @returns {Object} El class instance.
     */
    fadeOutFor(speed = this._animationSpeed, interval = this._animationSpeed) {
        this.fadeOut(interval);
        setTimeout(this.fadeIn.bind(this, interval), speed + interval);
        return this;
    }

    /**
     * Set focus on element.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    focus() {
        this.element.focus();
        return this;
    }

    /**
     * Retrieve a child element.
     * @param {String} $key - Child identifying $key.
     * @returns {Object} El class instance.
     */
    get($key = null) {
        const matches = this._getChildren($key);
        if (!matches.length) return null;
        if (matches.length > 1) return matches;
        return matches[0];
    }

    /**
     * Gets an attribute.
     * @public
     * @param {String} attr - Attribute name to get.
     * @returns {String} Attribute value.
     */
    getAttribute(attr) {
        if (!attr) return null;
        return this.element.getAttribute(attr);
    }

    /**
     * Gets the list of classes applied to the element.
     * @public
     * @returns {Object|Array} DOMTokenList or array of classes (depending on ClassList support).
     */
    getClassList() {
        if (this.element.classList) return this.element.classList;
        return this.element.className.split(' ');
    }

    /**
     * Gets the html content.
     * @public
     * @returns {String} Html content.
     */
    getHTML() {
        return this.element.innerHTML;
    }

    /**
     * Gets the text content.
     * @chainable
     * @public
     * @returns {String} Text content.
     */
    getText() {
        return this.element.textContent;
    }

    /**
     * Gets the value.
     * @public
     * @returns {String} Element value.
     */
    getValue() {
        return this.element.value;
    }

    /**
     * Check for existence of an attribute.
     * @public
     * @param {String} attrToCheck - Attribute to check.
     * @returns {Booolean} Has attribute.
     */
    hasAttribute(attrToCheck) {
        if (this.element.hasAttribute(attrToCheck)) return true;
        return false;
    }

    /**
     * Check for class existence.
     * @public
     * @param {String} classToCheck - Classname to check.
     * @returns {Boolean} Class exists.
     */
    hasClass(classToCheck) {

        // return new RegExp(` ${classToCheck} `).test(` ${this.element.className} `);
        if (this.element.classList) return this.element.classList.contains(classToCheck);
        return new RegExp('(^| )' + classToCheck + '( |$)', 'gi').test(this.element.className);

    }

    /**
     * Check if an element has a value.
     * @public
     * @returns {Boolean} Element has value.
     */
    hasValue() {

        const val = this.getValue();
        if (val !== '' && val.length > 0) return true;
        return false;

    }

    /**
     * Hide element with display rule.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    hide() {
        this.setStyles({
            display: 'none',
        });
        return this;
    }

    /**
     * Call hide method, then show after x amount of ms.
     * @chainable
     * @public
     * @param {Number} [speed=200] - Interval to hide.
     * @returns {Object} El class instance.
     */
    hideFor(speed = this._animationSpeed) {
        this.hide();
        setTimeout(this.show.bind(this), speed);
        return this;
    }

    /**
     * Return the next sibling element
     * @public
     * @returns {Object} node
     */
    next() {
        return this.element.nextElementSibling || null;
    }

    /**
     * Detach event handler.
     * @chainable
     * @private
     * @param {String} eventName - Name of event.
     * @param {Function} handler - Function reference.
     * @param {Object} [options] - Event options.
     * @param {Boolean} [options.capture] - Event capture option.
     * @param {Boolean} [options=false] - Event capture option.
     * @returns {Object} El class instance.
     */
    _off(eventName, handler, options = {
        capture: false,
    }) {
        const events = this.events;
        if (!handler) {
            for (let i = events.length - 1; i >= 0; i--) {
                if (events[i].event === eventName) {
                    this.element.removeEventListener(events[i].event, events[i].handler, events[i].options);
                    events.splice(i, 1);
                }
            }
            return this;
        }
        const index = events.map(i => i.event).indexOf(eventName);
        this.element.removeEventListener(eventName, handler, options);
        if (index > -1) events.splice(index, 1);
        return this;
    }

    /**
     * Detach event handler. If only the eventName parameter is given, all events of that event type will be detached. If no arguments are given, all events will be detached.
     * @chainable
     * @public
     * @param {String} eventName - Name of event.
     * @param {Function} handler - Function reference.
     * @param {Object} [options] - Event options.
     * @param {Boolean} [options.capture] - Event capture option.
     * @param {Boolean} [options=false] - Event capture option.
     * @returns {Object} El class instance.
     */
    off(eventName, handler, options = {
        capture: false,
    }) {
        if (eventName && handler) return this._off(eventName, handler, options);
        if (!eventName) return this.unbindEvents();
        const events = this.events;
        for (let i = events.length - 1; i >= 0; i--) {
            if (events[i].event && events[i].event === eventName) {
                this._off(events[i].event, events[i].handler, events[i].options);
                events.splice(i, 1);
            }
        }
        return this;
    }

    /**
     * Listen for events.
     * @chainable
     * @public
     * @param {String|Array} eventsToBind - Name of event(s) to bind. Can be either space-separated string or array.
     * @param {Function} handler - Function handler reference.
     * @param {Object|Boolean} options - event options.
     * @param {Boolean} options.capture - event capture option.
     * @param {Boolean} options.once - event once option.
     * @param {Boolean} options.passive - event passive option.
     * @returns {Object} El class instance.
     */
    on(eventsToBind, handler, options = {
        capture: false,
        once: false,
        passive: false,
    }) {
        let namespace = null;
        const eventParts = El.isArray(eventsToBind) ? eventsToBind : eventsToBind.split(/\s+/);
        for (let i = 0; i < eventParts.length; i++) {
            let eventName = eventParts[i];
            if (eventName.includes('.')) {
                const splitEvent = eventName.split('.');
                namespace = splitEvent.slice(1).join('.');
                eventName = splitEvent[0];
            }
            const eventHandler = El._eventHandler(handler, namespace);
            this.element.addEventListener(eventName, eventHandler, options);
            this.events.push({
                event: eventName,
                handler: eventHandler,
                namespace,
                options,
            });
        }

        return this;
    }

    one(eventsToBind, handler, options = {
        capture: false,
        once: true,
        passive: false,
    }) {
        if (!options.once) options.once = true;
        return this.on(eventsToBind, handler, options);
    }

    /**
     * Return the parent element.
     * @public
     * @returns {Object} Parent element.
     */
    parent() {
        return this.element.parentElement || this.element.parentNode || null;
    }

    /**
     * Prepend an element or text node.
     * @chainable
     * @public
     * @param {Object|String} el - Element to prepend or string to become text node to prepend.
     * @returns {Object} El class instance.
     */
    prepend(el) {
        this._prepend(el);
        if (el instanceof El) this.children.unshift(el);
    }

    /**
     * Prepend without pushing to children array
     */
    _prepend(el) {
        if (!el) return this;
        if (El.isObject(el)) this.element.insertBefore(el.element || el, this.element.firstChild);
        if (El.isString(el)) this.prependHTML(el);

        return this;
    }

    /**
     * Prepend HTML
     * @chainable
     * @public
     * @param {String} html - HTML string to prepend.
     * @returns {Object} El class instance.
     */
    prependHTML(html) {
        if (!html) return this;
        this.setHTML(`${html}${this.getHTML()}`);
        return this;
    }

    /**
     * Prepend Text
     * @chainable
     * @public
     * @param {String} text - Text string to prepend.
     * @returns {Object} El class instance.
     */
    prependText(text) {
        if (!text) return this;
        this.element.insertBefore(document.createTextNode(text), this.element.firstChild);
        return this;
    }

    /**
     * Completely remove an element.
     * @public
     */
    remove() {
        this.parent().removeChild(this.element);
        this.element = null;
    }

    /**
     * Remove single attribute.
     * @chainable
     * @public
     * @param {String} attr - Attribute name to remove.
     * @returns {Object} El class instance.
     */
    removeAttribute(attr) {
        if (!attr) return this;
        this.element.removeAttribute(attr);
        return this;
    }

    /**
     * Removes a class. Accepts a space-separated string or an array.
     * @chainable
     * @public
     * @param {String|Array} myClass - ClassName(s) to remove.
     * @returns {Object} El class instance.
     */
    removeClass(myClass) {
        if (!myClass) return this;
        if (El.isArray(myClass)) myClass = myClass.join(' ');
        if (myClass.includes(' ')) {
            myClass = myClass.split(' ');
            for (let i = 0; i < myClass.length; i++) {
                if (this.element.classList) {
                    this.element.classList.remove(myClass[i]);
                } else {
                    this.element.className = this.element.className.replace(myClass[i], '').replace(/^\s+|\s+$/g, '');
                }
            }
        } else if (this.element.classList) {
            this.element.classList.remove(myClass);
        } else {
            this.element.className = this.element.className.replace(myClass, '').replace(/^\s+|\s+$/g, '');
        }

        return this;
    }

    /**
     * Transform an element value.
     * @public
     * @param {String} [returnType=object] - The type of returned value. Either 'object', 'array', or 'string'.
     * @returns {Object|Array|String} The transformed value.
     */
    serialize(returnType) {

        returnType = returnType || 'object';

        const $key = this.$key;

        const value = this.getValue() || this.getText() || this.getHTML();

        const obj = {};

        obj[$key] = value;

        switch (returnType.toLowerCase()) {
            case 'string':
                return El.toQueryString(obj);
            case 'array': {
                const arr = [];
                arr.push(obj);
                return arr;
            }
            default:
                return obj;
        }
    }

    /**
     * Set single attribute.
     * @chainable
     * @public
     * @param {String} attr - Attribute name to set.
     * @param {String} attrValue - Attribute value.
     * @returns {Object} El class instance.
     */
    setAttribute(attr, attrValue) {
        if (!El.isTruthy(attrValue)) return this;
        this.element.setAttribute(attr, attrValue);
        return this;
    }

    /**
     * Sets the html content.
     * @chainable
     * @public
     * @param {String} html - Html to set.
     * @returns {Object} El class instance.
     */
    setHTML(html) {
        if (!El.isTruthy(html)) return this;
        this.element.innerHTML = html;
        return this;
    }

    /**
     * Set the unique $key for an element.
     * @chainable
     * @public
     * @param {String} $key - $Key to set.
     * @returns {Object} El class instance.
     */
    setKey($key) {

        this.$key = $key;
        return this;
    }

    /**
     * Set styles.
     * @chainable
     * @public
     * @param {Object} styles - Styles to set.
     * @returns {Object} El class instance.
     */
    setStyles(styles) {
        if (!styles) return this;
        for (const style in styles) {
            if (styles.hasOwnProperty(style)) {
                this.element.style[style] = styles[style];
            }
        }
        return this;
    }

    /**
     * Sets the text content.
     * @chainable
     * @public
     * @param {String} text - Text to set.
     * @returns {Object} El class instance.
     */
    setText(text) {
        if (!El.isTruthy(text)) return this;
        this.element.textContent = text;
        return this;
    }

    /**
     * Sets the value property.
     * @chainable
     * @public
     * @param {String} val - value to set
     * @returns {Object} El class instance.
     */
    setValue(val) {
        if (!El.isTruthy(val)) return this;
        this.element.value = val;
        return this;
    }

    /**
     * Show element with display rule.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    show() {
        this.setStyles({
            display: '',
        });
        return this;
    }

    /**
     * Call show method, then hide for x amount of ms.
     * @chainable
     * @public
     * @param {Number} [speed=200] - Interval to show.
     * @returns {Object} El class instance.
     */
    showFor(speed = this._animationSpeed) {
        this.show();
        setTimeout(this.hide.bind(this), speed);
        return this;
    }

    /**
     * Toggles the checked state of the element.
     * @public
     * @chainable
     * @param {Boolean} state - Desired check state.
     * @returns {Object} El class instance.
     */
    toggleCheck(state) {
        this.element.checked = El.isBoolean(state) ? state : !this.element.checked;
        return this;
    }

    /**
     * Trigger an event and dispatch it. Convenient for custom events.
     * @public
     * @chainable
     * @param {String} eventName - Event to trigger.
     * @param {Object} [params] - Event options.
     * @returns {Object} El class instance.
     */
    trigger(eventName, params = {}) {

        const customEventParams = {
            bubbles: params.bubbles || false,
            cancelable: params.cancelable || false,
        };

        const event = window.CustomEvent ?
            new CustomEvent(eventName, customEventParams) :
            document.createEvent(eventName);

        if (!window.CustomEvent) {
            event.initCustomEvent(eventName, customEventParams.bubbles, customEventParams.cancelable, params);
        }

        if ('bubbles' in params) delete params.bubbles;
        if ('cancelable' in params) delete params.cancelable;

        /**
         * Attach custom data to the event, if it exists
         */
        event.data = {};

        /**
         * Any leftover properties are custom data.
         * They need to be attached to the event.
         */
        for (const prop in params) {
            if (params.hasOwnProperty(prop)) {
                event.data[prop] = params[prop];
            }
        }

        this.element.dispatchEvent(event);
        return this;
    }

    /**
     * Unbind all events associated with an element.
     * @chainable
     * @public
     * @returns {Object} El class instance.
     */
    unbindEvents() {

        const events = this.events;
        const l = events.length;
        let i = l - 1;

        if (l) {
            for (; i >= 0; i--) {
                this.off(events[i].event, events[i].handler, events[i].options);
            }
        }

        return this;

    }

    /**
     * Unbind all events associated with a namespace.
     * @param {String} namespace - Namespace to unbind.
     * @chainable
     * @private
     * @returns {Object} El class instance.
     */
    unbindNamespace(namespace) {

        const events = this.events;
        const l = events.length;
        let i = l - 1;

        if (l) {
            for (; i >= 0; i--) {
                if (events[i].namespace === namespace) {
                    this.off(events[i].event, events[i].handler, events[i].options);
                }
            }
        }

        return this;

    }

}

window.El = El;

export default El;