Source

lib/helpers/utils.ts

import { isDevMode } from '@angular/core';
import { InjectableRegistryImp, InjectablesRegistry } from '@decaf-ts/injectable-decorators';
import { Primitives } from '@decaf-ts/decorator-validation';
import { KeyValue, StringOrBoolean, } from '../engine/types';
import { FunctionLike } from '../engine/types';
import { getLogger } from '../for-angular.module';

let injectableRegistry: InjectablesRegistry;

/**
 * @description Retrieves the singleton instance of the injectables registry
 * @summary This function implements the singleton pattern for the InjectablesRegistry.
 * It returns the existing registry instance if one exists, or creates a new instance
 * if none exists. The registry is used to store and retrieve injectable dependencies
 * throughout the application.
 *
 * @return {InjectablesRegistry} The singleton injectables registry instance
 *
 * @function getInjectablesRegistry
 * @memberOf module:for-angular
 */
export function getInjectablesRegistry(): InjectablesRegistry {
  if (!injectableRegistry)
    injectableRegistry = new InjectableRegistryImp();
  return injectableRegistry;
}

/**
 * @description Determines if the application is running in development mode
 * @summary This function checks whether the application is currently running in a development
 * environment. It uses Angular's isDevMode() function and also checks the window context
 * and hostname against the provided context parameter. This is useful for enabling
 * development-specific features or logging.
 *
 * @param {string} [context='localhost'] - The context string to check against the current environment
 * @return {boolean} True if the application is running in development mode, false otherwise
 *
 * @function isDevelopmentMode
 * @memberOf module:for-angular
 */
export function isDevelopmentMode(context: string = 'localhost'): boolean {
  if (!context)
    return isDevMode();
  const win = getWindow();
  return (
    isDevMode() ||
    win?.['env']?.['CONTEXT'].toLowerCase() !== context.toLowerCase() ||
    win?.['location']?.hostname?.includes(context)
  );
}

/**
 * @description Dispatches a custom event to the document window
 * @summary This function creates and dispatches a custom event to the browser window.
 * It's useful for cross-component communication or for triggering application-wide events.
 * The function allows specifying the event name, detail data, and additional event properties.
 *
 * @param {string} name - The name of the custom event to dispatch
 * @param {unknown} detail - The data to include in the event's detail property
 * @param {object} [props] - Optional additional properties for the custom event
 * @return {void}
 *
 * @function windowEventEmitter
 * @memberOf module:for-angular
 */
export function windowEventEmitter(
  name: string,
  detail: unknown,
  props?: object
): void {
  const data = Object.assign(
    {
      bubbles: true,
      composed: true,
      cancelable: false,
      detail: detail,
    },
    props || {}
  );
  (getWindow() as Window).dispatchEvent(new CustomEvent(name, data));
}
/**
 * @description Retrieves a property from the window's document object
 * @summary This function provides a safe way to access properties on the window's document object.
 * It uses the getWindowDocument function to get a reference to the document, then accesses
 * the specified property. This is useful for browser environment interactions that need
 * to access document properties.
 *
 * @param {string} key - The name of the property to retrieve from the document object
 * @return {any} The value of the specified property, or undefined if the document or property doesn't exist
 *
 * @function getOnWindowDocument
 * @memberOf module:for-angular
 */
export function getOnWindowDocument(key: string): Document | undefined {
  const doc = getWindowDocument()?.[key as keyof Document];
  return doc instanceof Document ?
    doc : undefined;
}

/**
 * @description Retrieves the document object from the window
 * @summary This function provides a safe way to access the document object from the window.
 * It uses the getOnWindow function to retrieve the 'document' property from the window object.
 * This is useful for browser environment interactions that need access to the document.
 *
 * @return {Document | undefined} The window's document object, or undefined if it doesn't exist
 *
 * @function getWindowDocument
 * @memberOf module:for-angular
 */
export function getWindowDocument(): Document | undefined {
  return getOnWindow('document') as Document;
}

/**
 * @description Retrieves a property from the window object
 * @summary This function provides a safe way to access properties on the window object.
 * It uses the getWindow function to get a reference to the window, then accesses
 * the specified property. This is useful for browser environment interactions that need
 * to access window properties or APIs.
 *
 * @param {string} key - The name of the property to retrieve from the window object
 * @return {unknown | undefined} The value of the specified property, or undefined if the window or property doesn't exist
 *
 * @function getOnWindow
 * @memberOf module:for-angular
 */
export function getOnWindow(key: string): unknown | undefined {
  return getWindow()?.[key];
}

/**
 * @description Sets a property on the window object
 * @summary This function provides a way to set properties on the window object.
 * It uses the getWindow function to get a reference to the window, then sets
 * the specified property to the provided value. This is useful for storing
 * global data or functions that need to be accessible across the application.
 *
 * @param {string} key - The name of the property to set on the window object
 * @param {any} value - The value to assign to the property
 * @return {void}
 *
 * @function setOnWindow
 * @memberOf module:for-angular
 */
export function setOnWindow(key: string, value: unknown): void {
  getWindow()[key] = value;
}

/**
 * @description Retrieves the global window object
 * @summary This function provides a safe way to access the global window object.
 * It uses globalThis to ensure compatibility across different JavaScript environments.
 * This is the core function used by other window-related utility functions to
 * access the window object.
 *
 * @return {Window} The global window object
 *
 * @function getWindow
 * @memberOf module:for-angular
 */
export function getWindow(): Window & KeyValue {
  return (globalThis as KeyValue)?.['window'] as Window & KeyValue;
}

/**
 * @description Retrieves the width of the browser window
 * @summary This function provides a convenient way to get the current width of the browser window.
 * It uses the getOnWindow function to access the 'innerWidth' property of the window object.
 * This is useful for responsive design implementations and viewport-based calculations.
 *
 * @return {number | undefined} The current width of the browser window in pixels
 *
 * @function getWindowWidth
 * @memberOf module:for-angular
 */
export function getWindowWidth(): number {
  return getOnWindow('innerWidth') as number || 0;
}

/**
 * @description Checks if a value is  not undefined
 * @summary This utility function determines whether a given value is not undefined.
 * It's a simple wrapper that makes code more readable when checking for defined values.
 * The function is particularly useful for checking StringOrBoolean properties that might be undefined.
 *
 * @param {StringOrBoolean | undefined} prop - The property to check
 * @return {boolean} True if the property is not undefined, false otherwise
 *
 * @function isNotUndefined
 * @memberOf module:for-angular
 */
export function isNotUndefined(prop: StringOrBoolean | undefined): boolean {
  return (prop !== undefined) as boolean;
}

/**
 * @description Generates a locale string from a class name or instance
 * @summary This utility function converts a class name or instance into a locale string
 * that can be used for internationalization purposes. It handles different input types
 * (string, function, or object) and applies formatting rules to generate a consistent
 * locale identifier. For short names (less than 3 parts), it reverses the dot-separated
 * string. For longer names, it uses the last part as a prefix and joins the rest with
 * underscores.
 *
 * @param {string|FunctionLike|object} instance - The input to generate the locale from (class name, constructor, or instance)
 * @param {string} [suffix] - Optional string to append to the instance name before processing
 * @return {string} A formatted locale string derived from the input
 *
 * @function getLocaleFromClassName
 * @memberOf module:for-angular
 */
export function getLocaleFromClassName(
  instance: string | FunctionLike | object,
  suffix?: string
): string {
  if (typeof instance !== 'string')
    instance =
      (instance as FunctionLike).name || (instance as object)?.constructor?.name;

  let name: string | string[] = instance;

  if (suffix) name = `${instance}${suffix.charAt(0).toUpperCase() + suffix.slice(1)}`;

  name = name
    .replace(/_|-/g, '')
    .replace(/(?:^\w|[A-Z]|\b\w)/g, (word: string, index: number) => {
      if (index > 1) word = '.' + word;
      return word.toLowerCase();
    })
    .split('.');

  if (name.length < 3)
    return name.reverse().join('.');

  const preffix = name[name.length - 1];
  name.pop();
  name = name.join('_');
  return `${preffix}.${name}`;


}

/**
 * @description Generates a localized string by combining locale and phrase
 * @summary This utility function creates a properly formatted locale string by combining
 * a locale identifier with a phrase. It handles edge cases such as empty phrases,
 * missing locales, and phrases that already include the locale prefix. This function
 * is useful for ensuring consistent formatting of localized strings throughout the application.
 *
 * @param {string} locale - The locale identifier (e.g., 'en', 'fr')
 * @param {string | undefined} phrase - The phrase to localize
 * @return {string} The formatted locale string, or empty string if phrase is undefined
 *
 * @function generateLocaleFromString
 * @memberOf module:for-angular
 */
export function generateLocaleFromString(
  locale: string,
  phrase: string | undefined
): string {
  if (!phrase) return '';
  if (!locale || phrase.includes(`${locale}.`)) return phrase;
  return `${locale}.${phrase}`;
}


/**
 * @description Retrieves the current locale language
 * @summary This utility function gets the current locale language based on the user's browser settings.
 * It provides a consistent way to access the user's language preference throughout the application.
 * The function returns the browser's navigator.language value, defaulting to 'en' if not available.
 *
 * @return {string} The current locale language (e.g., 'en', 'fr')
 *
 * @function getLocaleLanguage
 * @memberOf module:for-angular
 */
export function getLocaleLanguage(): string {
  const win = getWindow();
  return (win as Window).navigator.language || "en";
  // return win?.[WINDOW_KEYS.LANGUAGE_SELECTED] || (win.navigator.language || '').split('-')[0] || "en";
}



/**
 * @description Generates a random string or number of specified length
 * @summary This utility function creates a random string of a specified length.
 * It can generate either alphanumeric strings (including uppercase and lowercase letters)
 * or numeric-only strings. This is useful for creating random IDs, temporary passwords,
 * or other random identifiers throughout the application.
 *
 * @param {number} [length=8] - The length of the random value to generate
 * @param {boolean} [onlyNumbers=false] - Whether to generate only numeric characters
 * @return {string} A randomly generated string of the specified length and character set
 *
 * @function generateRandomValue
 * @memberOf module:for-angular
 */
export function generateRandomValue(length: number = 8, onlyNumbers: boolean = false): string {
  const chars = onlyNumbers
    ? '0123456789'
    : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++)
    result += chars.charAt(Math.floor(Math.random() * chars.length));

  return result;
}


/**
 * Converts a string representation of a boolean or a boolean value to a boolean type.
 *
 * @export
 * @param {('true' | 'false' | boolean)} prop - The value to convert. Can be the string 'true', 'false', or a boolean.
 * @returns {boolean} The boolean representation of the input value. Returns true if the input is the string 'true' or boolean true, false otherwise.
 */
export function stringToBoolean(prop: 'true' | 'false' | boolean): boolean {
  if(typeof prop === 'string')
    prop = prop.toLowerCase() === 'true' ? true : false;
  return prop;
}


/**
 * Checks if a value is a valid Date object
 *
 * @param {(string | Date | number)} date - The value to check. Can be a Date object, a timestamp number, or a date string
 * @return {boolean} Returns true if the value is a valid Date object (not NaN), otherwise false
 */
export function isValidDate(date: string | Date | number): boolean {
  try {
    return (date instanceof Date && !isNaN(date as unknown as number)) || (() => {
      const testRegex = new RegExp(/^\d{4}-\d{2}-\d{2}$/).test(date as string)
      if(typeof date !== Primitives.STRING || !(date as string)?.includes('T') && !testRegex)
         return false;

     date = (date as string).split('T')[0];
    if(!new RegExp(/^\d{4}-\d{2}-\d{2}$/).test(date))
      return false;

    return !!(new Date(date));
   })();
  } catch(error: unknown) {
    getLogger(isValidDate).error(error as Error | string);
    return false;
  }
}

/**
 * Formats a date into a localized string representation
 *
 * @param {(string | Date | number)} date - The date to format. Can be a Date object, a timestamp number, or a date string
 * @param {string} [locale] - The locale to use for formatting. If not provided, the system's locale will be used
 * @return {(Date | string)} A formatted date string in the format DD/MM/YYYY according to the specified locale,
 *                           or the original input as a string if the date is invalid
 */
export function formatDate(date: string | Date | number, locale?: string | undefined): Date | string {

  if(!locale)
    locale = getLocaleLanguage();

  if(typeof date === 'string' || typeof date === 'number')
    date = new Date(typeof date === 'string' ? date.replace(/\//g, '-') : date);

  if(!isValidDate(date))
    return `${date}` as string;
  const r = date.toLocaleString(locale, {
      year: "numeric",
      day: "2-digit",
      month: '2-digit'
  });


  return r;
}

/**
 * Attempts to parse a date string, Date object, or number into a valid Date object
 *
 * @param {(string | Date | number)} date - The date to parse. Can be a Date object, a timestamp number,
 *                                         or a date string in the format "DD/MM/YYYY HH:MM:SS:MS"
 * @return {(Date | null)} A valid Date object if parsing is successful, or null if the date is invalid
 *                         or doesn't match the expected format
 */
export function parseToValidDate(date: string | Date | number): Date | null {
  if(isValidDate(date))
    return date as Date;

  if(!`${date}`.includes('/'))
    return null;

  const [dateString, timeString] = (date as string).split(' ');
  const [day, month, year] = dateString.split('/').map(Number);
  const [hours, minutes, seconds, milliseconds] = timeString.split(':').map(Number);
  date = new Date(year, month - 1, day, hours, minutes, seconds, milliseconds);

  if(!isValidDate(date)) {
    console.warn('parseToValidDate - Invalid date format', date);
    return null;
  }

  return date;
}


/**
 * Maps an item object using a provided mapper object and optional additional properties.
 *
 * @param {KeyValue} item - The source object to be mapped.
 * @param {KeyValue} mapper - An object that defines the mapping rules. Keys represent the new property names,
 *                            and values represent the path to the corresponding values in the source object.
 * @param {KeyValue} [props] - Optional additional properties to be included in the mapped object.
 * @returns {KeyValue} A new object with properties mapped according to the mapper object and including any additional properties.
 */
export function itemMapper(item: KeyValue, mapper: KeyValue, props?: KeyValue): KeyValue {
  return Object.entries(mapper).reduce((accum: KeyValue, [key, value]) => {
    const arrayValue = (value as string).split('.');
    if (!value) {
      accum[key] = value;
    } else {
      if (arrayValue.length === 1) {
        accum[key] = item?.[value as string] || (value !== key ? value : "");
      } else {
        let val;

        for (const _value of arrayValue)
          val = !val
            ? item[_value]
            : (typeof val === 'string' ? JSON.parse(val) : val)[_value];

        if (isValidDate(new Date(val))) val = `${formatDate(val)}`;

        accum[key] = val === null || val === undefined ? value : val;
      }
    }
    return Object.assign({}, props || {}, accum);
  }, {});
}

/**
 * Maps an array of data objects using a provided mapper object.
 *
 * @template T - The type of the resulting mapped items.
 * @param {any[]} data - The array of data objects to be mapped.
 * @param {KeyValue} mapper - An object that defines the mapping rules.
 * @param {KeyValue} [props] - Additional properties to be included in the mapped items.
 *
 * @returns {T[]} - The array of mapped items. If an item in the original array does not have any non-null values after mapping,
 * the original item is returned instead.
 */
export function dataMapper<T>(data: T[], mapper: KeyValue, props?: KeyValue): T[] {
  if (!data || !data.length) return [];
  return data.reduce((accum: T[], curr) => {
    const item = itemMapper(curr as KeyValue, mapper, props) as T;
    const hasValues =
      [...new Set(Object.values(item as T[]))].filter((value) => value).length >
      0;
    // caso o item filtrado não possua nenhum valor, passar o objeto original
    accum.push(hasValues ? item : curr);
    return accum;
  }, []);
}


export function removeFocusTrap(): void {
  const doc = getWindowDocument();
  if(doc?.activeElement)
    (doc.activeElement as HTMLElement)?.blur();
}

export function cleanSpaces(value: string, lowercase = false): string {
  value = `${value}`.trim().replace(/\s+/g, ' ');
  return lowercase ? value.toLowerCase() : value;
}