/** * Determines whether the current runtime environment is a web browser. * * This function works by attempting to access `this` within an anonymous function * and checking if it refers to the global `window` object. It returns `true` * when executed in a browser context, and `false` otherwise (e.g., Node.js). * * Note: While modern environments typically provide more reliable detection mechanisms, * this fallback remains compatible across older runtimes where other detection techniques * may be unreliable. * * @returns {boolean} `true` if the code is running in a browser environment, `false` otherwise. */ export function isBrowser() { return window ? true : false; } /** * Interface for objects that can have string or symbol keys with any type of values. */ export interface LooseObject { [key: string | symbol]: any; } /** * Checks if a value is a native JavaScript object (created via {} or new Object()). * @param value - The value to check. * @returns True if the value is a plain object, false otherwise. */ export function isNativeObject(value: any): boolean { return value && value['constructor'] && value.constructor === Object; } /** * Recursively sorts all keys in an object alphabetically, preserving nested structure. * @param o - The object to sort. * @returns A new object with sorted keys at all levels. */ export function sortObject(o: LooseObject): LooseObject { let result: LooseObject = {}, keys = [...Object.keys(o)].sort(); keys.forEach((key) => { if (isNativeObject(o[key])) { result[key] = sortObject(o[key]); } else { result[key] = o[key]; } }); return result; } /** * Flattens a nested object into a single-level object with dot-separated keys. * Optionally sorts the resulting flat keys alphabetically (default: true). * @param o - The object to flatten. * @param doSort - Whether to sort the resulting keys (default: true). * @returns A flattened object with dot-notation keys. */ export function flattenObject(o: LooseObject, doSort: boolean = true): LooseObject { let result: LooseObject = {}; [...Object.getOwnPropertyNames(o)].forEach((propName) => { if (o[propName] instanceof Object) { const temp = flattenObject(o[propName], false); result[propName] = {}; for (const j in temp) { result[propName + '.' + j] = temp[j]; } } else { result[propName] = o[propName]; } }); return doSort ? sortObject(result) : result; } /** * Compares two objects for deep equality by flattening them and comparing their JSON representations. * @param o1 - The first object to compare. * @param o2 - The second object to compare. * @returns True if the objects are deeply equal, false otherwise. */ export function deepEqual(o1: LooseObject, o2: LooseObject): boolean { return JSON.stringify(flattenObject(o1)) === JSON.stringify(flattenObject(o2)); } /** * Retrieves a nested value from an object using a namespace path (dot-separated). * @param ns - The namespace string or array of keys representing the path. * @param obj - The object to traverse. * @returns The value at the specified namespace path, or undefined if not found. */ export function nameSpace(ns: string | string[], obj: LooseObject = {}): any { let nsa = ns.constructor === String ? ns.split('.') : ns; if (!obj || !nsa[0] || !obj[nsa[0]]) { console.warn('ns', nsa[0], 'not in', obj); return; } if (obj[nsa[0]].constructor === Object && nsa.length > 1) { return nameSpace((nsa as string[]).splice(1), obj[nsa[0]]); } return obj[nsa[0]]; } /** * Parses a template string or object, replacing placeholders in the form of {key.path} with * corresponding values from the provided data object using namespace resolution. * Also recursively processes objects and arrays within the input. * @param what - The string or object to parse (can contain {{placeholder}} expressions). * @param parseThis - The object containing values for placeholder substitution. * @returns The parsed result, with placeholders replaced by actual values. */ export function parse(what: string | object | any[], parseThis: object) { var args = Array.from(arguments); if (args.length > 2) { while (args.length > 1) { args[0] = parse(args[0], args[1]); args.splice(1, 1); } return args[0]; } if (typeof what === 'string') { what.match(/\{[^\{\}]*\}/g)?.forEach(function (pPropname) { var propname = pPropname.substring(1, pPropname.length - 1), value = nameSpace(propname, parseThis); //console.log(propname, value); if (typeof value !== 'undefined') { what = (what as string).replace(pPropname, value); } }); } else if (isNativeObject(what)) { switch (what.constructor) { case Object: Object.keys(what).forEach(function (pKey) { if (what.hasOwnProperty(pKey)) { (what as LooseObject)[pKey] = parse((what as LooseObject)[pKey], parseThis); } }); break; case Array: (what as Array).forEach(function (pValue, pKey, original) { original[pKey] = parse((what as any)[pKey], parseThis); }); break; } } return what; } /** * Converts a kebab-case string to PascalCase. * Example: 'my-component-name' -> 'MyComponentName' * @param str - The kebab-case string to convert. * @returns The converted PascalCase string. */ export function kebabToPascal(str: string) { return str .split('-') // Split by hyphens .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) // Capitalize each part .join(''); // Join without spaces } /** * Creates a debounced version of a function that delays execution until after the specified * number of milliseconds have elapsed since the last time this function was invoked. * Useful for rate-limiting expensive operations like event handlers or API calls. * @param func - The function to debounce. * @param milliseconds - The number of milliseconds to delay execution. * @returns A debounced version of the input function. */ export function debounce(func: Function, milliseconds: number): Function { let timeout: any; return () => { clearTimeout(timeout); timeout = setTimeout(function () { func(...arguments); }, milliseconds); }; } /** * Parses an HTML string and returns an array of DOM elements. * Handles self-closing tags (XHTML style) by converting them to full closing tags first. * @param html - The HTML string to parse. * @returns An array of HTMLElement objects representing the parsed HTML nodes. */ export function htmlToElements(html: string): HTMLElement[] { let template = document.createElement('template'); html = html.replace(/<([A-Za-z0-9\-]*)([^>\/]*)(\/>)/gi, '<$1$2>'); // replace XHTML closing tags by full closing tags template.innerHTML = html; template.content.normalize(); return Array.from(template.content.childNodes) as HTMLElement[]; } /** * Copies all getter and setter properties from a source object to a target object. * Does not copy methods or regular data properties—only accessor properties (get/set). * @param source - The source object with getters and/or setters to copy. * @param target - The target object to which the accessors will be copied. */ export function copyGettersSetters(source: LooseObject, target: LooseObject) { const descriptors = Object.getOwnPropertyDescriptors(source); for (const [key, descriptor] of Object.entries(descriptors)) { if ('get' in descriptor || 'set' in descriptor) { Object.defineProperty(target, key, descriptor); } } }