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), '');
}
/**
* Collects form data as a plain object.
*
* - Text-ish inputs -> string
* - checkbox:
* - single checkbox name -> boolean
* - multiple checkboxes with same name -> array of checked values
* - radio:
* - selected value or null if none selected
* - select[multiple] -> array of values
* - file inputs -> FileList (or array of FileList if multiple inputs share the same name)
*
* @param {HTMLFormElement} form - The form element
* @param {boolean} [includeDisabled=false] - Include disabled form controls
* @returns {Object} Plain object of form data
*/
static serializeForm( form, includeDisabled = false)
{
if (!(form instanceof HTMLFormElement)) {
throw new Error("First parameter must be a form element");
}
const data = {};
const elements = Array.from(form.elements).filter(el => {
if (!el.name) return false;
if (!includeDisabled && el.disabled) return false;
return true;
});
// Pre-count names for special handling (checkbox/file groups)
const nameCounts = elements.reduce((acc, el) => {
const t = (el.type || "").toLowerCase();
const key = `${t}:${el.name}`;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
for (const el of elements) {
const type = (el.type || "").toLowerCase();
switch (type) {
case "checkbox": {
const key = `checkbox:${el.name}`;
const isGroup = nameCounts[key] > 1;
if (isGroup) {
// collect checked values into an array
if (!Array.isArray(data[el.name])) data[el.name] = [];
if (el.checked) data[el.name].push(el.value);
} else {
// single checkbox -> boolean
data[el.name] = !!el.checked;
}
break;
}
case "radio": {
// store the selected value; ensure null if none is selected
if (el.checked) {
data[el.name] = el.value;
} else if (!(el.name in data)) {
data[el.name] = null;
}
break;
}
case "select-multiple": {
data[el.name] = Array.from(el.selectedOptions).map(opt => opt.value);
break;
}
case "file": {
const key = `file:${el.name}`;
const isGroup = nameCounts[key] > 1;
// Return the native FileList as requested.
// If multiple <input type="file" name="..."> exist, return an array of FileLists.
if (isGroup) {
if (!Array.isArray(data[el.name])) data[el.name] = [];
data[el.name].push(el.files); // FileList
} else {
data[el.name] = el.files; // FileList (length 0..n)
}
break;
}
default: {
data[el.name] = el.value;
}
}
}
return data;
}
/**
* 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 };