util_Helper.js

import { ObservableSlim } from "../_external/observableSlim/ObservableSlim.js";

/**
 * Helper class provides static helper functions.
 */
class Helper
{
    /**
     *
     * @param {string} str String to trim
     * @param {string|undefined} characters Characters to trim. Default is empty space.
     * @param {string|undefined} flags RegExp flag. Default is "g"
     * @returns {string}
     */
    static trim ( str, characters = " ", flags = "g" )
    {
        if (typeof str !== "string" || typeof characters !== "string" || typeof flags !== "string")
        {
            throw new TypeError("argument must be string");
        }

        if (!/^[gi]*$/.test(flags))
        {
            throw new TypeError("Invalid flags supplied '" + flags.match(new RegExp("[^gi]*")) + "'");
        }

        characters = characters.replace(/[\[\](){}?*+\^$\\.|\-]/g, "\\$&");

        return str.replace(new RegExp("^[" + characters + "]+|[" + characters + "]+$", flags), '');
    }

    /**
     * Serialize given from
     * @param {HTMLFormElement} form - The form element to serialize
     * @returns {object} - Plain javascript object containing the name and value of given form.
     */
    static serializeForm( form )
    {
        const object = {};
        new FormData( form ).forEach(( value, key) =>
        {
            // Reflect.has in favor of: object.hasOwnProperty(key)
            if( !Reflect.has( object, key ) )
            {
                object[ key ] = value;
                return;
            }

            if( !Array.isArray( object[ key ] ) )
            {
                object[ key ] = [ object[ key ] ];
            }

            object[ key ].push( value );
        });
        return object;
    }

    /**
     * Creates an unique ID
     * @returns {string}
     * @throws {Error} - If crypto module is not available
     */
    static createUid()
    {
        if ( typeof crypto === 'undefined'  )
        {
            throw new Error( 'Crypto is not available.' );
        }

        return ( [ 1e7 ] + -1e3 + -4e3 + -8e3 + -1e11 ).replace( /[018]/g, c =>
            ( c ^ crypto.getRandomValues( new Uint8Array( 1 ) )[ 0 ] & 15 >> c / 4 ).toString( 16 )
        );
    }

    /**
     * Checks if given value is string
     *
     * @param {*} v - Value to check
     * @returns {boolean}
     */
    static isString( v )
    {
        return ( typeof v === 'string' || v instanceof String );
    }

    /**
     * Checks if given value is an array or not
     *
     * @param {*} v - Value to check
     * @returns {boolean}
     */
    static isArray( v )
    {
        return Array.isArray( v );
    }

    /**
     * Checks if given value is a plain object
     *
     * @see {@link https://github.com/lodash/lodash/blob/master/isPlainObject.js}
     * @param value
     * @returns {boolean}
     */
    static isPlainObject( value )
    {
        if ( !Helper._isObjectLike( value ) || Helper._getTag( value ) != '[object Object]' )
        {
            return false
        }

        if ( Object.getPrototypeOf( value ) === null )
        {
            return true
        }

        let proto = value
        while ( Object.getPrototypeOf( proto ) !== null)
        {
            proto = Object.getPrototypeOf(proto)
        }

        return Object.getPrototypeOf( value ) === proto
    }

    /**
     * Checks if given value is a class constructor
     *
     * @see {@link https://stackoverflow.com/questions/30758961/how-to-check-if-a-variable-is-an-es6-class-declaration}
     * @param v
     * @returns {boolean}
     */
    static isClass( v )
    {
        return typeof v === 'function' && /^\s*class\s+/.test(v.toString());
    }

    /**
     * Deep merges two objects into target
     *
     * @param {Object} target
     * @param {Object} sources
     * @return {Object}
     * @example
     *
     * const merged = mergeDeep({a: 1}, { b : { c: { d: { e: 12345}}}});
     * // => { a: 1, b: { c: { d: [Object] } } }
     */
    static deepMerge( target, ...sources )
    {
        if (!sources.length) return target;
        const source = sources.shift();

        if ( Helper.isPlainObject( target ) && Helper.isPlainObject( source ) )
        {
            for ( const key in source )
            {
                if ( Helper.isPlainObject( source[ key ] ) )
                {
                    if ( !target[ key ] ) Object.assign( target, { [key]: {} } );
                    Helper.deepMerge( target[ key ], source[ key ] );
                }
                else
                {
                    Object.assign( target, { [key]: source[key] });
                }
            }
        }

        return Helper.deepMerge( target, ...sources );
    }

    /**
     * Create an observable object
     *
     * @param {function=} onChange - Callback triggered on change. Default is undefined.
     * @param {object=} objReference - Referenced object which will be transformed to an observable. Default is an empty new object.
     * @param {boolean=} batchUpDelay - Flag defining if change events are batched up for 10ms before being triggered. Default is true.
     * @returns {ProxyConstructor}
     */
    static createObservable( onChange = undefined, objReference = {}, batchUpDelay = true )
    {
        return ObservableSlim.create(
            objReference,
            batchUpDelay,
            onChange
        );
    }

    // Refer to:
    // https://github.com/lodash/lodash/blob/master/isObjectLike.js
    static _isObjectLike( value )
    {
        return typeof value === 'object' && value !== null
    }


    // Refer to:
    // https://github.com/lodash/lodash/blob/master/.internal/getTag.js
    static _getTag( value )
    {
        if ( value == null )
        {
            return value === undefined ? '[object Undefined]' : '[object Null]'
        }
        return Object.prototype.toString.call( value );
    }
}

export { Helper };