198 lines
5.9 KiB
TypeScript
198 lines
5.9 KiB
TypeScript
import type { IconComponentProps } from 'phosphor-svelte';
|
|
import type { Component } from 'svelte';
|
|
|
|
/**
|
|
* IconDef is an object that represents an icon element from the phosphor library, alongside an optional set of typed properties.
|
|
*/
|
|
export type IconDef = {
|
|
component: Component;
|
|
props?: IconComponentProps;
|
|
};
|
|
|
|
/**
|
|
* Defines a set of default properties for icons used in the application.
|
|
*/
|
|
export const defaultIconProps: IconComponentProps = {
|
|
size: '1.2rem',
|
|
weight: 'bold'
|
|
};
|
|
|
|
/**
|
|
* MaybeGetter is a type that can either be a value of type T or a function that returns a value of type T.
|
|
* This is useful for cases where you might want to pass a value directly or a function that computes the
|
|
* value later, potentially taking advantage of reactivity.
|
|
*/
|
|
export type MaybeGetter<T> = T | (() => T);
|
|
|
|
/**
|
|
* ResolveGetter returns the underlying value stored by a MaybeGetter type.
|
|
* @returns Raw value T or function return T.
|
|
*/
|
|
export const resolveGetter = <T>(getter: MaybeGetter<T>): T => {
|
|
if (typeof getter === 'function') {
|
|
return (getter as () => T)();
|
|
}
|
|
return getter;
|
|
};
|
|
|
|
/**
|
|
* Generates a unique identifier string unless an identifier is provided.
|
|
* If a prefix is provided, it will be prepended to the identifier.
|
|
* The identifier is a combination of a random part and a timestamp.
|
|
*
|
|
* @param {string} [prefix] - Optional prefix to prepend to the identifier.
|
|
* @param {string} [identifier] - Optional identifier to use instead of generating a new one.
|
|
* @returns {string} - A unique identifier string.
|
|
*/
|
|
export const generateIdentifier = (prefix?: string, identifier?: string): string => {
|
|
if (identifier) {
|
|
return `${prefix ? prefix + '-' : ''}${identifier}`;
|
|
}
|
|
const randomPart = Math.random().toString(36).substring(2, 10);
|
|
const timestampPart = Date.now().toString(36);
|
|
return `${prefix ? prefix + '-' : ''}${randomPart}-${timestampPart}`;
|
|
};
|
|
|
|
/**
|
|
* Option type definition for a select option. Used in various components.
|
|
*/
|
|
export type Option =
|
|
| {
|
|
value: string;
|
|
label?: string;
|
|
}
|
|
| string;
|
|
|
|
/** getLabel returns the option label if it exists*/
|
|
export const getLabel = (option: Option): string => {
|
|
if (typeof option === 'string') {
|
|
return option;
|
|
} else {
|
|
return option.label ?? option.value;
|
|
}
|
|
};
|
|
|
|
/** getValue returns the option value */
|
|
export const getValue = (option: Option): string => {
|
|
if (typeof option === 'string') {
|
|
return option;
|
|
} else {
|
|
return option.value;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* targetMust returns the target of an event coerced to a particular type and
|
|
* throws an error if the target does not exist or is not connected.
|
|
*
|
|
* @returns The target element coerced to a particular type.
|
|
* @throws Will throw an error if the target is null or not connected to the DOM.
|
|
*/
|
|
export function targetMust<T extends HTMLElement>(event: Event): T {
|
|
const target = event.target as T | null;
|
|
if (!target) {
|
|
throw new Error('Event target is null');
|
|
}
|
|
if (!target.isConnected) {
|
|
throw new Error('Event target is not connected to the DOM');
|
|
}
|
|
return target;
|
|
}
|
|
|
|
/**
|
|
* capitalizeFirstLetter capitalizes the first letter of a string
|
|
* @param str The string to capitalize
|
|
* @returns The string with the first letter capitalized and all others lowercase
|
|
*/
|
|
export const capitalizeFirstLetter = (str: string): string => {
|
|
const lower = str.toLowerCase();
|
|
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
|
};
|
|
|
|
/**
|
|
* prefixZero adds a leading zero to the string if it is less than 10 and not 0
|
|
* @param str The string to prefix
|
|
* @returns The string with a leading zero if it was only 1 digit long
|
|
*/
|
|
export const prefixZero = (str: string): string => {
|
|
if (str.length === 1 && str !== '0') {
|
|
return '0' + str;
|
|
}
|
|
return str;
|
|
};
|
|
|
|
/**
|
|
* Trims the specified character from the start and/or end of the string.
|
|
* @param str The string to trim.
|
|
* @param char The character to trim.
|
|
* @param trimStart Whether to trim from the start of the string. Default: true.
|
|
* @param trimEnd Whether to trim from the end of the string. Default: true.
|
|
* @returns The trimmed string.
|
|
*/
|
|
export const trimEdges = (str: string, char: string, trimStart?: boolean, trimEnd?: boolean) => {
|
|
let start = 0,
|
|
end = str.length;
|
|
|
|
if (trimStart || trimStart === undefined) {
|
|
while (start < end && str[start] === char) start++;
|
|
}
|
|
|
|
if (trimEnd || trimEnd === undefined) {
|
|
while (end > start && str[end - 1] === char) end--;
|
|
}
|
|
|
|
return str.substring(start, end);
|
|
};
|
|
|
|
// helper: only treat plain objects as mergeable
|
|
const isPlainObject = (v: unknown): v is Record<string, unknown> =>
|
|
typeof v === 'object' &&
|
|
v !== null &&
|
|
!Array.isArray(v) &&
|
|
Object.getPrototypeOf(v) === Object.prototype;
|
|
|
|
/** Merge two plain object maps. No `any` used. */
|
|
function mergePlainObjects(
|
|
baseObj: Record<string, unknown>,
|
|
overrideObj: Record<string, unknown>
|
|
): Record<string, unknown> {
|
|
const res: Record<string, unknown> = { ...baseObj };
|
|
|
|
for (const k of Object.keys(overrideObj)) {
|
|
const v = overrideObj[k];
|
|
if (v === undefined) continue; // undefined preserves base
|
|
const b = res[k];
|
|
|
|
if (isPlainObject(v) && isPlainObject(b)) {
|
|
res[k] = mergePlainObjects(b as Record<string, unknown>, v as Record<string, unknown>);
|
|
} else {
|
|
// primitives, null, arrays, non-plain objects replace base
|
|
res[k] = v;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Merge `base` with `override`.
|
|
* - `null` in `override` replaces (kept as valid override)
|
|
* - `undefined` in `override` is ignored (keeps base)
|
|
* - Only plain objects are deep-merged
|
|
* - If `override` is null/undefined we return a shallow copy of `base`
|
|
*/
|
|
export const mergeOverrideObject = <T extends Record<string, unknown>>(
|
|
base: T,
|
|
override?: Partial<T> | null
|
|
): T => {
|
|
if (override == null) return { ...base } as T;
|
|
|
|
// Use plain maps internally to avoid explicit any
|
|
const baseMap = { ...base } as Record<string, unknown>;
|
|
const overrideMap = override as Record<string, unknown>;
|
|
|
|
const merged = mergePlainObjects(baseMap, overrideMap);
|
|
|
|
return merged as T;
|
|
};
|