common-ui/src/lib/utils/functions.ts

265 lines
8.0 KiB
TypeScript

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<T extends unknown | string>(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}>`]: `???/${tag};`,
}),
{} as Record<string, string>,
);
Object.entries(ignoredTags ?? {}).forEach(([key, value]) => {
_unsafe = _unsafe.replaceAll(key, value);
});
let escaped = _unsafe
.replaceAll(/&/g, '&amp;')
.replaceAll(/ /g, '&nbsp;')
.replaceAll(/</g, '&lt;')
.replaceAll(/>/g, '&gt;')
.replaceAll(/"/g, '&quot;');
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<T>(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<T extends ITrackable<PrimaryKey>, PrimaryKey extends Id = T['id']>() {
return (_index: number, item: T): Id => item.id;
}
export function hasFormChanged(form: UntypedFormGroup, initialFormValue: Record<string, string | number | boolean>): 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<string, unknown>, object: Record<string, unknown>) {
if (!object) {
throw new Error(`The object compared should be an object: ${object}`);
}
if (!base) {
return object;
}
const res = transform(object, (result: Record<string, unknown>, 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<string, unknown>, value as Record<string, unknown>)
: 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<T, Q>(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<Q, T[]>());
}
declare global {
interface String {
capitalize(): string;
}
interface Array<T> {
/**
* 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<Key>(condition: (value: T) => Key): Map<Key, T[]>;
}
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 <T>(this: T[], predicate: (value: T) => boolean = () => true): T[] {
return this.filter(value => !!value && predicate(value));
};
Array.prototype.groupBy = function <T, Key>(this: T[], condition: (value: T) => Key): Map<Key, T[]> {
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;
}