import type { Id, ITrackable } from '../listing'; import { UntypedFormGroup } from '@angular/forms'; import { forOwn, has, isEqual, isPlainObject, transform } from 'lodash-es'; import dayjs, { type Dayjs } from 'dayjs'; import { inject } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; export function capitalize(value: string | String): string { if (!value) { return ''; } return value.charAt(0).toUpperCase() + value.slice(1); } export function humanize(value: string, lowercase = true): string { if (!value) { return ''; } const words = (lowercase ? value.toLowerCase() : value).split(/[ \-_]+/); return words.map(capitalize).join(' '); } export function humanizeCamelCase(value: string): string { return value.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase(); } export function escapeHtml(unsafe: T, options?: { ignoreTags: string[] }) { if (typeof unsafe !== 'string') { return unsafe; } let _unsafe = unsafe as string; const ignoredTags = options?.ignoreTags?.reduce( (acc, tag) => ({ ...acc, [`<${tag}>`]: `???${tag};`, [``]: `???/${tag};`, }), {} as Record, ); Object.entries(ignoredTags ?? {}).forEach(([key, value]) => { _unsafe = _unsafe.replaceAll(key, value); }); let escaped = _unsafe .replaceAll(/&/g, '&') .replaceAll(/ /g, ' ') .replaceAll(//g, '>') .replaceAll(/"/g, '"'); if (ignoredTags) { Object.entries(ignoredTags).forEach(([key, value]) => { escaped = escaped.replaceAll(value, key); }); } return escaped; } export function _log(value: unknown, message = '') { console.log(`%c[${dayjs().format('mm:ss.SSS')}] ${message}`, 'color: yellow;', value); } export function toNumber(str: string): number { try { return parseInt(`${str}`.replace(/\D/g, ''), 10); } catch (e) { return 0; } } export function randomString() { return Math.random().toString(36).substring(2, 9); } export function isJustOne(list: T[]): list is [T] { return list.length === 1; } export function isToday(date: string | Date | Dayjs) { return dayjs(date).isSame(new Date(), 'day'); } export function trackByFactory, PrimaryKey extends Id = T['id']>() { return (_index: number, item: T): Id => item.id; } export function hasFormChanged(form: UntypedFormGroup, initialFormValue: Record): boolean { if (!form || !initialFormValue) { return false; } for (const key of Object.keys(form.getRawValue())) { const initialValue = initialFormValue[key]; const updatedValue = form.get(key)?.value; if (initialValue === null && updatedValue !== null) { const updatedValueType = typeof updatedValue; if (updatedValueType !== 'string' && updatedValueType !== 'boolean') { return true; } else if (updatedValueType === 'string' && updatedValue.length > 0) { return true; } else if (updatedValueType === 'boolean' && updatedValue === true) { return true; } } else if (initialValue !== updatedValue) { if (Array.isArray(updatedValue)) { if (JSON.stringify(initialValue) !== JSON.stringify(updatedValue)) { return true; } } else if (updatedValue instanceof dayjs && typeof initialValue === 'string') { if (!(updatedValue as Dayjs).isSame(dayjs(initialValue), 'day')) { return true; } } else if (updatedValue instanceof Date && dayjs(initialValue as any).isValid()) { if (!dayjs(updatedValue).isSame(dayjs(initialValue as any), 'day')) { return true; } } else { return true; } } } return false; } const HOURS_IN_A_DAY = 24; const MINUTES_IN_AN_HOUR = 60; export function getLeftDateTime(ISOString: string) { const date = dayjs(ISOString); const now = new Date(Date.now()); const daysLeft = date.diff(now, 'days'); const hoursFromNow = date.diff(now, 'hours'); const hoursLeft = hoursFromNow - HOURS_IN_A_DAY * daysLeft; const minutesFromNow = date.diff(now, 'minutes'); const minutesLeft = minutesFromNow - HOURS_IN_A_DAY * MINUTES_IN_AN_HOUR * daysLeft; return { daysLeft, hoursLeft, minutesLeft }; } export function deepDiffObj(base: Record, object: Record) { if (!object) { throw new Error(`The object compared should be an object: ${object}`); } if (!base) { return object; } const res = transform(object, (result: Record, value: any, key: string) => { if (!has(base, key)) { result[key] = value; } // fix edge case: not defined to explicitly defined as undefined if (!isEqual(value, base[key])) { result[key] = isPlainObject(value) && isPlainObject(base[key]) ? deepDiffObj(base[key] as Record, value as Record) : value; } }); // map removed fields to undefined forOwn(base, (_value: any, key: string) => { if (!has(object, key)) { res[key] = undefined; } }); return res; } export function bool(value: unknown): boolean { if (typeof value !== 'string') { return Boolean(value); } const _value = value.toLowerCase().trim(); if (_value === 'true') { return true; } if (_value === 'false') { return false; } return Boolean(_value); } export function groupBy(array: T[], predicate: (value: T, index: number, items: T[]) => Q) { return array.reduce((dict, value, index, items) => { const key = predicate(value, index, items); if (dict.has(key)) { const group = dict.get(key); if (!group) { throw new Error(`Oh, why, group ${key} is undefined`); } group.push(value); return dict; } return dict.set(key, [value]); }, new Map()); } declare global { interface String { capitalize(): string; } interface Array { /** * Returns a new array with all falsy values removed. * The values false, null, 0, "", undefined, and NaN are considered falsy. * @param and - Additional function that is called for each truthy element in the array. * The value returned from the function determines whether the element is kept or removed. */ filterTruthy(and?: (value: T) => boolean): T[]; groupBy(condition: (value: T) => Key): Map; } interface Console { /** * Logs a beautifully formatted message to the console. * @param value - The object to log. * @param message - Additional message. */ write(value: unknown, message?: string): void; } } console.write = _log; String.prototype.capitalize = function _capitalize(this: string): string { return capitalize(this); }; Array.prototype.filterTruthy = function (this: T[], predicate: (value: T) => boolean = () => true): T[] { return this.filter(value => !!value && predicate(value)); }; Array.prototype.groupBy = function (this: T[], condition: (value: T) => Key): Map { return groupBy(this, condition); }; /** * Use this in field initialization or in constructor of a service / component * @param param * @param route */ export function getParam(param: string, route = inject(ActivatedRoute)): string | null { if (route.snapshot.paramMap.has(param)) { return route.snapshot.paramMap.get(param); } if (route.parent) { return getParam(param, route.parent); } return null; }