index.js

/* eslint-disable jsdoc/check-types */

/**
 * @typedef {Object} jLight
 *
 * @property {HTMLElement[]} elements
 * The collections elements.
 *
 * @property {number} length
 * The collections element count.
 *
 * @property {string} tagName
 * The collections first elements tag name.
 *
 * @property {cssClassCallback} addClass
 * Adds css classes to the collections elements.
 *
 * @property {cssClassCallback} removeClass
 * Removes css classes to the collections elements.
 *
 * @property {toggleCssClassCallback} toggleClass
 * Toggles css classes of the collections elements.
 *
 * @property {hasClassCallback} hasClass
 * Whether at least one of the collections elements has all of the provided classes.
 *
 * @property {cssCallback} css
 * Applies style rules to the collections elements, gets the current style rules
 * or gets the current value for a specific style property.
 *
 * @property {visibilityCallback} show
 * Shows the collections elements.
 *
 * @property {defaultCallback} hide
 * Hides the collections elements.
 *
 * @property {toggleVisibilityCallback} toggle
 * Toggles the collections elements visibility.
 *
 * @property {onCallback} on
 * Adds event handlers to elements.
 *
 * @property {onCallback} once
 * Adds event handlers to elements fo one time execution.
 *
 * @property {offCallback} off
 * Removes event handlers from elements.
 *
 * @property {delegateCallback} delegate
 * [DEPRECATED] Delegates event handlers to elements.
 *
 * @property {delegateCallback} undelegate
 * [DEPRECATED] Undelegates event handlers from elements.
 *
 * @property {triggerCallback} trigger
 * Triggers events on the collections elements.
 *
 * @property {propCallback} prop
 * Sets a property of the collections elements or gets its value.
 *
 * @property {attrCallback} attr
 * Sets an attribute or attributes to the collections elements or gets its value or values.
 *
 * @property {removeAttrCallback} removeAttr
 * Removes the supplied attributes from the collections elements.
 *
 * @property {contentCallback} text
 * Gets or sets the text content of the collections elements.
 *
 * @property {contentCallback} html
 * Gets or sets the HTML content of the collections elements.
 *
 * @property {valCallback} val
 * Gets or sets the collections elements values.
 *
 * @property {dataCallback} data
 * Gets or sets the jLight data of the collections elements.
 *
 * @property {defaultCallback} empty
 * Empties the collections elements HTML content.
 *
 * @property {cloneCallback} clone
 * Clones the collection.
 *
 * @property {elementsCallback} add
 * Adds elements to the collection.
 *
 * @property {removeCallback} remove
 * Removes elements from the collection.
 *
 * @property {elementsCallback} prepend
 * Prepends elements to the collections elements.
 *
 * @property {elementsCallback} append
 * Appends elements to the collections elements.
 *
 * @property {elementsCallback} prependTo
 * Prepends the collections elements to elements.
 *
 * @property {elementsCallback} appendTo
 * Appends the collections elements to elements.
 *
 * @property {elementsCallback} insertBefore
 * Inserts the collections elements before elements.
 *
 * @property {elementsCallback} insertAfter
 * Inserts the collections elements after elements.
 *
 * @property {elementsCallback} before
 * Inserts elements before the collections elements.
 *
 * @property {elementsCallback} after
 * Inserts elements after the collections elements.
 *
 * @property {elementsCallback} wrap
 * Wraps the collections elements in elements.
 *
 * @property {indexElementCallback} get
 * Gets the element at the supplied index from the collection.
 *
 * @property {indexjLightCallback} eq
 * Gets the jLight element at the supplied index from the collection.
 *
 * @property {defaultCallback} first
 * Gets the first jLight element from the collection.
 *
 * @property {defaultCallback} last
 * Gets the last jLight element from the collection.
 *
 * @property {defaultCallback} parent
 * Gets a jLight collection from the collections parent elements.
 *
 * @property {defaultCallback} parents
 * Gets a jLight collection from all the collections parent elements.
 *
 * @property {defaultCallback} children
 * Gets a jLight collection from the collections children elements.
 *
 * @property {defaultCallback} siblings
 * Gets a jLight collection from the collections sibling elements.
 *
 * @property {selectorCallback} prev
 * Gets a jLight collection from the collections previous sibling elements.
 *
 * @property {selectorCallback} next
 * Gets a jLight collection from the collections next sibling elements.
 *
 * @property {selectorCallback} closest
 * Gets a jLight collection from all the collections parent elements matching the selector.
 *
 * @property {elementsCallback} not
 * Gets a jLight collection from the collections elements which are not part of elements.
 *
 * @property {elementsCallback} has
 * Gets a jLight collection from the collections elements which contain elements.
 *
 * @property {filterCallback} filter
 * Gets a filtered jLight collection based on the input.
 *
 * @property {outerIteratorCallback} forEach
 * Runs a function on each of the collections elements.
 *
 * @property {outerIteratorCallback} each
 * [DEPRECATED] Runs a function on each of the collections elements.
 *
 * @property {sliceCallback} slice
 * Slices the collection.
 *
 * @property {spliceCallback} splice
 * Splices the collection.
 *
 * @property {arrayLengthCallback} push
 * Pushes elements to the collection.
 *
 * @property {defaultCallback} pop
 * Pops the last element from the collection.
 *
 * @property {defaultCallback} reverse
 * Reverses a collection.
 *
 * @property {defaultCallback} shift
 * Shifts a collection.
 *
 * @property {arrayLengthCallback} unshift
 * Unshifts a collection.
 *
 * @property {sortCallback} sort
 * Sorts a collection.
 *
 * @property {reduceCallback} reduce
 * Reduces a collection.
 *
 * @property {mapCallback} map
 * Maps a collection.
 *
 * @property {multipleElementsCallback} concat
 * Concats a collection with other collections.
 *
 * @property {elementsBooleanCallback} includes
 * Whether the collections elements include at least one of elements.
 *
 * @property {arrayBooleanCallback} some
 * Whether at least one of the collections elements meets the conditon.
 *
 * @property {arrayBooleanCallback} every
 * Whether every one of the collections elements meets the conditon.
 *
 * @property {elementsNumberCallback} indexOf
 * Gets the given elements index inside the collection.
 *
 * @property {elementsNumberCallback} lastIndexOf
 * Gets the given elements last index inside the collection.
 *
 * @property {selectorCallback} find
 * Gets a jLight collection from the collections children elements matching the selector.
 *
 * @property {getOrSetValueCallback} width
 * Gets or sets the width of the collections elements.
 *
 * @property {getOrSetValueCallback} height
 * Gets or sets the height of the collections elements.
 *
 * @property {dimensionCallback} innerWidth
 * Gets the inner width of the collections elements.
 *
 * @property {dimensionCallback} innerHeight
 * Gets the inner height of the collections elements.
 *
 * @property {outerDimensionCallback} outerWidth
 * Gets the outer width of the collections elements.
 *
 * @property {outerDimensionCallback} outerHeight
 * Gets the outer height of the collections elements.
 *
 * @property {dimensionCallback} scrollWidth
 * Gets the scroll width of the collections elements.
 *
 * @property {dimensionCallback} scrollHeight
 * Gets the scroll height of the collections elements.
 *
 * @property {getOrSetValueCallback} scrollTop
 * Gets or sets the scrollTop of the collections elements.
 *
 * @property {getOrSetValueCallback} scrollLeft
 * Gets or sets the scrollLeft of the collections elements.
 *
 * @property {offsetCallback} offset
 * Gets or sets the collections elements offset.
 *
 * @property {animateCallback} animate
 * Animates the given properties to the given values on the collections elements.
 *
 * @property {scrollToCallback} scrollTo
 * Scrolls the collections elements to elements.
 *
 * @property {defaultCallback} stop
 * Stops all running animations on the collections elements.
 *
 * @property {fadeCallback} fadeIn
 * Fades the collections elements in.
 *
 * @property {animateOutCallback} fadeOut
 * Fades the collections elements out.
 *
 * @property {fadeCallback} fadeToggle
 * Toggles the display state of the collections elements by fading.
 *
 * @property {slideCallback} slideDown
 * Slides the collections elements down.
 *
 * @property {animateOutCallback} slideUp
 * Slides the collections elements up.
 *
 * @property {slideToggleCallback} slideToggle
 * Toggles the display state of the collections elements by sliding.
 *
 * @property {isCallback} is
 * Whether a property of an element of the collection is true
 * or if an element of the collection is part of another set.
 *
 * @property {elementsBooleanCallback} contains
 * Whether the collections elements contain at least one of elements.
 *
 * @property {inViewCallback} inView
 * Checks if the collections first element is in view or runs a function if that is the case.
 *
 * @property {delayCallback} delay
 * Delays code execution.
 *
 * @property {whenCallback} when
 * Calls the supplied function with the supplied arguments if the given condition is met.
 *
 * @property {stringCallback} serialize
 * Serializes the collections elements values to a URL encoded string.
 *
 * @property {stringObjectCallback} serializeJson
 * Serializes the collections elements values to a JSON object.
 */

/**
 * @callback defaultCallback
 * @returns {jLight} jLight collection
 */

/**
 * @callback ajaxCompleteCallback
 * @param {*} [response] The requests response
 * @param {number} [status] The requests HTTP status code
 * @param {XMLHttpRequest} [request] The orginal XMLHttpRequest object
 * @returns {void} void
 */

/**
 * @callback xhrCallback
 * @param {XMLHttpRequest} [request] The orginal XMLHttpRequest object
 * @returns {XMLHttpRequest} XMLHttpRequest
 */

/**
 * @callback onStepCallback
 * @param {number} percent The eased percentage of the current time over the duration
 * @returns {void} void
 */

/**
 * @callback cssClassCallback
 * @param {string} cssClasses Space sepearated classes to supply to the function
 * @returns {jLight} jLight collection
 */

/**
 * @callback toggleCssClassCallback
 * @param {string} cssClasses Space sepearated classes to supply to the function
 * @param {boolean} [force] Force whether to add or remove classes
 * @returns {jLight} jLight collection
 */

/**
 * @callback hasClassCallback
 * @param {string} cssClasses Space sepearated classes to supply to the function
 * @returns {boolean}
 * Whether at least one of the collections elements has all of the provided classes
 */

/**
 * @callback cssCallback
 * @param {string|Object.<string, string>} property The property name or the properties to set
 * @param {string} [value] The value to set the supplied property to
 * @returns {jLight|string|CSSStyleDeclaration}
 * jLight collection, property value or elements CSSStyleDeclaration
 */

/**
 * @callback visibilityCallback
 * @param {string} [type] The css display type to apply to the function (default: 'block')
 * @returns {jLight} jLight collection
 */

/**
 * @callback toggleVisibilityCallback
 * @param {string} [type] The css display type to apply to the function (default: 'block')
 * @param {boolean} [force] Force whether to show or hide elements
 * @returns {jLight} jLight collection
 */

/**
 * @callback eventCallback
 * @param {Event} [event] The dispatched event with jLight elements for the events
 * currentTarget and target attached (event.$currentTarget and event.$target)
 */

/* eslint-disable max-len */

/**
 * @callback onCallback
 * @param {string} eventNames Space separated list of event names
 * @param {eventCallback} callbackOrSelector The function to execute when the event occurs
 * or a selector to delegate events to children of the current collections elements
 * @param {eventCallback|
 *   {capture: boolean, once: boolean, passive: boolean, signal: AbortSignal, mozSystemGroup: boolean}
 * } [delegatedCallbackOrOptions]
 * The callback to run when the event is delegated or the options to apply to the listener
 * @param {{capture: boolean, once: boolean, passive: boolean, signal: AbortSignal, mozSystemGroup: boolean}} [options]
 * The options to apply to the listener
 * @returns {jLight} jLight collection
 */

/* eslint-enable max-len */

/**
 * @callback offCallback
 * @param {string} eventNames Space separated list of event names
 * @param {Function} [callback] The function to remove from being executed when the event occurs
 * @returns {jLight} jLight collection
 */

/**
 * @callback delegateCallback
 * @param {string} eventNames Space separated list of event names
 * @param {Function} callback The function to apply
 * @returns {jLight} jLight collection
 */

/**
 * @callback triggerCallback
 * @param {string} eventNames Space separated list of event names
 * @param {*} [jLightEventData] Custom data passed to the event
 * @returns {jLight} jLight collection
 */

/**
 * @callback propCallback
 * @param {string} property The property to set or get
 * @param {boolean} [state] The state to set the property to
 * @returns {jLight|boolean}
 * jLight collection or if the property is set on at least one of the collections elements
 */

/**
 * @callback attrCallback
 * @param {string|Object.<string, *>} [attribute] The attribute or attributes to set or get
 * @param {*} [value] The value to set the attribute to
 * @returns {jLight|Object.<string, string>|boolean}
 * jLight collection, the attributes value, an object of each attribute on the
 * collections elements or if the attribute whether present on at least one of
 * the collections elements
 */

/**
 * @callback removeAttrCallback
 * @param {string|string[]} attribute The attribute or attributes to remove
 * @returns {jLight} jLight collection
 */

/**
 * @callback iteratorCallback
 * @param {jLight} [$element] The current jLight element
 * @param {number} [index] The current index
 * @returns {void} void
 */

/**
 * @callback valCallback
 * @param {boolean|null|string|boolean[]|Function|iteratorCallback} [valueOrFunction]
 * The value to set the collections elements value to.
 * If a function is supplied its return value will be set as the value.
 * If the jLight element is an HTMLSelectElement with the multiple
 * attribute each option will be passed separately to the function.
 * In that case an array of booleans can be used to set the selected options.
 * @returns {jLight|boolean|string} jLight collection or the value
 */

/**
 * @callback dataCallback
 * @param {string|Object.<string, *>} [keyOrData] The key of the data value to get or set or
 * an object of data values to set.
 * @param {string|string[]} [value] The value to set the data at the supplied key to.
 * @returns {jLight|*} jLight collection
 */

/**
 * @callback cloneCallback
 * @param {boolean} [deep] Whether to apply deep cloning of the affected nodes (default: true)
 * @returns {jLight} jLight collection
 */

/**
 * @callback elementsCallback
 * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} elements
 * The elements to apply the function to
 * @returns {jLight} jLight collection
 */

/**
 * @callback removeCallback
 * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} [elements] The elements to remove
 * @param {boolean} [removeFromDom]
 * Whether the elements should also be removed from the DOM (default: true)
 * @returns {jLight|null} jLight collection or null
 */

/**
 * @callback contentCallback
 * @param {string} [content] The content to supply to the function
 * @returns {jLight|string} jLight collection or the text content
 */

/**
 * @callback indexElementCallback
 * @param {number} index The index to supply to the function
 * @returns {HTMLElement|undefined} The element or null
 */

/**
 * @callback indexjLightCallback
 * @param {number} index The index to supply to the function
 * @returns {jLight} jLight collection
 */

/**
 * @callback selectorCallback
 * @param {string} [selector] The selector to use for matching elements
 * @returns {jLight} jLight collection
 */

/**
 * @callback elementsBooleanCallback
 * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} elements
 * The elements to supply to the function
 * @returns {boolean} If the conditon is met
 */

/**
 * @callback filterCallback
 * @param {string|iteratorCallback} callbackOrSelector
 * The selector to filter the collection by or a custom
 * function to decide which elements to filter out
 * @returns {jLight} jLight collection
 */

/**
 * @callback outerIteratorCallback
 * @param {iteratorCallback} callback The function to apply to each element
 * @returns {jLight} jLight collection
 */

/**
 * @callback sliceCallback
 * @param {number} [start] The index to start the slicing
 * @param {number} [end] The index to end the slicing
 * @returns {jLight} jLight collection
 */

/**
 * @callback spliceCallback
 * @param {number} start The index to start the splicing
 * @param {number} [deleteCount] The count of elements to delete
 * @returns {jLight} jLight collection
 */

/**
 * @callback arrayLengthCallback
 * @param {...(jLight|string|HTMLElement|HTMLCollection|NodeList)} elements
 * The elements to apply to the function.
 * @returns {number} The collections new length
 */

/**
 * @callback compareCallback
 * @param {jLight} $element1 The element to compare
 * @param {jLight} $element2 The element to compare to
 * @returns {number} The value to determine which place the element to compare will take
 */

/**
 * @callback sortCallback
 * @param {compareCallback} compareFunction The function used for sorting
 * @returns {jLight} jLight collection
 */

/**
 * @callback reduceInnerCallback
 * @param {*} accumulator The accumulated value
 * @param {jLight} $element The current element
 * @param {number} [index] The current elements index
 * @returns {*} The current reduction result
 */

/**
 * @callback reduceCallback
 * @param {reduceInnerCallback} callback The function used for reduction
 * @param {*} [initialValue] The initial value used for reduction
 * @returns {*} The reduction result
 */

/**
 * @callback mapInnerCallback
 * @param {jLight} [$element] The current element
 * @param {number} [index] The current elements index
 * @returns {jLight|string|HTMLElement|HTMLCollection|NodeList} The elements to map
 */

/**
 * @callback mapCallback
 * @param {mapInnerCallback} callback The function used for mapping
 * @returns {jLight} jLight collection
 */

/**
 * @callback multipleElementsCallback
 * @param {...(jLight|string|HTMLElement|HTMLCollection|NodeList|
 * Array.<jLight|string|HTMLElement|HTMLCollection|NodeList>)} elements
 * The elements to apply to the function.
 * @returns {jLight} jLight collection
 */

/**
 * @callback iteratorBooleanCallback
 * @param {jLight} [$element] The current jLight element
 * @param {number} [index] The current index
 * @returns {boolean} Whether the condition is met
 */

/**
 * @callback arrayBooleanCallback
 * @param {iteratorBooleanCallback} callback
 * The elements to supply to the function
 * @returns {boolean} If the conditon is met
 */

/**
 * @callback elementsNumberCallback
 * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} elements
 * The elements to supply to the function
 * @returns {number} The resulting number
 */

/**
 * @callback getOrSetValueCallback
 * @param {number} [value] The value to supply to the function
 * @returns {jLight|number} jLight collection or the value
 */

/**
 * @callback dimensionCallback
 * @returns {number} The dimension value
 */

/**
 * @callback outerDimensionCallback
 * @param {boolean} [includeMargins] Whether to include the elements margins (default: false)
 * @returns {number} The dimension value
 */

/**
 * @callback offsetCallback
 * @param {boolean|{top: number, left: number}} [value]
 * The value to set the elements offset to or if the
 * returned offset should be relative to the viewport
 * @param {boolean} [relativeToViewport]
 * Whether the offset should be set relative to the viewport (default: false)
 * @returns {jLight|{top: number, left: number}} jLight collection or elements offset
 */

/**
 * @callback animateCallback
 * @param {Object.<string, string>} [properties] The css properties and values to animate
 * @param {number} [duration] The duration for the animation in ms (default: 300)
 * @param {Function} [callback] The function to run after the animation is complete (default: noop)
 * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
 * @returns {jLight} jLight collection
 */

/**
 * @callback scrollToCallback
 * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} elements
 * The elements to scroll the collections elements to
 * @param {number} [duration] The duration of the scroll animation in ms (default: 300)
 * @param {{x: number, y: number}|{}} [offset] The offset for the target position
 * @param {Function} [callback] The function to run after the scrolling is complete
 * @returns {jLight} jLight collection
 */

/**
 * @callback animateOutCallback
 * @param {number} [duration] The duration for the animation in ms (default: 300)
 * @param {Function} [callback] The function to run after the animation is complete (default: noop)
 * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
 * @returns {jLight} jLight collection
 */

/**
 * @callback fadeCallback
 * @param {number} [duration] The duration for the animation in ms (default: 300)
 * @param {Function} [callback] The function to run after the animation is complete (default: noop)
 * @param {string} [type]
 * The css display type to apply to the collections elements (default: 'block')
 * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
 * @returns {jLight} jLight collection
 */

/**
 * @callback slideCallback
 * @param {number} [duration] The duration for the animation in ms (default: 300)
 * @param {Function} [callback] The function to run after the animation is complete (default: noop)
 * @param {string} [height] The css height value to end the sliding animation (default: 'auto')
 * @param {string} [type]
 * The css display type to apply to the collections elements (default: 'block')
 * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
 * @returns {jLight} jLight collection
 */

/**
 * @callback slideToggleCallback
 * @param {number} [duration] The duration for the animation in ms (default: 300)
 * @param {Function} [callback] The function to run after the animation is complete (default: noop)
 * @param {string} [height] The css height value to end the sliding animation (default: 'auto')
 * @param {string} [type]
 * The css display type to apply to the collections elements (default: 'block')
 * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
 * @param {boolean} [force] Force whether to slide down or up
 * @returns {jLight} jLight collection
 */

/**
 * @callback isCallback
 * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} propertyOrElements
 * The property or set to compare the collections elements to
 * @returns {boolean} If the property is set on one of the collections elements or
 * at least on of the elements is contained in the supplied elements
 */

/**
 * @callback inViewCallback
 * @param {Function|{top: number, bottom: number, left: number, right: number}} [offsetOrCallback]
 * The offset used for determining if the element is in view
 * or the function to run each time that is the case
 * @param {Function|
 *  {scrollTimer: ?number, isInView: ?Function, onEnter: ?Function, onExit: ?Function}
 * } [callbackOrOptions]
 * The function to run each time the element is in view
 * or a custom options object to define the functions behavior
 * (defaults: { scrollTimer: 100, isInView: noop, onEnter: noop, onExit: noop })
 * @returns {jLight|boolean} jLight collection or whether the collections first element is in view
 */

/**
 * @callback delayCallback
 * @param {number} [delay] The duration to delay the code execution for
 * @returns {Promise} The corresponding promise
 */

/**
 * @callback whenCallback
 * @param {boolean|Function} condition The condition to check for.
 * If a function is supplied its return value will be used for checking
 * @param {string|Function} callback The function to run when the condition is met.
 * If a string is provided it should be a valid jLight function name
 * @param {...*} [args] The arguments to supply to the given jLight function
 * @returns {jLight} jLight collection
 */

/**
 * @callback stringCallback
 * @returns {string} The resulting string
 */

/**
 * @callback stringObjectCallback
 * @returns {Object.<string, string>} The resulting JSON object
 */

/* eslint-disable jsdoc/valid-types */

/**
 * @callback
 */

/* eslint-enable jsdoc/valid-types */

const jLightGlobalElements = [];
const jLightGlobalData = [];

/**
 * @module Utility
 * @tutorial tut-utility
 */

/**
 * Provides an empty function.
 *
 * @function
 * @tutorial noop
 * @returns {void} void
 */
export const noop = () => { };

/**
 * Generates a semi-random string of length 9.
 *
 * @function
 * @tutorial uniqid
 * @returns {string} The generated string
 */
export const uniqid = () => Math.random().toString(36).substr(2, 9);

/* eslint-disable no-bitwise */

/**
 * Generates a unique number hash from a string.
 *
 * @function
 * @tutorial generateHash
 * @param {string} string The string to hash
 * @returns {number} The converted string
 */
export const generateHash = (string) => Math.abs(string.split('').reduce((hash, b) => {
  const a = ((hash << 5) - hash) + b.charCodeAt(0);

  return a & a;
}, 0));

/**
 * Checks if an object is empty.
 *
 * @function
 * @tutorial isEmptyObject
 * @param {object} object The object to check
 * @returns {boolean} If the object is empty
 */
export const isEmptyObject = (object) => object
  && object.constructor === Object
  && !Object.keys(object).length;

/**
 * Checks if two objects are the same.
 *
 * @function
 * @tutorial isSameObject
 * @param {object} object1 The object to compare
 * @param {object} object2 The object to compare to
 * @returns {boolean} If the objects are the same
 */
export const isSameObject = (object1, object2) => object1
  && object2
  && object1.constructor === Object
  && object2.constructor === Object
  && Object.is(object1, object2);

/**
 * Prevents the events default beheavior, propagation and immediate propagation
 *
 * @function
 * @tutorial preventEvent
 * @param {Event} event The event to prevent
 * @returns {void} void
 */
export const preventEvent = (event) => {
  event.preventDefault();
  event.stopPropagation();
  event.stopImmediatePropagation();
};

/**
 * Provides easing functionality using easeInOutCubic.
 *
 * @function
 * @tutorial doEasing
 * @param {number} duration The duration over which to apply the easing
 * @param {onStepCallback} onStep The function to run at each easing step
 * @param {Function} [callback] The function to run after the easing is complete
 * @returns {void} void
 */
export const doEasing = (duration, onStep, callback) => {
  let start;

  const easing = (t) => (t < 0.5
    ? 4 * t * t * t
    : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1);

  const step = (timestamp) => {
    if (!start) {
      start = timestamp;
    }

    const time = timestamp - start;

    onStep(easing(Math.min(time / duration, 1)));

    if (time < duration) {
      window.requestAnimationFrame(step);
    } else if (callback) {
      callback();
    }
  };

  window.requestAnimationFrame(step);
};

/**
 * @module String
 * @tutorial tut-string
 */

/**
 * Converts the first character of a string to lowercase.
 *
 * @function
 * @tutorial lcfirst
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const lcfirst = (string) => string.charAt(0).toLowerCase() + string.slice(1);

/**
 * Converts the first character of a string to uppercase.
 *
 * @function
 * @tutorial ucfirst
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const ucfirst = (string) => string.charAt(0).toUpperCase() + string.slice(1);

/**
 * Converts a string from camel case to kebap case.
 *
 * @function
 * @tutorial camelToKebab
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const camelToKebab = (string) => (typeof string === 'string'
  ? string.replace(/([a-z][A-Z])/g, (chars) => `${chars[0]}-${chars[1].toLowerCase()}`).toLowerCase()
  : '');

/**
 * Converts a string from camel case to snake case.
 *
 * @function
 * @tutorial camelToSnake
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const camelToSnake = (string) => (typeof string === 'string'
  ? lcfirst(string).replace(/[A-Z]/g, (char) => `_${char.toLowerCase()}`).toLowerCase()
  : '');

/**
 * Converts a string from kebap case to camel case.
 *
 * @function
 * @tutorial kebabToCamel
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const kebabToCamel = (string) => (typeof string === 'string'
  ? lcfirst(string.replace(/-([a-zA-Z])/g, (chars) => chars[1].toUpperCase()))
  : '');

/**
 * Converts a string from kebap case to snake case.
 *
 * @function
 * @tutorial kebabToSnake
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const kebabToSnake = (string) => (typeof string === 'string'
  ? string.replace(/-/g, '_').toLowerCase()
  : '');

/**
 * Converts a string from snake case to camel case.
 *
 * @function
 * @tutorial snakeToCamel
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const snakeToCamel = (string) => (typeof string === 'string'
  ? lcfirst(string.replace(/(_[a-zA-Z])/g, (chars) => chars[1].toUpperCase()))
  : '');

/**
 * Converts a string from snake case to kebap case.
 *
 * @function
 * @tutorial snakeToKebab
 * @param {string} string The string to convert
 * @returns {string} The converted string
 */
export const snakeToKebab = (string) => (typeof string === 'string'
  ? string.replace(/_/g, '-').toLowerCase()
  : '');

/* eslint-enable no-bitwise */

/**
 * @module Ajax
 * @tutorial tut-ajax
 */

/**
 * Handles asynchronous HTTP requests.
 *
 * @function
 * @tutorial ajax
 * @param {object} opts The options for the request
 * @param {string} [opts.url] The url to send the request to
 * @param {string} [opts.method] The HTTP method to use
 * @param {Object.<string, *>|string} [opts.data] The data to send
 * @param {Object.<string, string>} [opts.headers] The headers to send
 * @param {boolean} [opts.processData] Whether to process the data or not
 * @param {boolean} [opts.crossDomain] Whether to send the request cross domain or not
 * @param {boolean} [opts.async] Whether to send the request asynchronously or not
 * @param {string|false} [opts.contentType] The content type to use
 * @param {string|null} [opts.username] The username to use for authentification
 * @param {string|null} [opts.password] The password to use for authentification
 * @param {ajaxCompleteCallback} [opts.done] The callback to run if the request was successful
 * @param {ajaxCompleteCallback} [opts.fail] The callback to run if the request has failed
 * @param {ajaxCompleteCallback} [opts.always] The callback to run always
 * @param {Function} [opts.abort] The callback to run if the request was aborted
 * @param {xhrCallback} [opts.xhr] The callback to modify the XMLHttpRequest object before sending
 * @returns {XMLHttpRequest} The corresponding XMLHttpRequest object
 */
export const ajax = (opts = {}) => {
  const options = {
    url: window.location.href,
    method: 'POST',
    data: {},
    headers: {},
    processData: true,
    crossDomain: false,
    async: true,
    contentType: 'application/x-www-form-urlencoded',
    username: null,
    password: null,
    done: noop,
    fail: noop,
    always: noop,
    abort: noop,
    xhr: (request) => request,
    ...opts,
  };

  const request = options.xhr(new XMLHttpRequest());
  const { headers } = options;
  let { data } = options;

  options.method = options.method.toUpperCase();

  if (!options.crossDomain && !headers['X-Requested-With']) {
    headers['X-Requested-With'] = 'XMLHttpRequest';
  }

  if (!headers['Content-Type']) {
    headers['Content-Type'] = options.contentType;
  }

  if (headers['Content-Type'] === false) {
    delete headers['Content-Type'];
  }

  if (options.processData && typeof data !== 'string') {
    if (headers['Content-Type'] === 'application/json') {
      data = JSON.stringify(data);
    } else if (data && typeof data === 'object') {
      const params = [];

      Object.entries(data).forEach(([theKey, value]) => {
        const key = theKey;

        if (Array.isArray(value)) {
          value.forEach((item) => {
            params.push(`${key}[]=${item}`);
          });
        } else {
          params.push(`${key}=${value}`);
        }
      });

      data = params.join('&');
    }
  }

  request.open(
    options.method,
    `${options.url}${options.method === 'GET' ? `?${data}` : ''}`,
    options.async,
    options.username,
    options.password,
  );

  Object.entries(headers).forEach(([key, value]) => {
    request.setRequestHeader(key, value);
  });

  const fail = () => options.fail(
    request.response,
    request.status,
    request,
  );

  request.onerror = fail;
  request.ontimeout = fail;
  request.onabort = options.abort;

  request.onreadystatechange = () => {
    if (request.readyState === XMLHttpRequest.DONE) {
      if (request.status && request.status >= 200 && request.status < 300) {
        const isJsonResponse = (request.getResponseHeader('content-type') || '')
          .includes('application/json');

        options.done(
          isJsonResponse
            ? JSON.parse(request.response)
            : request.response,
          request.status,
          request,
        );
      } else if (request.status) {
        fail();
      }

      options.always(
        request.response,
        request.status,
        request,
      );
    }
  };

  request.send(options.method === 'POST' ? data : null);

  return request;
};

/**
 * Handles asynchronous GET requests.
 *
 * @function
 * @tutorial ajax-get
 * @param {string} url The url for the request
 * @param {object} opts The options for the request
 * @param {Object.<string, *>|string} [opts.data] The data to send
 * @param {Object.<string, string>} [opts.headers] The headers to send
 * @param {boolean} [opts.processData] Whether to process the data or not
 * @param {boolean} [opts.crossDomain] Whether to send the request cross domain or not
 * @param {boolean} [opts.async] Whether to send the request asynchronously or not
 * @param {string|false} [opts.contentType] The content type to use
 * @param {string|null} [opts.username] The username to use for authentification
 * @param {string|null} [opts.password] The password to use for authentification
 * @param {ajaxCompleteCallback} [opts.done] The callback to run if the request was successful
 * @param {ajaxCompleteCallback} [opts.fail] The callback to run if the request has failed
 * @param {ajaxCompleteCallback} [opts.always] The callback to run always
 * @param {Function} [opts.abort] The callback to run if the request was aborted
 * @param {xhrCallback} [opts.xhr] The callback to modify the XMLHttpRequest object before sending
 * @returns {XMLHttpRequest} The corresponding XMLHttpRequest object
 */
export const get = (url, opts = {}) => ajax({
  ...opts,
  method: 'GET',
  url,
});

/**
 * Handles asynchronous POST requests.
 *
 * @function
 * @tutorial ajax-post
 * @param {string} url The url for the request
 * @param {object} opts The options for the request
 * @param {Object.<string, *>|string} [opts.data] The data to send
 * @param {Object.<string, string>} [opts.headers] The headers to send
 * @param {boolean} [opts.processData] Whether to process the data or not
 * @param {boolean} [opts.crossDomain] Whether to send the request cross domain or not
 * @param {boolean} [opts.async] Whether to send the request asynchronously or not
 * @param {string|false} [opts.contentType] The content type to use
 * @param {string|null} [opts.username] The username to use for authentification
 * @param {string|null} [opts.password] The password to use for authentification
 * @param {ajaxCompleteCallback} [opts.done] The callback to run if the request was successful
 * @param {ajaxCompleteCallback} [opts.fail] The callback to run if the request has failed
 * @param {ajaxCompleteCallback} [opts.always] The callback to run always
 * @param {Function} [opts.abort] The callback to run if the request was aborted
 * @param {xhrCallback} [opts.xhr] The callback to modify the XMLHttpRequest object before sending
 * @returns {XMLHttpRequest} The corresponding XMLHttpRequest object
 */
export const post = (url, opts = {}) => ajax({
  method: 'POST',
  url,
  ...opts,
});

const initalizeJLightElementData = (element, selector) => {
  if (jLightGlobalElements.indexOf(element) > -1) {
    return;
  }

  jLightGlobalElements.push(element);

  jLightGlobalData[jLightGlobalElements.length - 1] = {
    jLightInternal: { selector },
  };
};

const removeJLightElementData = (element) => {
  const elementIndex = jLightGlobalElements.indexOf(element);

  if (elementIndex < 0) {
    return;
  }

  jLightGlobalElements.splice(elementIndex, 1);
  jLightGlobalData.splice(elementIndex, 1);
};

const getJLightElementData = (element) => {
  const elementIndex = jLightGlobalElements.indexOf(element);

  if (elementIndex > -1) {
    return jLightGlobalData[elementIndex];
  }

  initalizeJLightElementData(element, element);

  return getJLightElementData(element);
};

const setJLightElementData = (element, key, value) => {
  const elementIndex = jLightGlobalElements.indexOf(element);

  if (elementIndex > -1) {
    jLightGlobalData[elementIndex][key] = value;

    return;
  }

  initalizeJLightElementData(element, element);
  setJLightElementData(element, key, value);
};

const updateJLightElementData = (element, data) => {
  const elementIndex = jLightGlobalElements.indexOf(element);

  if (elementIndex > -1) {
    jLightGlobalData[elementIndex] = {
      ...jLightGlobalData[elementIndex],
      ...data,
    };

    return;
  }

  initalizeJLightElementData(element, element);
  updateJLightElementData(element, data);
};

const addJLightElementEventData = (element, type, callback, realCallback) => {
  const jLightElementData = getJLightElementData(element);
  const events = jLightElementData.jLightInternal.events || [];

  events.push({
    type,
    callback,
    realCallback,
  });

  updateJLightElementData(element, {
    jLightInternal: {
      ...jLightElementData.jLightInternal,
      events,
    },
  });
};

const removeJLightElementEventData = (element, type, callback, realCallback) => {
  const jLightElementData = getJLightElementData(element);
  const events = jLightElementData.jLightInternal.events || [];

  updateJLightElementData(element, {
    jLightInternal: {
      ...jLightElementData.jLightInternal,
      events: events.filter((event) => event.type !== type
        || event.callback !== callback
        || event.realCallback !== realCallback),
    },
  });
};

const createElementsFromString = (string) => {
  const div = document.createElement('div');

  div.innerHTML = string.trim();

  const { children } = div;

  [...children].forEach((theChild) => {
    let child = theChild;

    if (!(child instanceof HTMLElement)) {
      const fallbackDiv = document.createElement('div');

      fallbackDiv.textContent = child.textContent;
      child = fallbackDiv;
    }

    initalizeJLightElementData(child, child.tagName.toLowerCase());
  });

  return children;
};

const getElementsFromArgument = (argument) => {
  if (argument.elements) {
    return argument.elements;
  }

  if (typeof argument === 'string') {
    return argument.match(/<(.|\n)+>/)
      ? [...createElementsFromString(argument)]
      : [...document.querySelectorAll(argument)];
  }

  if (argument instanceof HTMLElement) {
    return [argument];
  }

  if (argument instanceof HTMLCollection || argument instanceof NodeList) {
    return [...argument];
  }

  return [];
};

const getPrevMatchingElement = (element, selector) => {
  const prev = element.previousElementSibling;

  if (prev && prev.matches(selector)) {
    return prev;
  }

  if (prev) {
    return getPrevMatchingElement(prev, selector);
  }

  return null;
};

const getNextMatchingElement = (element, selector) => {
  const next = element.nextElementSibling;

  if (next && next.matches(selector)) {
    return next;
  }

  if (next) {
    return getNextMatchingElement(next, selector);
  }

  return null;
};

const getParents = (elements, all, isRecursing) => {
  const parents = [];

  elements.forEach(({ parentElement }) => {
    if (!parents.includes(parentElement)) {
      if (isRecursing && elements.includes(parentElement)) {
        return;
      }

      parents.push(parentElement);
    }
  });

  if (all && !parents.includes(document.documentElement)) {
    return [...parents, ...getParents(parents, all, true)];
  }

  return parents;
};

const getClosestMatchingElement = (element, selector) => {
  const closest = element.parentElement;

  // eslint-disable-next-line no-nested-ternary
  return closest
    ? closest.matches(selector)
      ? closest
      : getClosestMatchingElement(closest, selector)
    : undefined;
};

const canBeSeralized = (element) => (element.name
  && (element instanceof HTMLInputElement
    || element instanceof HTMLTextAreaElement
    || element instanceof HTMLSelectElement));

const addValueToJson = (element, theSerializedJson) => {
  const serializedJson = theSerializedJson;

  if (element.getAttribute('type') === 'checkbox') {
    serializedJson[element.name] = element.checked;
  } else {
    serializedJson[element.name] = element.value;
  }

  return serializedJson;
};

const getUpdateAnimationId = (element) => {
  const jLightElementData = getJLightElementData(element).jLightInternal || {};
  const animationId = uniqid();

  updateJLightElementData(element, {
    jLightInternal: {
      ...jLightElementData,
      currentAnimation: animationId,
    },
  });

  return animationId;
};

const attachListener = (
  $,
  elements,
  eventNames,
  callbackOrSelector,
  delegatedCallbackOrOptions,
  theOptions = {},
) => {
  let types = [eventNames];

  if (types[0].indexOf(' ') > -1) {
    types = types[0].split(' ');
  }

  const hasDelegatedCallback = typeof delegatedCallbackOrOptions === 'function';
  const options = (hasDelegatedCallback ? theOptions : delegatedCallbackOrOptions) || {};
  const delegatedCallback = hasDelegatedCallback ? delegatedCallbackOrOptions : noop;

  if (typeof callbackOrSelector === 'function' || callbackOrSelector === false) {
    elements.forEach((element) => {
      const callback = (theEvent) => {
        const event = theEvent;

        event.$target = $([event.target]);
        event.$currentTarget = $([event.currentTarget]);

        if (callbackOrSelector === false
          || callbackOrSelector(event, event.jLightEventData) === false
          || delegatedCallback === false) {
          preventEvent(event);
        }
      };

      types.forEach((type) => {
        addJLightElementEventData(element, type, callbackOrSelector, callback);

        element.addEventListener(type, (event) => {
          if (options.once) {
            removeJLightElementEventData(element, type, callbackOrSelector, callback);
          }

          return callback(event);
        }, options);
      });
    });
  } else {
    types.forEach((type) => {
      elements.forEach((element) => {
        const callback = (theEvent) => {
          const event = theEvent;

          if (element.contains(event.target) && event.target.matches(callbackOrSelector)) {
            event.$target = $([event.target]);
            event.$currentTarget = $([event.currentTarget]);

            if (delegatedCallback === false
              || delegatedCallback(event, event.jLightEventData) === false) {
              preventEvent(event);
            }
          }

          if (options.once) {
            removeJLightElementEventData(element, type, delegatedCallback, callback);
          }
        };

        addJLightElementEventData(element, type, delegatedCallback, callback);
        element.addEventListener(type, callback, options);
      });
    });
  }

  return $(elements);
};

const indexOfOfLastIndexOf = (identifier, elements, $elements) => {
  const theElements = getElementsFromArgument($elements);
  const isLast = identifier === 'lastIndexOf';
  let index = -1;

  theElements.forEach((referenceElement) => {
    if ((index < 0 && !isLast) || isLast) {
      index = elements[identifier](referenceElement);
    }
  });

  return index;
};

const getOrSetDimension = (identifier, $elements, value) => {
  const { elements } = $elements;

  if (value !== undefined) {
    elements.forEach((theElement) => {
      const element = theElement;

      element.style[identifier] = `${value}${typeof value !== 'string' ? 'px' : ''}`;
    });

    return $elements;
  }

  const upperIdentifier = ucfirst(identifier);
  let dimension;

  elements.forEach((element) => {
    if (dimension === undefined) {
      dimension = Math.max(
        element[`client${upperIdentifier}`],
        element[`offset${upperIdentifier}`],
      );
    }
  });

  return dimension;
};

const getDimension = (identifier, elements, includeMargins) => {
  let dimension = 0;
  let spacingOne;
  let spacingTwo;
  let functionSuffix;

  switch (identifier) {
    case 'innerWidth':
      spacingOne = 'border-left';
      spacingTwo = 'border-right';
      functionSuffix = 'Width';

      break;
    case 'innerHeight':
      spacingOne = 'border-top';
      spacingTwo = 'border-bottom';
      functionSuffix = 'Height';

      break;
    case 'outerWidth':
      spacingOne = 'margin-left';
      spacingTwo = 'margin-right';
      functionSuffix = 'Width';

      break;
    case 'outerHeight':
      spacingOne = 'margin-top';
      spacingTwo = 'margin-bottom';
      functionSuffix = 'Height';

      break;
    default:
      break;
  }

  elements.forEach((element) => {
    if (dimension) {
      return;
    }

    let spacingLeftOrTop = 0;
    let spacingRightOrBottom = 0;
    const computedStyles = window.getComputedStyle(element);
    const isInner = identifier.indexOf('inner') > -1;

    if (isInner || includeMargins) {
      const sign = isInner ? -1 : 1;

      spacingLeftOrTop = sign * parseFloat(computedStyles.getPropertyValue(spacingOne), 10);
      spacingRightOrBottom = sign * parseFloat(computedStyles.getPropertyValue(spacingTwo), 10);
    }

    dimension = Math.max(
      element[`client${functionSuffix}`],
      element[`offset${functionSuffix}`],
    ) + spacingLeftOrTop + spacingRightOrBottom;
  });

  return dimension;
};

const getOrSetTextOrHtml = (identifier, $elements, theValue) => {
  const { elements } = $elements;
  let value = theValue;

  if (value || typeof value === 'number' || typeof value === 'string' || value === null) {
    elements.forEach((theElement, index) => {
      const element = theElement;

      if (typeof value === 'function') {
        value = theValue(index, element[identifier]);
      }

      element[identifier] = value === 0 ? '0' : (value || '');
    });

    return $elements;
  }

  elements.forEach((element) => {
    if (value === undefined) {
      value = element[identifier];
    }
  });

  return value;
};

const modifyClasses = (identifier, $elements, cssString, force) => {
  const isToggle = identifier === 'toggle';
  let cssClasses = [cssString];

  if (cssString.indexOf(' ') > -1) {
    cssClasses = cssString.split(' ');
  }

  $elements.elements.forEach((element) => {
    cssClasses.forEach((cssClass) => {
      if (isToggle) {
        element.classList[identifier](cssClass, force);
      } else {
        element.classList[identifier](cssClass);
      }
    });
  });

  return $elements;
};

const prependOrAppend = (identifier, $elements, elements) => {
  const theElements = getElementsFromArgument($elements);

  elements.forEach((element) => {
    theElements.forEach((elementToInsert) => {
      if (element !== elementToInsert) {
        element[identifier](elementToInsert);
      }
    });
  });

  return elements;
};

const prependToOrAppendTo = (identifier, $elements, elements) => {
  const theElements = getElementsFromArgument($elements);

  elements.forEach((element) => {
    theElements.forEach((elementToInsertTo) => {
      if (element !== elementToInsertTo) {
        elementToInsertTo[identifier](element);
      }
    });
  });

  return elements;
};

const insertBeforeOrInsertAfter = (identifier, $elements, elements, type) => {
  const theElements = getElementsFromArgument($elements);

  elements.forEach((element) => {
    theElements.forEach((referenceElement) => {
      if (element !== referenceElement) {
        if (type === 'insert') {
          referenceElement.insertAdjacentElement(identifier, element);
        } else {
          element.insertAdjacentElement(identifier, referenceElement);
        }
      }
    });
  });

  return elements;
};

const getPrevOrNextElements = (identifier, selector, elements) => {
  const theElements = [];

  if (!selector) {
    elements.forEach((element) => {
      const theElement = element[identifier];

      if (theElement && !theElements.includes(theElement)) {
        theElements.push(theElement);
      }
    });
  } else {
    const isPrev = identifier === 'previousElementSibling';
    let theElement;

    elements.forEach((element) => {
      if (isPrev) {
        theElement = getPrevMatchingElement(element, selector);
      } else {
        theElement = getNextMatchingElement(element, selector);
      }

      if (theElement && !theElements.includes(theElement)) {
        theElements.push(theElement);
      }
    });
  }

  return theElements;
};

const getScrollWidthOrScrollHeight = (identifier, elements) => {
  let dimension = 0;

  elements.forEach((element) => {
    if (dimension === undefined) {
      dimension = element[identifier];
    }
  });

  return dimension;
};

const getOrSetScrollTopOrScrollLeft = (identifier, value, $elements) => {
  const { elements } = $elements;

  if (value !== undefined) {
    elements.forEach((theElement) => {
      const element = theElement;

      element[identifier] = parseFloat(value, 10);
    });

    return $elements;
  }

  let scrollValue;

  elements.forEach((element) => {
    if (scrollValue === undefined) {
      scrollValue = element[identifier];
    }
  });

  return scrollValue;
};

const isInView = (boundingBox, offset) => boundingBox.top >= parseFloat(offset.top, 10)
  && boundingBox.left >= parseFloat(offset.left, 10)
  && boundingBox.bottom <= (window.innerHeight || document.documentElement.clientHeight)
  + parseFloat(offset.bottom, 10)
  && boundingBox.right <= (window.innerWidth || document.documentElement.clientWidth)
  + parseFloat(offset.right, 10);

const $ = (elements) => ({
  ...(() => {
    const theElements = {};

    elements.forEach((element, index) => {
      theElements[index] = element;
    });

    return theElements;
  })(),

  /**
   * @module Properties
   * @tutorial tut-properties
   */

  /**
   * The collections elements.
   *
   * @tutorial tut-properties
   * @type {HTMLElement[]}
   */
  elements,

  /**
   * The collections element count.
   *
   * @tutorial tut-properties
   * @type {number}
   */
  length: elements.filter((element) => element).length,

  /**
   * The collections first elements tag name.
   *
   * @tutorial tut-properties
   * @type {string}
   */
  tagName: elements[0] ? elements[0].tagName : undefined,

  /**
   * @module CSS
   * @tutorial tut-css
   */

  /**
   * Adds css classes to the collections elements.
   *
   * @function
   * @tutorial addClass
   * @param {string} cssClasses Space sepearated classes to add
   * @returns {jLight} jLight collection
   */
  addClass: (cssClasses) => modifyClasses('add', $(elements), cssClasses),

  /**
   * Removes css classes to the collections elements.
   *
   * @function
   * @tutorial removeClass
   * @param {string} cssClasses Space sepearated classes to remove
   * @returns {jLight} jLight collection
   */
  removeClass: (cssClasses) => modifyClasses('remove', $(elements), cssClasses),

  /**
   * Toggles css classes of the collections elements.
   *
   * @function
   * @tutorial toggleClass
   * @param {string} cssClasses Space sepearated classes to toggle
   * @param {boolean} [force] Force whether to add or remove classes
   * @returns {jLight} jLight collection
   */
  toggleClass: (cssClasses, force) => modifyClasses('toggle', $(elements), cssClasses, force),

  /**
   * Whether at least one of the collections elements has all of the provided classes.
   *
   * @function
   * @tutorial hasClass
   * @param {string} cssClasses Space sepearated classes to check for
   * @returns {boolean}
   * Whether at least one of the collections elements has all of the provided classes
   */
  hasClass: (cssClasses) => {
    let theCssClasses = [cssClasses];
    let hasClass;

    if (cssClasses.indexOf(' ') > -1) {
      theCssClasses = cssClasses.split(' ');
    }

    elements.forEach((element) => {
      if (hasClass !== undefined) {
        return;
      }

      let matchCount = 0;

      theCssClasses.forEach((cssClass) => {
        matchCount += element.classList.contains(cssClass) ? 1 : 0;
      });

      if (matchCount === theCssClasses.length) {
        hasClass = true;
      }
    });

    return hasClass || false;
  },

  /**
   * Applies style rules to the collections elements, gets the current style rules or
   * gets the current value for a specific style property.
   *
   * @function
   * @tutorial css
   * @param {string|Object.<string, string>} property The property name or the properties to set
   * @param {string} [value] The value to set the supplied property to
   * @returns {jLight|string|CSSStyleDeclaration}
   * jLight collection, property value or elements CSSStyleDeclaration
   */
  css: (property, value) => {
    if (typeof property === 'object' && property !== null) {
      elements.forEach((theElement) => {
        const element = theElement;

        Object.entries(property).forEach(([key, val]) => {
          element.style[key] = val;
        });
      });

      return $(elements);
    }

    if (value !== undefined) {
      elements.forEach((theElement) => {
        const element = theElement;

        element.style[property] = value;
      });

      return $(elements);
    }

    const computedStyles = window.getComputedStyle(elements[0]);

    if (property) {
      return computedStyles.getPropertyValue(property);
    }

    return computedStyles;
  },

  /**
   * Shows the collections elements.
   *
   * @function
   * @tutorial show
   * @param {string} [type]
   * The css display type to apply to the collections elements (default: 'block')
   * @returns {jLight} jLight collection
   */
  show: (type = 'block') => {
    elements.forEach((theElement) => {
      const element = theElement;

      element.style.display = type;
    });

    return $(elements);
  },

  /**
   * Hides the collections elements.
   *
   * @function
   * @tutorial hide
   * @returns {jLight} jLight collection
   */
  hide: () => {
    elements.forEach((theElement) => {
      const element = theElement;

      element.style.display = 'none';
    });

    return $(elements);
  },

  /**
   * Toggles the collections elements visibility.
   *
   * @function
   * @tutorial toggle
   * @param {string} [type] The css display type to apply to the function (default: 'block')
   * @param {boolean} [force] Force whether to show or hide elements
   * @returns {jLight} jLight collection
   */
  toggle: (type = 'block', force) => {
    const forceDefined = force !== undefined;
    const forceShow = forceDefined && force;
    const forceHide = forceDefined && !force;

    elements.forEach((theElement) => {
      const element = theElement;
      const computedStyles = window.getComputedStyle(element);

      if (forceShow || (computedStyles.getPropertyValue('display') === 'none' && !forceHide)) {
        element.style.display = type;
      } else {
        element.style.display = 'none';
      }
    });

    return $(elements);
  },

  /**
   * @module Event
   * @tutorial tut-event
   */

  /* eslint-disable max-len */

  /**
   * Adds event handlers to elements.
   *
   * @function
   * @tutorial on
   * @param {string} eventNames Space separated list of event names
   * @param {eventCallback} callbackOrSelector The function to execute when the event occurs
   * or a selector to delegate events to children of the current collections elements
   * @param {eventCallback|
   *   {capture: boolean, once: boolean, passive: boolean, signal: AbortSignal, mozSystemGroup: boolean}
   * } [delegatedCallbackOrOptions]
   * The callback to run when the event is delegated or the options to apply to the listener
   * @param {{capture: boolean, once: boolean, passive: boolean, signal: AbortSignal, mozSystemGroup: boolean}} [options]
   * The options to apply to the listener
   * @returns {jLight} jLight collection
   */
  on: (eventNames, callbackOrSelector, delegatedCallbackOrOptions, options) => attachListener(
    $,
    elements,
    eventNames,
    callbackOrSelector,
    delegatedCallbackOrOptions,
    options,
  ),

  /**
   * Adds event handlers to elements fo one time execution.
   *
   * @function
   * @tutorial on
   * @param {string} eventNames Space separated list of event names
   * @param {eventCallback} callbackOrSelector The function to execute when the event occurs
   * or a selector to delegate events to children of the current collections elements
   * @param {eventCallback|
   *   {capture: boolean, once: boolean, passive: boolean, signal: AbortSignal, mozSystemGroup: boolean}
   * } [delegatedCallbackOrOptions]
   * The callback to run when the event is delegated or the options to apply to the listener
   * @param {{capture: boolean, once: boolean, passive: boolean, signal: AbortSignal, mozSystemGroup: boolean}} [options]
   * The options to apply to the listener
   * @returns {jLight} jLight collection
   */
  once: (eventNames, callbackOrSelector, delegatedCallbackOrOptions, options) => attachListener(
    $,
    elements,
    eventNames,
    callbackOrSelector,
    delegatedCallbackOrOptions,
    {
      ...options,
      once: true,
    },
  ),

  /* eslint-enable max-len */

  /**
   * Removes event handlers from elements.
   *
   * @function
   * @tutorial off
   * @param {string} eventNames Space separated list of event names
   * @param {Function} [callback] The function to remove from being executed when the event occurs
   * @returns {jLight} jLight collection
   */
  off: (eventNames, callback) => {
    let types = [eventNames];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    elements.forEach((element) => {
      const jLightElementData = getJLightElementData(element);
      const events = jLightElementData.jLightInternal.events || [];

      types.forEach((type) => {
        events.forEach((event) => {
          if (event.type === type && (!callback || event.callback === callback)) {
            removeJLightElementEventData(element, type, callback, event.realCallback);
            element.removeEventListener(type, event.realCallback);
          }
        });
      });
    });

    return $(elements);
  },

  /**
   * [DEPRECATED] Delegates event handlers to elements.
   *
   * @deprecated
   * Will be removed in version 1.2.0,
   * use {@link jLight jLight}.{@link module:Event~on on} instead
   * @function
   * @tutorial delegate
   * @param {string} eventNames Space separated list of event names
   * @param {Function} callback The function to execute when the event occurs
   * @returns {jLight} jLight collection
   */
  delegate: (eventNames, callback) => {
    // eslint-disable-next-line no-console
    console.warn('jLight.delegate is deprecated an will be removed in version 1.2.0, use jLight.on instead');

    let types = [eventNames];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    const realCallback = (theEvent) => {
      const event = theEvent;
      const jLightElementData = getJLightElementData(elements[0]);
      const { selector } = jLightElementData.jLightInternal;

      if (!selector) {
        return;
      }

      if (event.target.matches(selector)) {
        event.$target = $([event.target]);
        event.$currentTarget = $([event.currentTarget]);

        if (callback === false
          || callback(event, event.jLightEventData) === false) {
          preventEvent(event);
        }
      }
    };

    types.forEach((type) => {
      addJLightElementEventData(document, type, callback, realCallback);
      document.addEventListener(type, realCallback);
    });

    return $(elements);
  },

  /**
   * [DEPRECATED] Undelegates event handlers from elements.
   *
   * @deprecated
   * Will be removed in version 1.2.0,
   * use {@link jLight jLight}.{@link module:Event~off off} instead
   * @function
   * @tutorial delegate
   * @param {string} eventNames Space separated list of event names
   * @param {Function} callback The function to remove
   * @returns {jLight} jLight collection
   */
  undelegate: (eventNames, callback) => {
    // eslint-disable-next-line no-console
    console.warn('jLight.undelegate is deprecated an will be removed in version 1.2.0, use jLight.off instead');

    const jLightElementData = getJLightElementData(document);
    const events = jLightElementData.jLightInternal.events || [];
    let types = [eventNames];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    types.forEach((type) => {
      events.forEach((event) => {
        if (event.type === type && event.callback === callback) {
          removeJLightElementEventData(document, type, callback, event.realCallback);
          document.removeEventListener(type, event.realCallback);
        }
      });
    });

    return $(elements);
  },

  /**
   * Triggers events on the collections elements.
   *
   * @function
   * @tutorial trigger
   * @param {string} eventNames Space separated list of event names
   * @param {*} [jLightEventData] Custom data passed to the event
   * @returns {jLight} jLight collection
   */
  trigger: (eventNames, jLightEventData) => {
    const nativeTypes = ['click', 'focus', 'focusin', 'blur', 'focusout'];
    let types = [eventNames];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    types.forEach((type) => {
      if (!jLightEventData && nativeTypes.includes(type)) {
        elements.forEach((element) => {
          if (type === 'click') {
            element.click();
          } else if (type === 'focus' || type === 'focusin') {
            element.focus();
          } else if (type === 'blur' || type === 'focusout') {
            element.blur();
          }
        });

        return;
      }

      const event = document.createEvent('Event');

      event.jLightEventData = jLightEventData;
      event.initEvent(type, true, true);

      elements.forEach((element) => {
        element.dispatchEvent(event);
      });
    });

    return $(elements);
  },

  /**
   * @module ElementData
   * @tutorial tut-element-data
   */

  /**
   * Sets a property of the collections elements or gets its value.
   *
   * @function
   * @tutorial prop
   * @param {string} property The property to set or get
   * @param {boolean} [state] The state to set the property to
   * @returns {jLight|boolean}
   * jLight collection or if the property is set on at least one of the collections elements
   */
  prop: (property, state) => {
    if (state === undefined) {
      return elements[0][property];
    }

    elements.forEach((theElement) => {
      const element = theElement;

      if (element[property] !== undefined) {
        element[property] = state;
      }
    });

    return $(elements);
  },

  /**
   * Sets an attribute or attributes to the collections elements or gets its value or values.
   *
   * @function
   * @tutorial attr
   * @param {string|Object.<string, *>} [attribute] The attribute or attributes to set or get
   * @param {*} [value] The value to set the attribute to
   * @returns {jLight|Object.<string, string>|boolean}
   * jLight collection, the attributes value, an object of each attribute on the
   * collections elements or if the attribute whether present on at least one of
   * the collections elements
   */
  attr: (attribute, value) => {
    if (typeof attribute === 'object' && attribute !== null) {
      Object.entries(attribute).forEach(([key, theValue]) => {
        elements.forEach((element) => {
          element.setAttribute(camelToKebab(key), `${theValue}`);
        });
      });

      return $(elements);
    }

    if (attribute === undefined) {
      const attrs = {};

      elements.forEach((element) => {
        [...element.attributes].forEach((attr) => {
          if (!attrs[attr]) {
            attrs[attr.name] = attr.value || true;
          }
        });
      });

      return attrs;
    }

    if (value === undefined) {
      let attr;

      elements.forEach((element) => {
        if (attr === undefined && element.hasAttribute(attribute)) {
          attr = element.getAttribute(attribute) || true;
        }
      });

      return attr;
    }

    elements.forEach((element) => {
      element.setAttribute(attribute, `${value}`);
    });

    return $(elements);
  },

  /**
   * Removes the supplied attributes from the collections elements.
   *
   * @function
   * @tutorial removeAttr
   * @param {string|string[]} attribute The attribute or attributes to remove
   * @returns {jLight} jLight collection
   */
  removeAttr: (attribute) => {
    elements.forEach((element) => {
      if (Array.isArray(attribute)) {
        attribute.forEach((attr) => {
          element.removeAttribute(attr);
        });
      } else {
        element.removeAttribute(attribute);
      }
    });

    return $(elements);
  },

  /**
   * Gets or sets the text content of the collections elements.
   *
   * @function
   * @tutorial text
   * @param {string} [text] The text to supply to the function
   * @returns {jLight|string} jLight collection or the text content
   */
  text: (text) => getOrSetTextOrHtml('textContent', $(elements), text),

  /**
   * Gets or sets the HTML content of the collections elements.
   *
   * @function
   * @tutorial html
   * @param {string} [html] The html to supply to the function
   * @returns {jLight|string} jLight collection or the html content
   */
  html: (html) => getOrSetTextOrHtml('innerHTML', $(elements), html),

  /**
   * Gets or sets the collections elements values.
   *
   * @function
   * @tutorial val
   * @param {boolean|null|string|boolean[]|Function|iteratorCallback} [valueOrFunction]
   * The value to set the collections elements value to.
   * If a function is supplied its return value will be set as the value.
   * If the jLight element is an HTMLSelectElement with the multiple
   * attribute each option will be passed separately to the function.
   * In that case an array of booleans can be used to set the selected options.
   * @returns {jLight|boolean|string} jLight collection or the value
   */
  val: (valueOrFunction) => {
    if (valueOrFunction !== undefined) {
      const isFunction = typeof valueOrFunction === 'function';

      elements.forEach((theElement) => {
        const element = theElement;

        if (element.getAttribute('type') === 'checkbox') {
          element.checked = isFunction
            ? valueOrFunction()
            : !!valueOrFunction;
        } else if (element.tagName === 'SELECT' && element.multiple) {
          [...element.querySelectorAll('option')].forEach((theOption, index) => {
            const option = theOption;
            let selected = false;

            if (Array.isArray(valueOrFunction)) {
              selected = !!valueOrFunction[index];
            } else if (isFunction) {
              selected = !!valueOrFunction($(getElementsFromArgument(option)), index);
            } else {
              selected = !!valueOrFunction;
            }

            if (selected) {
              option.setAttribute('selected', 'selected');
            } else {
              option.removeAttribute('selected');
            }

            option.selected = selected;
          });
        } else {
          element.value = isFunction
            ? valueOrFunction()
            : valueOrFunction;
        }
      });

      return $(elements);
    }

    let value;

    elements.forEach((element) => {
      if (value === undefined) {
        if (element.getAttribute('type') === 'checkbox') {
          value = element.checked;
        } else if (element.tagName === 'SELECT' && element.multiple) {
          value = [...element.querySelectorAll('option')]
            .map((option) => (option.selected ? option.value : null))
            .filter((theValue) => !!theValue);

          if (!value.length) {
            value = '';
          }
        } else {
          value = element.value;
        }
      }
    });

    return value;
  },

  /**
   * Gets or sets the jLight data of the collections elements.
   *
   * @function
   * @tutorial data
   * @param {string|Object.<string, *>} [keyOrData] The key of the data value to get or set or
   * an object of data values to set.
   * @param {string|string[]} [value] The value to set the data at the supplied key to.
   * @returns {jLight|*} jLight collection
   */
  data: (keyOrData, value) => {
    if (typeof keyOrData === 'object' && keyOrData !== null) {
      const newData = keyOrData;

      if (newData.jLightInternal) {
        delete newData.jLightInternal;
      }

      elements.forEach((element) => {
        updateJLightElementData(element, { ...newData });
      });

      return $(elements);
    }

    const key = kebabToCamel(keyOrData);

    if (key === 'jLightInternal') {
      return {};
    }

    if (value !== undefined) {
      elements.forEach((element) => {
        setJLightElementData(element, key, value);
      });

      return $(elements);
    }

    let data;

    elements.forEach((element) => {
      if (!data) {
        const jLightElementData = getJLightElementData(element);

        if (!keyOrData) {
          data = jLightElementData;

          Object.entries(element.dataset).forEach(([dataKey, dataValue]) => {
            if (!data[dataKey]) {
              if (!Number.isNaN(Number(dataValue))) {
                data[dataKey] = parseFloat(dataValue, 10);

                if (Number.isNaN(data[dataKey])) {
                  data[dataKey] = undefined;
                }
              } else {
                data[dataKey] = dataValue;
              }
            }
          });
        } else {
          data = jLightElementData[key];

          if (data === undefined) {
            data = element.getAttribute(`data-${keyOrData}`)
              || element.getAttribute(`data-${key}`);

            if (!data
              && (element.hasAttribute(`data-${keyOrData}`)
                || element.hasAttribute(`data-${key}`))) {
              data = true;
            } else if (!Number.isNaN(Number(data))) {
              data = parseFloat(data, 10);

              if (Number.isNaN(data)) {
                data = undefined;
              }
            }
          }
        }
      }
    });

    if (data && data.jLightInternal) {
      const newData = {};

      Object.entries(data).forEach(([dataKey, dataValue]) => {
        if (dataKey !== 'jLightInternal') {
          newData[dataKey] = dataValue;
        }
      });

      data = newData;
    }

    return data;
  },

  /**
   * @module Manipulation
   * @tutorial tut-manipulation
   */

  /**
   * Empties the collections elements HTML content.
   *
   * @function
   * @tutorial empty
   * @returns {jLight} jLight collection
   */
  empty: () => {
    elements.forEach((theElement) => {
      const element = theElement;

      element.innerHTML = '';
    });

    return $(elements);
  },

  /**
   * Clones the collection.
   *
   * @function
   * @tutorial clone
   * @param {boolean} [deep] Whether to apply deep cloning of the affected nodes (default: true)
   * @returns {jLight} jLight collection
   */
  clone: (deep = true) => $(elements.map((element) => element.cloneNode(deep))),

  /**
   * Adds elements to the collection.
   *
   * @function
   * @tutorial add
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  add: ($elements) => {
    const theElements = getElementsFromArgument($elements);
    const addedElements = [];

    theElements.forEach((referenceElement) => {
      if (referenceElement && !elements.includes(referenceElement)) {
        addedElements.push(referenceElement);
      }
    });

    return $([...elements, ...addedElements]);
  },

  /**
   * Removes elements from the collection.
   *
   * @function
   * @tutorial remove
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} [$elements] The elements to remove
   * @param {boolean} [removeFromDom]
   * Whether the elements should also be removed from the DOM (default: true)
   * @returns {jLight|null} jLight collection or null
   */
  remove: ($elements, removeFromDom = true) => {
    if (!$elements) {
      elements.forEach((element) => {
        element.remove();
        removeJLightElementData(element);
      });

      return null;
    }

    const remainingElements = [];
    const theElements = getElementsFromArgument($elements);

    elements.forEach((element) => {
      let wasRemoved;

      theElements.forEach((referenceElement) => {
        if (element === referenceElement) {
          wasRemoved = true;

          if (removeFromDom) {
            element.remove();
            removeJLightElementData(element);
          }
        }
      });

      if (!wasRemoved) {
        remainingElements.push(element);
      }
    });

    return $(remainingElements);
  },

  /**
   * Prepends elements to the collections elements.
   *
   * @function
   * @tutorial prepend
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  prepend: ($elements) => $(prependOrAppend('prepend', $elements, elements)),

  /**
   * Appends elements to the collections elements.
   *
   * @function
   * @tutorial append
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  append: ($elements) => $(prependOrAppend('append', $elements, elements)),

  /**
   * Prepends the collections elements to elements.
   *
   * @function
   * @tutorial prependTo
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  prependTo: ($elements) => $(prependToOrAppendTo('prepend', $elements, elements)),

  /**
   * Appends the collections elements to elements.
   *
   * @function
   * @tutorial appendTo
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  appendTo: ($elements) => $(prependToOrAppendTo('append', $elements, elements)),

  /**
   * Inserts the collections elements before elements.
   *
   * @function
   * @tutorial insertBefore
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  insertBefore: ($elements) => $(insertBeforeOrInsertAfter('beforeBegin', $elements, elements, 'insert')),

  /**
   * Inserts the collections elements after elements.
   *
   * @function
   * @tutorial insertAfter
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  insertAfter: ($elements) => $(insertBeforeOrInsertAfter('afterEnd', $elements, elements, 'insert')),

  /**
   * Inserts elements before the collections elements.
   *
   * @function
   * @tutorial before
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  before: ($elements) => $(insertBeforeOrInsertAfter('beforeBegin', $elements, elements)),

  /**
   * Inserts elements after the collections elements.
   *
   * @function
   * @tutorial after
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  after: ($elements) => $(insertBeforeOrInsertAfter('afterEnd', $elements, elements)),

  /**
   * Wraps the collections elements in elements.
   *
   * @function
   * @tutorial wrap
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  wrap: ($elements) => {
    const theElements = getElementsFromArgument($elements);

    elements.forEach((element) => {
      theElements.forEach((referenceElement) => {
        if (element !== referenceElement) {
          element.parentNode.insertBefore(referenceElement, element);
          referenceElement.appendChild(element);
        }
      });
    });

    return $(elements);
  },

  /**
   * @module Selection
   * @tutorial tut-selection
   */

  /**
   * Gets the element at the supplied index from the collection.
   *
   * @function
   * @tutorial get
   * @param {number} index The index to supply to the function
   * @returns {HTMLElement|undefined} The element or null
   */
  get: (index) => elements[index],

  /**
   * Gets the jLight element at the supplied index from the collection.
   *
   * @function
   * @tutorial eq
   * @param {number} index The index to supply to the function
   * @returns {jLight} jLight collection
   */
  eq: (index) => $(elements[index] ? [elements[index]] : []),

  /**
   * Gets the first jLight element from the collection.
   *
   * @function
   * @tutorial first
   * @returns {jLight} jLight collection
   */
  first: () => $(elements[0] ? [elements[0]] : []),

  /**
   * Gets the last jLight element from the collection.
   *
   * @function
   * @tutorial last
   * @returns {jLight} jLight collection
   */
  last: () => $(elements.length ? [elements[elements.length - 1]] : []),

  /**
   * Gets a jLight collection from the collections parent elements.
   *
   * @function
   * @tutorial parent
   * @returns {jLight} jLight collection
   */
  parent: () => $(getParents(elements)),

  /**
   * Gets a jLight collection from all the collections parent elements.
   *
   * @function
   * @tutorial parents
   * @returns {jLight} jLight collection
   */
  parents: () => $(getParents(elements, true)),

  /**
   * Gets a jLight collection from the collections children elements.
   *
   * @function
   * @tutorial children
   * @returns {jLight} jLight collection
   */
  children: () => $(elements.reduce(
    (children, element) => [...children, ...element.children], [],
  )),

  /**
   * Gets a jLight collection from the collections sibling elements.
   *
   * @function
   * @tutorial siblings
   * @returns {jLight} jLight collection
   */
  siblings: () => {
    const siblings = [];

    elements.forEach((element) => {
      const { parentElement } = element;

      [...parentElement.children].forEach((child) => {
        if (!siblings.includes(child) && !elements.includes(child)) {
          siblings.push(child);
        }
      });
    });

    return $(siblings);
  },

  /**
   * Gets a jLight collection from the collections previous sibling elements.
   *
   * @function
   * @tutorial prev
   * @param {string} [selector] The selector to use for matching elements
   * @returns {jLight} jLight collection
   */
  prev: (selector) => $(getPrevOrNextElements('previousElementSibling', selector, elements)),

  /**
   * Gets a jLight collection from the collections next sibling elements.
   *
   * @function
   * @tutorial next
   * @param {string} [selector] The selector to use for matching elements
   * @returns {jLight} jLight collection
   */
  next: (selector) => $(getPrevOrNextElements('nextElementSibling', selector, elements)),

  /**
   * Gets a jLight collection from all the collections parent elements matching the selector.
   *
   * @function
   * @tutorial closest
   * @param {string} [selector] The selector to use for matching elements
   * @returns {jLight} jLight collection
   */
  closest: (selector) => {
    const closestElements = [];

    elements.forEach((element) => {
      const closest = getClosestMatchingElement(element, selector);

      if (closest && !closestElements.includes(closest)) {
        closestElements.push(closest);
      }
    });

    return $(closestElements);
  },

  /**
   * Gets a jLight collection from the collections elements which are not part of elements.
   *
   * @function
   * @tutorial not
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  not: ($elements) => {
    const selectedElements = getElementsFromArgument($elements);

    const remainingElements = elements.filter((element) => {
      let keepElement = true;

      selectedElements.forEach((selectedElement) => {
        if (keepElement && selectedElement === element) {
          keepElement = false;
        }
      });

      return keepElement;
    });

    return $(remainingElements);
  },

  /**
   * Gets a jLight collection from the collections elements which contain elements.
   *
   * @function
   * @tutorial has
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to apply the function to
   * @returns {jLight} jLight collection
   */
  has: ($elements) => {
    const selectedElements = getElementsFromArgument($elements);

    const filteredElements = elements.filter((element) => {
      let hasElement = false;

      selectedElements.forEach((selectedElement) => {
        if (!hasElement
          && element !== selectedElement
          && element.contains(selectedElement)) {
          hasElement = true;
        }
      });

      return hasElement;
    });

    return $(filteredElements);
  },

  /**
   * @module ArrayLike
   * @tutorial tut-array-like
   */

  /**
   * Gets a filtered jLight collection based on the input.
   *
   * @function
   * @tutorial filter
   * @param {string|iteratorCallback} callbackOrSelector
   * The selector to filter the collection by or a custom
   * function to decide which elements to filter out
   * @returns {jLight} jLight collection
   */
  filter: (callbackOrSelector) => {
    const filteredElements = [];

    elements.forEach((element, index) => {
      if (typeof callbackOrSelector === 'function') {
        if (callbackOrSelector($([element]), index)) {
          filteredElements.push(element);
        }
      } else if (element.matches(callbackOrSelector)) {
        filteredElements.push(element);
      }
    });

    return $(filteredElements);
  },

  /**
   * Runs a function on each of the collections elements.
   *
   * @function
   * @tutorial forEach
   * @param {iteratorCallback} callback The function to apply to each element
   * @returns {jLight} jLight collection
   */
  forEach: (callback) => {
    elements.forEach((element, index) => {
      callback($([element]), index);
    });

    return $(elements);
  },

  /**
   * [DEPRECATED] Runs a function on each of the collections elements.
   *
   * @deprecated
   * Will be removed in version 1.2.0,
   * use {@link jLight jLight}.{@link module:ArrayLike~forEach forEach} instead
   * @function
   * @tutorial each
   * @param {iteratorCallback} callback The function to apply to each element
   * @returns {jLight} jLight collection
   */
  each: (callback) => {
    // eslint-disable-next-line no-console
    console.warn('jLight.each is deprecated an will be removed in version 1.2.0, use jLight.forEach instead');

    return $(elements).forEach(callback);
  },

  /**
   * Slices the collection.
   *
   * @function
   * @tutorial slice
   * @param {number} [start] The index to start the slicing
   * @param {number} [end] The index to end the slicing
   * @returns {jLight} jLight collection
   */
  slice: (start, end) => $(elements.slice(start, end)),

  /**
   * Splices the collection
   *
   * @function
   * @tutorial splice
   * @param {number} start The index to start the splicing
   * @param {number} [deleteCount] The count of elements to delete
   * @returns {jLight} jLight collection
   */
  splice: (start, deleteCount) => $(elements.splice(start, deleteCount)),

  /**
   * Pushes elements to the collection.
   *
   * @function
   * @tutorial push
   * @param {...(jLight|string|HTMLElement|HTMLCollection|NodeList)} args
   * The elements to apply to the function.
   * @returns {number} The collections new length
   */
  push: (...args) => {
    args.forEach((arg) => {
      elements.push(...getElementsFromArgument(arg));
    });

    return elements.length;
  },

  /**
   * Pops the last element from the collection.
   *
   * @function
   * @tutorial pop
   * @returns {jLight} jLight collection
   */
  pop: () => $([elements.pop()]),

  /**
   * Reverses a collection.
   *
   * @function
   * @tutorial reverse
   * @returns {jLight} jLight collection
   */
  reverse: () => $(elements.reverse()),

  /**
   * Shifts a collection.
   *
   * @function
   * @tutorial shift
   * @returns {jLight} jLight collection
   */
  shift: () => $([elements.shift()]),

  /**
   * Unshifts a collection.
   *
   * @function
   * @tutorial unshift
   * @param {...(jLight|string|HTMLElement|HTMLCollection|NodeList)} args
   * The elements to apply to the function.
   * @returns {number} The collections new length
   */
  unshift: (...args) => {
    const theElements = [];

    args.forEach((arg) => {
      theElements.push(...getElementsFromArgument(arg));
    });

    elements.unshift(...theElements.reverse());

    return elements.length;
  },

  /**
   * Sorts a collection.
   *
   * @function
   * @tutorial sort
   * @param {compareCallback} compareFunction The function used for sorting
   * @returns {jLight} jLight collection
   */
  sort: (compareFunction = noop) => $(
    elements.sort((element1, element2) => compareFunction($([element1]), $([element2]))),
  ),

  /**
   * Reduces a collection.
   *
   * @function
   * @tutorial reduce
   * @param {reduceInnerCallback} callback The function used for reduction
   * @param {*} [initialValue] The initial value used for reduction
   * @returns {*} The reduction result
   */
  reduce: (callback, initialValue) => elements.reduce(
    (accumulator, element, index) => callback(accumulator, $([element]), index),
    initialValue,
  ),

  /**
   * Maps a collection.
   *
   * @function
   * @tutorial map
   * @param {mapInnerCallback} callback The function used for mapping
   * @returns {jLight} jLight collection
   */
  map: (callback) => $(elements.map((element, index) => {
    const argument = callback($([element]), index);

    return getElementsFromArgument(argument)[0];
  })),

  /**
   * Concats a collection with other collections.
   *
   * @function
   * @tutorial concat
   * @param {...(jLight|string|HTMLElement|HTMLCollection|NodeList|
   * Array.<jLight|string|HTMLElement|HTMLCollection|NodeList>)} args
   * The elements to apply to the function.
   * @returns {jLight} jLight collection
   */
  concat: (...args) => {
    const theElements = [];

    args.forEach((arg) => {
      if (Array.isArray(arg)) {
        arg.forEach((a) => {
          theElements.push(...getElementsFromArgument(a));
        });
      } else {
        theElements.push(...getElementsFromArgument(arg));
      }
    });

    return $(elements.concat(theElements));
  },

  /**
   * Whether the collections elements include at least one of elements.
   *
   * @function
   * @tutorial includes
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to supply to the function
   * @returns {boolean} If the conditon is met
   */
  includes: ($elements) => {
    const theElements = getElementsFromArgument($elements);
    let includes;

    elements.forEach((element) => {
      if (!includes) {
        theElements.forEach((referenceElement) => {
          if (!includes) {
            includes = element === referenceElement;
          }
        });
      }
    });

    return includes;
  },

  /**
   * Whether at least one of the collections elements meets the conditon.
   *
   * @function
   * @tutorial some
   * @param {iteratorBooleanCallback} callback
   * The elements to supply to the function
   * @returns {boolean} If the conditon is met
   */
  some: (callback) => {
    let isMet;

    elements.forEach((element, index) => {
      if (!isMet && callback($([element]), index)) {
        isMet = true;
      }
    });

    return isMet;
  },

  /**
   * Whether every one of the collections elements meets the conditon.
   *
   * @function
   * @tutorial every
   * @param {iteratorBooleanCallback} callback
   * The elements to supply to the function
   * @returns {boolean} If the conditon is met
   */
  every: (callback) => {
    let matchCount = 0;

    elements.forEach((element, index) => {
      if (callback($([element]), index)) {
        matchCount += 1;
      }
    });

    return matchCount === elements.length;
  },

  /**
   * Gets the given elements index inside the collection.
   *
   * @function
   * @tutorial indexOf
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to supply to the function
   * @returns {number} The resulting number
   */
  indexOf: ($elements) => indexOfOfLastIndexOf('indexOf', elements, $elements),

  /**
   * Gets the given elements last index inside the collection.
   *
   * @function
   * @tutorial lastIndexOf
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to supply to the function
   * @returns {number} The resulting number
   */
  lastIndexOf: ($elements) => indexOfOfLastIndexOf('lastIndexOf', elements, $elements),

  /**
   * Gets a jLight collection from the collections children elements matching the selector.
   *
   * @function
   * @tutorial find
   * @param {string} [selector] The selector to use for matching elements
   * @returns {jLight} jLight collection
   */
  find: (selector) => {
    let foundElements = [];

    elements.forEach((element) => {
      foundElements = [...foundElements, ...element.querySelectorAll(selector)];
    });

    return $(foundElements);
  },

  /**
   * @module Dimensions
   * @tutorial tut-dimensions
   */

  /**
   * Gets or sets the width of the collections elements.
   *
   * @function
   * @tutorial width
   * @param {number} [width] The width to supply to the function
   * @returns {jLight|number} jLight collection or the width
   */
  width: (width) => getOrSetDimension('width', $(elements), width),

  /**
   * Gets or sets the height of the collections elements.
   *
   * @function
   * @tutorial height
   * @param {number} [height] The height to supply to the function
   * @returns {jLight|number} jLight collection or the height
   */
  height: (height) => getOrSetDimension('height', $(elements), height),

  /**
   * Gets the inner width of the collections elements.
   *
   * @function
   * @tutorial innerWidth
   * @returns {number} The dimension value
   */
  innerWidth: () => getDimension('innerWidth', elements),

  /**
   * Gets the inner height of the collections elements.
   *
   * @function
   * @tutorial innerHeight
   * @returns {number} The dimension value
   */
  innerHeight: () => getDimension('innerHeight', elements),

  /**
   * Gets the outer width of the collections elements.
   *
   * @function
   * @tutorial outerWidth
   * @param {boolean} [includeMargins] Whether to include the elements margins (default: false)
   * @returns {number} The dimension value
   */
  outerWidth: (includeMargins) => getDimension('outerWidth', elements, includeMargins),

  /**
   * Gets the outer height of the collections elements.
   *
   * @function
   * @tutorial outerHeight
   * @param {boolean} [includeMargins] Whether to include the elements margins (default: false)
   * @returns {number} The dimension value
   */
  outerHeight: (includeMargins) => getDimension('outerHeight', elements, includeMargins),

  /**
   * Gets the scroll width of the collections elements.
   *
   * @function
   * @tutorial scrollWidth
   * @returns {number} The dimension value
   */
  scrollWidth: () => getScrollWidthOrScrollHeight('scrollWidth', elements),

  /**
   * Gets the scroll height of the collections elements.
   *
   * @function
   * @tutorial scrollHeight
   * @returns {number} The dimension value
   */
  scrollHeight: () => getScrollWidthOrScrollHeight('scrollHeight', elements),

  /**
   * Gets or sets the scrollTop of the collections elements.
   *
   * @function
   * @tutorial scrollTop
   * @param {number} [value] The value to supply to the function
   * @returns {jLight|number} jLight collection or the value
   */
  scrollTop: (value) => getOrSetScrollTopOrScrollLeft('scrollTop', value, $(elements)),

  /**
   * Gets or sets the scrollLeft of the collections elements.
   *
   * @function
   * @tutorial scrollLeft
   * @param {number} [value] The value to supply to the function
   * @returns {jLight|number} jLight collection or the value
   */
  scrollLeft: (value) => getOrSetScrollTopOrScrollLeft('scrollLeft', value, $(elements)),

  /**
   * Gets or sets the collections elements offset.
   *
   * @function
   * @tutorial offset
   * @param {boolean|{top: number, left: number}} [value]
   * The value to set the elements offset to or if the
   * returned offset should be relative to the viewport
   * @param {boolean} [relativeToViewport]
   * Whether the offset should be set relative to the viewport (default: false)
   * @returns {jLight|{top: number, left: number}} jLight collection or elements offset
   */
  offset: (value, relativeToViewport) => {
    if (value && typeof value !== 'boolean') {
      const { top, left } = value;
      const topIsPixelUnit = typeof top === 'number' || (top && top.indexOf('px') > -1);
      const leftIsPixelUnit = typeof left === 'number' || (left && left.indexOf('px') > -1);
      const offsetTop = relativeToViewport ? window.pageYOffset : 0;
      const offsetLeft = relativeToViewport ? window.pageXOffset : 0;
      const unitTop = topIsPixelUnit ? 'px' : '';
      const unitLeft = leftIsPixelUnit ? 'px' : '';

      elements.forEach((theElement) => {
        const element = theElement;
        const computedStyles = window.getComputedStyle(element);

        element.style.top = (top || top === 0)
          ? `${topIsPixelUnit ? parseFloat(top, 10) + offsetTop : top}${unitTop}`
          : `${parseFloat(computedStyles.getPropertyValue('top'), 10)}${offsetTop || ''}${unitTop}`;

        element.style.left = (left || left === 0)
          ? `${leftIsPixelUnit ? parseFloat(left, 10) + offsetLeft : top}${unitLeft}`
          : `${parseFloat(computedStyles.getPropertyValue('left'), 10)}${offsetLeft || ''}${unitLeft}`;
      });

      return $(elements);
    }

    let offset;

    elements.forEach((element) => {
      const boundingBox = element.getBoundingClientRect();

      if (offset === undefined) {
        offset = boundingBox;
      }
    });

    const relative = typeof value === 'boolean' && value;

    return {
      top: (offset.top || 0) + (relative ? window.pageYOffset : 0),
      left: (offset.left || 0) + (relative ? window.pageXOffset : 0),
    };
  },

  /**
   * @module Animation
   * @tutorial tut-animation
   */

  /**
   * Animates the given properties to the given values on the collections elements.
   *
   * @function
   * @tutorial animate
   * @param {Object.<string, string>} [properties] The css properties and values to animate
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @returns {jLight} jLight collection
   */
  animate: (properties, duration = 300, callback = noop, easing = 'ease') => {
    const animationId = getUpdateAnimationId(elements[0]);

    elements.forEach((theElement, elementIndex) => {
      const element = theElement;
      let transition = '';

      Object.entries(properties).forEach(([key, value], index) => {
        const theKey = camelToKebab(key);

        transition += `${index === 0 ? '' : ','}${theKey} ${duration}ms ${easing}`;

        if (element.style[key] === undefined) {
          element.style[key] = window
            .getComputedStyle(element)
            .getPropertyValue(key);
        }

        setTimeout(() => {
          element.style[key] = value;
        }, 0);
      });

      updateJLightElementData(element, {
        jLightInternal: {
          ...(getJLightElementData(element).jLightInternal || {}),
          animatedProperties: Object.keys(properties),
        },
      });

      element.style.transition = transition;

      setTimeout(() => {
        if (getJLightElementData(elements[0]).jLightInternal.currentAnimation !== animationId) {
          return;
        }

        if (elementIndex === elements.length - 1) {
          elements.forEach((elementToReset) => {
            const theElementToReset = elementToReset;

            theElementToReset.style.transition = '';
          });

          callback();
        }
      }, duration);
    });

    return $(elements);
  },

  /**
   * Scrolls the collections elements to elements.
   *
   * @function
   * @tutorial scrollTo
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to scroll the collections elements to
   * @param {number} [duration] The duration of the scroll animation in ms (default: 300)
   * @param {{x: number, y: number}|{}} [offset] The offset for the target position
   * @param {Function} [callback] The function to run after the scrolling is complete
   * @returns {jLight} jLight collection
   */
  scrollTo: ($elements, duration = 300, offset = {}, callback) => {
    const theOffset = {
      x: 0,
      y: 0,
      ...offset,
    };

    const [element] = elements;
    const targets = getElementsFromArgument($elements);
    const boundingBox = element.getBoundingClientRect();
    const left = targets[0].offsetLeft
      - boundingBox.left
      - window.pageXOffset;
    const top = targets[0].offsetTop
      - boundingBox.top
      - window.pageYOffset;
    const innerWidth = Math.max(element.clientWidth, element.offsetWidth);
    const innerHeight = Math.max(element.clientHeight, element.offsetHeight);
    const { scrollWidth, scrollHeight } = element;
    const startPositionX = element.scrollLeft;
    const startPositionY = element.scrollTop;
    const targetX = scrollWidth - left < innerWidth
      ? scrollWidth - innerWidth - theOffset.x
      : left - theOffset.x;
    const targetY = scrollHeight - top < innerHeight
      ? scrollHeight - innerHeight - theOffset.y
      : top - theOffset.y;
    const differenceX = targetX - startPositionX;
    const differenceY = targetY - startPositionY;

    doEasing(duration, (percent) => {
      element.scrollLeft = startPositionX + differenceX * percent;
      element.scrollTop = startPositionY + differenceY * percent;
    }, callback);

    return $(elements);
  },

  /**
   * Stops all running animations on the collections elements.
   *
   * @function
   * @tutorial stop
   * @returns {jLight} jLight collection
   */
  stop: () => {
    elements.forEach((theElement) => {
      const element = theElement;
      const computedStyles = window.getComputedStyle(element);
      const jLightInternalData = getJLightElementData(element).jLightInternal;
      const animatedProperties = jLightInternalData.animatedProperties
        || [];

      updateJLightElementData(element, {
        jLightInternal: {
          ...jLightInternalData,
          currentAnimation: null,
        },
      });

      animatedProperties.forEach((key) => {
        element.style[key] = computedStyles.getPropertyValue(
          camelToKebab(key),
        );
      });

      element.style.transition = '';
    });

    return $(elements);
  },

  /**
   * Fades the collections elements in.
   *
   * @function
   * @tutorial fadeIn
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [type]
   * The css display type to apply to the collections elements (default: 'block')
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @returns {jLight} jLight collection
   */
  fadeIn: (duration, callback, type, easing) => {
    elements.forEach((theElement) => {
      const element = theElement;

      getUpdateAnimationId(element);
      element.style.opacity = 0;
      element.style.display = type || 'block';

      setTimeout(() => {
        $([element]).animate({ opacity: 1 }, duration, callback, easing);
      }, 1);
    });

    return $(elements);
  },

  /**
   * Fades the collections elements out.
   *
   * @function
   * @tutorial fadeOut
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @returns {jLight} jLight collection
   */
  fadeOut: (duration, callback, easing) => {
    elements.forEach((theElement) => {
      const element = theElement;

      setTimeout(() => {
        $([element]).animate({ opacity: 0 }, duration, () => {
          element.style.display = 'none';

          if (callback) {
            callback();
          }
        }, easing);
      }, 1);
    });

    return $(elements);
  },

  /**
   * Toggles the display state of the collections elements by fading.
   *
   * @function
   * @tutorial fadeToggle
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [type]
   * The css display type to apply to the collections elements (default: 'block')
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @returns {jLight} jLight collection
   */
  fadeToggle: (duration, callback, type, easing) => {
    elements.forEach((theElement) => {
      const element = theElement;
      const computedStyles = window.getComputedStyle(element);

      if (computedStyles.getPropertyValue('display') === 'none') {
        $([element]).fadeIn(duration, callback, type, easing);
      } else {
        $([element]).fadeOut(duration, callback, easing);
      }
    });

    return $(elements);
  },

  /**
   * Slides the collections elements down.
   *
   * @function
   * @tutorial slideDown
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [height] The css height value to end the sliding animation (default: 'auto')
   * @param {string} [type]
   * The css display type to apply to the collections elements (default: 'block')
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @returns {jLight} jLight collection
   */
  slideDown: (duration, callback, height, type, easing) => {
    elements.forEach((theElement) => {
      const element = theElement;
      const startHeight = Math.max(element.clientHeight, element.offsetHeight);
      const computedStyles = window.getComputedStyle(element);
      const paddingTop = parseFloat(computedStyles.getPropertyValue('padding-top'), 10);
      const paddingBottom = parseFloat(computedStyles.getPropertyValue('padding-bottom'), 10);

      element.style.overflow = 'hidden';
      element.style.visibility = 'hidden';
      element.style.display = type || 'block';
      element.style.height = height || 'auto';

      const targetHeight = Math.max(element.clientHeight, element.offsetHeight)
        + parseFloat(computedStyles.getPropertyValue('border-bottom'), 10);

      element.style.height = `${startHeight}px`;
      element.style.visibility = '';
      element.style.paddingTop = 0;
      element.style.paddingBottom = 0;

      setTimeout(() => {
        $([element]).animate({
          height: `${targetHeight}px`,
          paddingTop: `${paddingTop}px`,
          paddingBottom: `${paddingBottom}px`,
        }, duration, () => {
          element.style.height = '';
          element.style.paddingTop = '';
          element.style.paddingBottom = '';
          element.style.overflow = '';

          if (callback) {
            callback();
          }
        }, easing);
      }, 10);
    });

    return $(elements);
  },

  /**
   * Slides the collections elements up.
   *
   * @function
   * @tutorial slideUp
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @returns {jLight} jLight collection
   */
  slideUp: (duration, callback, easing) => {
    elements.forEach((theElement) => {
      const element = theElement;
      const startHeight = Math.max(element.clientHeight, element.offsetHeight);
      const computedStyles = window.getComputedStyle(element);

      element.style.overflow = 'hidden';
      element.style.height = `${startHeight}px`;
      element.style.minHeight = 'auto';
      element.style.paddingTop = computedStyles.getPropertyValue('padding-top');
      element.style.paddingBottom = computedStyles.getPropertyValue('padding-bottom');

      setTimeout(() => {
        $([element]).animate({ height: 0, paddingTop: 0, paddingBottom: 0 }, duration, () => {
          element.style.display = 'none';
          element.style.height = '';
          element.style.overflow = '';
          element.style.minHeight = '';
          element.style.paddingTop = '';
          element.style.paddingBottom = '';

          if (callback) {
            callback();
          }
        }, easing);
      }, 1);
    });

    return $(elements);
  },

  /**
   * Toggles the display state of the collections elements by sliding.
   *
   * @function
   * @tutorial slideToggle
   * @param {number} [duration] The duration for the animation in ms (default: 300)
   * @param {Function} [callback]
   * The function to run after the animation is complete (default: noop)
   * @param {string} [height] The css height value to end the sliding animation (default: 'auto')
   * @param {string} [type]
   * The css display type to apply to the collections elements (default: 'block')
   * @param {string} [easing] Which type of css easing to use for the animation (default: 'ease')
   * @param {boolean} [force] Force whether to slide down or up
   * @returns {jLight} jLight collection
   */
  slideToggle: (duration, callback, height, type, easing, force) => {
    const forceDefined = force !== undefined;
    const forceSlideDown = forceDefined && force;
    const forceSlideUp = forceDefined && !force;

    elements.forEach((theElement) => {
      const element = theElement;
      const computedStyles = window.getComputedStyle(element);

      if (forceSlideDown || (computedStyles.getPropertyValue('display') === 'none' && !forceSlideUp)) {
        $([element]).slideDown(duration, callback, height, type, easing);
      } else {
        $([element]).slideUp(duration, callback, easing);
      }
    });

    return $(elements);
  },

  /**
   * @module Utility
   * @tutorial tut-utility
   */

  /**
   * Whether a property of an element of the collection is true
   * or if an element of the collection is part of another set.
   *
   * @function
   * @tutorial is
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} propertyOrElements
   * The property or set to compare the collections elements to
   * @returns {boolean} If the property is set on one of the collections elements or
   * at least on of the elements is contained in the supplied elements
   */
  is: (propertyOrElements) => {
    let is;

    if (typeof propertyOrElements === 'string') {
      elements.forEach((element) => {
        if (is === undefined) {
          is = element[propertyOrElements];
        }
      });
    } else {
      const theElements = getElementsFromArgument(propertyOrElements);

      theElements.forEach((theElement) => {
        if (elements.includes(theElement)) {
          if (is === undefined) {
            is = true;
          }
        }
      });
    }

    return !!is;
  },

  /**
   * Whether the collections elements contain at least one of elements.
   *
   * @function
   * @tutorial contains
   * @param {jLight|string|HTMLElement|HTMLCollection|NodeList} $elements
   * The elements to supply to the function
   * @returns {boolean} If the conditon is met
   */
  contains: ($elements) => {
    const theElements = getElementsFromArgument($elements);
    let contains;

    elements.forEach((element) => {
      if (!contains) {
        theElements.forEach((referenceElement) => {
          if (!contains) {
            contains = element.contains(referenceElement);
          }
        });
      }
    });

    return contains;
  },

  /**
   * Checks if the collections first element is in view or runs a function if that is the case.
   *
   * @function
   * @tutorial inView
   * @param {Function|{top: number, bottom: number, left: number, right: number}} [offsetOrCallback]
   * The offset used for determining if the element is in view
   * or the function to run each time that is the case
   * @param {Function|
   *  {scrollTimer: ?number, isInView: ?Function, onEnter: ?Function, onExit: ?Function}
   * } [callbackOrOptions]
   * The function to run each time the element is in view
   * or a custom options object to define the functions behavior
   * (defaults: { scrollTimer: 100, isInView: noop, onEnter: noop, onExit: noop })
   * @returns {jLight|boolean} jLight collection or whether the collections first element is in view
   */
  inView: (offsetOrCallback, callbackOrOptions) => {
    let offset = {
      top: 0,
      bottom: 0,
      left: 0,
      right: 0,
    };

    if (typeof offsetOrCallback === 'object' && offsetOrCallback) {
      offset = {
        ...offset,
        ...offsetOrCallback,
      };
    }

    const offsetOrCallbackIsFunction = typeof offsetOrCallback === 'function';
    const callbackOrOptionsIsFunction = typeof callbackOrOptions === 'function';

    if (!offsetOrCallbackIsFunction
      && !callbackOrOptionsIsFunction
      && typeof callbackOrOptions !== 'object'
      && callbackOrOptions !== null) {
      return isInView(elements[0].getBoundingClientRect(), offset);
    }

    const options = callbackOrOptions || {};
    const scrollTimer = options.scrollTimer || 100;
    let scrollTimeout;

    window.addEventListener('scroll', () => {
      if (scrollTimeout) {
        clearTimeout(scrollTimeout);
      }

      scrollTimeout = setTimeout(() => {
        const isElementInView = isInView(elements[0].getBoundingClientRect(), offset);

        if (isElementInView && options.isInView) {
          options.isInView();
        }

        if (isElementInView && !getJLightElementData(elements[0]).isInView) {
          if (options.onEnter) {
            options.onEnter();
          } else if (offsetOrCallbackIsFunction) {
            offsetOrCallback();
          } else if (callbackOrOptionsIsFunction) {
            callbackOrOptions();
          }

          updateJLightElementData(elements[0], {
            isInView: true,
          });

          return;
        }

        if (!isElementInView && getJLightElementData(elements[0]).isInView) {
          if (options.onExit) {
            options.onExit();
          }

          updateJLightElementData(elements[0], {
            isInView: false,
          });
        }
      }, scrollTimer);
    });

    return $(elements);
  },

  /**
   * Delays code execution.
   *
   * @function
   * @tutorial delay
   * @param {number} [delay] The duration to delay the code execution for
   * @returns {Promise} The corresponding promise
   */
  delay: (delay) => new Promise((resolve) => setTimeout(resolve, delay)),

  /**
   * Calls the supplied function with the supplied arguments if the given condition is met.
   *
   * @function
   * @tutorial when
   * @param {boolean|Function} condition The condition to check for.
   * If a function is supplied its return value will be used for checking
   * @param {string|Function} callback The function to run when the condition is met.
   * If a string is provided it should be a valid jLight function name
   * @param {...*} [args] The arguments to supply to the given jLight function
   * @returns {jLight} jLight collection
   */
  when: (condition, callback, ...args) => {
    if (typeof condition === 'function' ? condition() : condition) {
      if (typeof callback === 'string') {
        $(elements)[callback](...args);
      } else {
        callback();
      }
    }

    return $(elements);
  },

  /**
   * Serializes the collections elements values to a URL encoded string.
   *
   * @function
   * @tutorial serialize
   * @returns {string} The resulting string
   */
  serialize: () => {
    let serializedString = '';

    elements.forEach((element, index) => {
      if (element instanceof HTMLFormElement) {
        serializedString += Array.from(new FormData(element),
          (formDataItem) => formDataItem.map(encodeURIComponent).join('=')).join('&');
      } else if (canBeSeralized(element)) {
        let { value } = element;

        if (element.getAttribute('type') === 'checkbox') {
          value = element.checked;
        }

        serializedString += `${index === 0 ? '' : '&'}${element.name}=${value}`;
      }
    });

    return serializedString;
  },

  /**
   * Serializes the collections elements values to a JSON object.
   *
   * @function
   * @tutorial serializeJson
   * @returns {Object.<string, string>} The resulting JSON object
   */
  serializeJson: () => {
    let serializedJson = {};

    elements.forEach((element) => {
      if (element instanceof HTMLFormElement) {
        [...element.children].forEach((child) => {
          if (child.name) {
            serializedJson = addValueToJson(element, serializedJson);
          }
        });
      } else if (canBeSeralized(element)) {
        serializedJson = addValueToJson(element, serializedJson);
      }
    });

    return serializedJson;
  },
});

const documentAndWindowJLightElement = (argument) => ({
  on: (theTypes, callbackOrSelector, delegatedCallback) => {
    let types = [theTypes];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    if (typeof callbackOrSelector === 'function' || callbackOrSelector === false) {
      const callback = (theEvent) => {
        const event = theEvent;

        event.$target = $([event.target]);
        event.$currentTarget = $([event.currentTarget]);

        if (callbackOrSelector === false
          || callbackOrSelector(event, event.jLightEventData) === false
          || delegatedCallback === false) {
          preventEvent(event);
        }
      };

      types.forEach((type) => {
        addJLightElementEventData(argument, type, callbackOrSelector, callback);
        argument.addEventListener(type, callback);
      });
    } else {
      types.forEach((type) => {
        argument.addEventListener(type, (theEvent) => {
          const event = theEvent;

          if (event.target.matches(callbackOrSelector)) {
            event.$target = $([event.target]);
            event.$currentTarget = $([event.currentTarget]);

            if (delegatedCallback === false
              || delegatedCallback(event, event.jLightEventData) === false) {
              preventEvent(event);
            }
          }
        });
      });
    }

    return documentAndWindowJLightElement(argument);
  },
  off: (theTypes, callback) => {
    const jLightElementData = getJLightElementData(argument);
    const events = jLightElementData.jLightInternal.events || [];
    let types = [theTypes];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    types.forEach((type) => {
      events.forEach((event) => {
        if (event.type === type && event.callback === callback) {
          removeJLightElementEventData(argument, type, callback, event.realCallback);
          argument.removeEventListener(type, event.realCallback);
        }
      });
    });

    return documentAndWindowJLightElement(argument);
  },
  trigger: (theTypes, jLightEventData) => {
    let types = [theTypes];

    if (types[0].indexOf(' ') > -1) {
      types = types[0].split(' ');
    }

    const event = document.createEvent('Event');

    types.forEach((type) => {
      event.jLightEventData = jLightEventData;
      event.initEvent(type, true, true);
      argument.dispatchEvent(event);
    });

    return documentAndWindowJLightElement(argument);
  },
  innerWidth: () => window.innerWidth,
  innerHeight: () => window.innerHeight,
  outerWidth: () => window.outerWidth,
  outerHeight: () => window.outerHeight,
  scrollTop: (value) => {
    if (value !== undefined) {
      window.scrollTo(0, value);

      return documentAndWindowJLightElement(argument);
    }

    return window.pageYOffset;
  },
  scrollLeft: (value) => {
    if (value !== undefined) {
      window.scrollTo(value, 0);

      return documentAndWindowJLightElement(argument);
    }

    return window.pageXOffset;
  },
  scrollTo: (theArgument, duration = 300, theOffset = {}, callback) => {
    const offset = {
      x: 0,
      y: 0,
      ...theOffset,
    };

    const elements = getElementsFromArgument(theArgument);
    const boundingBox = elements[0].getBoundingClientRect();
    const top = boundingBox.top + window.pageYOffset;
    const left = boundingBox.left + window.pageXOffset;
    const { innerWidth, innerHeight } = window;
    const { scrollWidth, scrollHeight } = document.body;
    const targetX = scrollWidth - left < innerWidth
      ? scrollWidth - innerWidth - offset.x
      : left - offset.x;
    const targetY = scrollHeight - top < innerHeight
      ? scrollHeight - innerHeight - offset.y
      : top - offset.y;
    const startPositionX = window.pageXOffset;
    const startPositionY = window.pageYOffset;
    const differenceX = targetX - startPositionX;
    const differenceY = targetY - startPositionY;

    doEasing(duration, (percent) => {
      window.scrollTo(
        startPositionX + differenceX * percent,
        startPositionY + differenceY * percent,
      );
    }, callback);

    return documentAndWindowJLightElement(argument);
  },
});

/**
 * @global
 * @function $
 * @tutorial tut-constructor
 * @description jLights default export
 * @param {jLight|string|Function|HTMLElement|HTMLCollection|NodeList|document|window} argument
 * The argument to initialize jLight on
 * @returns {jLight} jLight collection
 */
export default (argument) => {
  if (!argument) {
    return $([]);
  }

  if (argument.elements
    && Array.isArray(argument.elements)
    && argument.elements.every((element) => element instanceof HTMLElement)) {
    return $(argument.elements);
  }

  if (typeof argument === 'function') {
    if (document.readyState !== 'loading') {
      argument();
    } else {
      document.addEventListener('DOMContentLoaded', argument);
    }

    return documentAndWindowJLightElement(document);
  }

  if (argument === document || argument === window) {
    initalizeJLightElementData(argument, argument);

    return documentAndWindowJLightElement(argument);
  }

  if (argument instanceof HTMLElement) {
    return $([argument]);
  }

  if (argument instanceof NodeList
    || argument instanceof HTMLCollection) {
    return $([...argument]);
  }

  let elements = [];

  if (typeof argument === 'string') {
    if (argument.match(/<(.|\n)+>/)) {
      elements = [...createElementsFromString(argument)];
    } else {
      try {
        elements = [...document.querySelectorAll(argument)];

        elements.forEach((element) => {
          initalizeJLightElementData(element, argument);
        });
      } catch (e) {
        return $([]);
      }
    }
  }

  return $(elements);
};