helpers.js

/**
 * Global helper functions
 *
 * @namespace Helpers
 */

/**
 * Send XMLHttpRequest and call either error or success callback
 * @memberof Helpers
 * @param { String } method
 * @param { String } url
 * @param { (Object | Null) } body
 * @param { Function } errCallback
 * @param { Function } successCallback
 */
function ajax(method, url, body, errCallback, successCallback) {
    const xhr = new XMLHttpRequest();
    const form = body ? new FormData() : null;

    xhr.onload = () => {
        if (xhr.status !== 200) {
            errCallback(xhr.status);
            return;
        }

        successCallback(xhr.response);
    };

    xhr.onerror = errCallback;
    xhr.open(method, url);

    if (body) {
        Object.keys(body).forEach(key => form.append(key, body[key]));
        xhr.send(form);
    }

    else xhr.send();
}

/**
 * Check whether required DOM elements exist, if so, call fn with provided els
 * @memberof Helpers
 * @param { Function } fn Function to be called if elements exist
 * @param { Array.<HTMLElement | NodeList> } required All elements in this array are required to exist, otherwise fn won't be called
 * @param { Array.<HTMLElement | NodeList> } [optional] These are optional elements that will be passed to fn if it's called
 */
function callFnWithElementsIfExist(fn, required, optional = []) {
    let call = true;

    required.forEach((el) => {
        if (!el) call = false;
        else if (NodeList.prototype.isPrototypeOf(el) && !el.length) call = false; // eslint-disable-line
    });

    if (call) fn(...required, ...optional);
}

/**
 * @summary Create a debounced version of the passed function
 * @description Function to execute and timeout can be passed either at init or at call time and can be redefined
 * @memberof Helpers
 * @param { Function } [fn] Function to debounce
 * @param { Integer } [delay] Delay after which it'll be executed after the last call to de debounced function
 * @return { Function } Debounced function
 * @example
 * const debouncedFn = debounce(testingFunction, 500);
 *
 * // This function can be called any time
 * debouncedFn();
 *
 * // It can also be redefined
 * debouncedFn(anotherTestingFn); // will execute anotherTestingFn instead of testingFunction if this call is the last
 * debouncedFn(anotherTestingFn, 800); // either fn or both fn and delay can be overriden at call time
 *
 * // Unless it's called again w/o params, in this case, the params provided at init will be used
 * debouncedFn(); // Will exec testingFunction after 500ms
 *
 * // Can also be canceled
 * clearTiemout(debouncedFn);
 */
function debounce(fn, delay) { // eslint-disable-line
    let timer;

    return (finalFn = fn, finalDelay = delay) => { // eslint-disable-line
        if (!finalFn || !finalDelay) throw new TypeError('A function and a delay must be provided');
        clearTimeout(timer);
        timer = setTimeout(finalFn, finalDelay);
        return timer;
    };
}

/**
 * Get current url
 * @memberof Helpers
 * @param { Bool } [pathName] Whether to return only relative url without anchors and query parameters
 * @return { String }
 */
function getUrl(pathName) {
    if (pathName) return window.location.pathname;
    return window.location.href;
}

/**
 * Get a cookie by name
 * @memberof Helpers
 * @param { String } name Cookie name
 * @return { String }
 */
function getCookie(name) {
    const value = `; ${document.cookie}`;
    const parts = value.split(`; ${name}=`);
    if (parts.length === 2) return parts.pop().split(';').shift();
}

/**
 * Set or update a cookie
 * @memberof Helpers
 * @param { String } name Cookie name in ASCII
 * @param { String } val Cookie value as a key=value string with ";" separator
 * @return { String }
 */
function setCookie(name, val) {
    document.cookie = `${name}=${val}`;
}

/**
 * Get the viewport width
 * @memberof Helpers
 * @return { Number } Size in pixels
 */
function getWindowWidth() {
    return window.innerWidth;
}

/**
 * Get the viewport height
 * @memberof Helpers
 * @return { Number } Size in pixels
 */
function getWindowHeight() {
    return window.innerWidth;
}

/**
 * Get a query parameter's value
 * @memberof Helpers
 * @param { String } paramName Name of the parameter to get the value from
 * @return { String }
 */
function getUrlParamValue(paramName) {
    const urlParams = new URLSearchParams(window.location.search);
    return urlParams.get([paramName]);
}

/**
 * Get url query string (unparsed)
 * @memberof Helpers
 * @return { String }
 */
function getUrlQueryString() {
    return window.location.search;
}

/**
 * Navigate to a new page
 * @memberof Helpers
 * @param { String } url
 * @param { Bool } [redirect] Whether this is a redirect and as such, won't be saved in browser history
 */
function navigateTo(url, redirect) {
    if (redirect) window.location.replace(url);
    else window.location.assign(url);
}

/**
 * Trigger scroll to coordinates relative to the element
 * @param { Element } el Scroll to a particular set of coordinates inside a given element
 * @param { Integer } left Number of pixels along the horizontal axis of the element that will be displayed in the upper left
 * @param { Integer } top Number of pixels along the vertical axis of the element that will be displayed in the upper left
 * @param { Bool } smooth Whether the scrolling should animate smoothly
 * @memberof Helpers
 */
function scrollIn(el, left, top, smooth) {
    if (smooth) el.scrollTo({ left, top, behavior: 'smooth' });
    else el.scrollTo();
}

/**
 * Trigger scroll to the element
 * @param { Element } el Scroll untill the element is at the top of the viewport (like clicking an anchor tag)
 * @param { Bool } smooth Whether the scrolling should animate smoothly
 * @memberof Helpers
 */
function scrollTo(el, smooth) {
    if (smooth) el.scrollIntoView({ behavior: 'smooth' });
    else el.scrollIntoView();
}

/**
 * Create a thottled version of the passed function
 * @memberof Helpers
 * @param { Function } fn Function to throttle
 * @param { Integer } delay Minimum allowed interval of time between two calls of the function
 * @return { Function } Throttled function
 */
function throttle(delay, fn) {
    let wait = false;
    let toExec;

    return () => {
        if (wait) {
            if (toExec) clearTimeout(toExec);

            toExec = setTimeout(fn, delay);
            return;
        }

        if (toExec) clearTimeout(toExec);
        fn();
        wait = true;

        setTimeout(() => {
            wait = false;
        }, delay);
    };
}

export {
    ajax,
    callFnWithElementsIfExist,
    debounce,
    getCookie,
    getUrl,
    getUrlParamValue,
    getUrlQueryString,
    getWindowWidth,
    getWindowHeight,
    navigateTo,
    setCookie,
    scrollIn,
    scrollTo,
    throttle
};