common-ui/src/lib/utils/functions.ts
2024-07-12 18:51:08 +03:00

329 lines
10 KiB
TypeScript

import { inject } from '@angular/core';
import { UntypedFormGroup } from '@angular/forms';
import { ActivatedRoute } from '@angular/router';
import dayjs, { type Dayjs } from 'dayjs';
import { forOwn, has, isEqual, isPlainObject, transform } from 'lodash-es';
import { Id, ITrackable } from '../listing/models/trackable';
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 size(value: number): string {
if (value >= 1000 ** 3) {
return `${(value / 1000 ** 3).toFixed(2)} GB`;
}
if (value >= 1000 ** 2) {
return `${(value / 1000 ** 2).toFixed(2)} MB`;
}
return `${(value / 1000).toFixed(2)} KB`;
}
interface IReplaceOptions {
readonly searchValue: string | RegExp;
readonly replaceValue: string;
}
const replacements: { searchValue: string; replaceValue: string }[] = [
{ searchValue: '&', replaceValue: '&' },
{ searchValue: ' ', replaceValue: ' ' },
{ searchValue: '<', replaceValue: '&lt;' },
{ searchValue: '>', replaceValue: '&gt;' },
{ searchValue: '"', replaceValue: '&quot;' },
{ searchValue: "'", replaceValue: '&#039;' },
];
const escapeReplacements: IReplaceOptions[] = replacements.map(({ searchValue, replaceValue }) => ({
searchValue: new RegExp(searchValue, 'g'),
replaceValue,
}));
const unescapeReplacements: IReplaceOptions[] = replacements.map(({ searchValue, replaceValue }) => ({
searchValue: new RegExp(replaceValue, 'g'),
replaceValue: searchValue,
}));
export function escapeHtml<T extends unknown | string>(unsafe: T, options?: { ignoreTags: string[] }) {
return replaceHtml(unsafe, {
ignoreTags: options?.ignoreTags,
replacements: escapeReplacements,
});
}
export function unescapeHtml<T extends unknown | string>(unsafe: T, options?: { ignoreTags: string[] }) {
return replaceHtml(unsafe, {
ignoreTags: options?.ignoreTags,
replacements: unescapeReplacements,
});
}
export function replaceHtml<T extends unknown | string>(
unsafe: T,
options: {
ignoreTags?: string[];
replacements: IReplaceOptions[];
},
) {
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;
for (const replacement of options.replacements) {
escaped = escaped.replaceAll(replacement.searchValue, replacement.replaceValue);
}
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, unknown>): 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;
}
export function getParamFromDialog(param: string, activatedRoute = inject(ActivatedRoute)) {
const getLastChild = (route: ActivatedRoute) => {
let child = route;
while (child.firstChild) {
child = child.firstChild;
}
return child;
};
return getParam(param, getLastChild(activatedRoute.root));
}