265 lines
8.0 KiB
TypeScript
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, '&')
|
|
.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<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;
|
|
}
|