web_lib

Common web application libraries
git clone https://radroots.dev/git/web_lib.git
Log | Files | Refs | LICENSE

index.ts (8827B)


      1 import { browser } from '$app/environment';
      2 import { goto } from '$app/navigation';
      3 import { win_h, win_w } from '$lib/stores/app';
      4 import type { CallbackRoute, NavigationParamTuple, NavigationRouteParamKey, NavigationRouteParamTuple, } from '$lib/types/lib';
      5 import type { ThemeLayer, ThemeMode } from '@radroots/themes';
      6 import type { WebFilePath } from '@radroots/utils';
      7 import { getContext, setContext } from "svelte";
      8 import { get } from "svelte/store";
      9 
     10 export const SYMBOLS = {
     11     bullet: '•',
     12     dash: `—`,
     13     up: `↑`,
     14     down: `↓`,
     15     percent: `%`
     16 };
     17 
     18 export const get_store = get;
     19 
     20 export const get_context = <T>(key: string): T =>
     21     getContext<T>(key);
     22 
     23 export const set_context = <T>(key: string, value: T): T =>
     24     setContext(key, value);
     25 
     26 export const sleep = (ms: number): Promise<void> =>
     27     new Promise((r) => setTimeout(r, ms));
     28 
     29 export const trim_slashes = (path: string): string =>
     30     path.replace(/^\/+|\/+$/g, '');
     31 
     32 export const normalize_path = (path: string): string =>
     33     path
     34         .replace(/-/g, '_')
     35         .replace(/\//g, '-')
     36         .replace(/-+/g, '-');
     37 
     38 export const sanitize_path = (id: string): string =>
     39     id.replace(/[^A-Za-z0-9_-]+/g, '');
     40 
     41 export const fmt_id = (raw_id?: string): string => {
     42     if (!browser) return '';
     43     const pathname = window.location.pathname;
     44     const trimmed = trim_slashes(pathname);
     45     const prefix = normalize_path(trimmed);
     46     const suffix = raw_id ? `-${sanitize_path(raw_id)}` : '';
     47     return `*${prefix}${suffix}`;
     48 };
     49 
     50 export const view_effect = <T extends string>(view: T): void => {
     51     if (!browser) return;
     52     for (const el of document.querySelectorAll(`[data-view]`)) {
     53         if (el.getAttribute(`data-view`) !== view) el.classList.add(`hidden`)
     54         else el.classList.remove(`hidden`)
     55     }
     56 };
     57 
     58 export const el_id = (id: string): HTMLElement | undefined => {
     59     if (!browser) return undefined;
     60     const el = document.getElementById(id);
     61     return el ? el : undefined;
     62 };
     63 
     64 export const build_storage_key = (
     65     raw_id: string,
     66     base_prefix: string
     67 ): string =>
     68     `${fmt_id()}-${sanitize_path(raw_id)}`
     69         .replace(new RegExp(`^\\*${normalize_path(trim_slashes(base_prefix))}-?`), '*');
     70 
     71 export const get_system_theme = (fallback: ThemeMode = "light"): ThemeMode => {
     72     if (!browser) return fallback;
     73     return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
     74 };
     75 
     76 export const theme_set = (theme_key: string, color_mode: ThemeMode): void => {
     77     if (!browser) return;
     78     document.documentElement.setAttribute("data-theme", `${theme_key}_${color_mode}`);
     79 };
     80 export const fmt_cl = (classes?: string): string => `${classes || ``}`;
     81 
     82 export const handle_err = async (e: unknown, fcall: string): Promise<void> => {
     83     try {
     84         console.log(`[handle_err] `, e, fcall)
     85         /*return void await catch_err(e, fcall, async (opts) => {
     86             console.log(`handle_err e `, e)
     87             console.log(JSON.stringify(opts, null, 4), `handle_err opts`)
     88         });*/
     89     } catch (e) {
     90         console.log(`(handle_err) `, e)
     91     }
     92 };
     93 
     94 export const window_set = (): void => {
     95     if (!browser) return;
     96     win_h.set(window.innerHeight);
     97     win_w.set(window.innerWidth);
     98 };
     99 
    100 export const parse_layer = (layer?: number, layer_default?: ThemeLayer): ThemeLayer => {
    101     switch (layer) {
    102         case 0:
    103         case 1:
    104         case 2:
    105             return layer;
    106         default:
    107             return layer_default ?? 0;
    108     };
    109 };
    110 
    111 export const value_constrain = (regex_charset: RegExp, value: string): string => {
    112     return value
    113         .split(``)
    114         .filter((char) => regex_charset.test(char))
    115         .join(``);
    116 };
    117 
    118 
    119 export const encode_query_params = <T extends string>(params_list: NavigationParamTuple<T>[] = []): string => {
    120     let query = "";
    121     for (const [k, v] of params_list) {
    122         if (k && v) {
    123             if (query) query += `&`;
    124             query += `${k.trim()}=${encodeURIComponent(v.trim())}`;
    125         }
    126     }
    127     return query ? `?${query}` : ``;
    128 };
    129 
    130 export const encode_route = <TRoute extends string, TParam extends string>(route: TRoute, params_list?: NavigationParamTuple<TParam>[]): string => {
    131     const query = encode_query_params(params_list);
    132     if (!query) return route;
    133     return `${route === `/` ? `/` : route.replace(/\/+$/, ``)}${query}`;
    134 };
    135 
    136 export const debounce = <TArgs extends readonly unknown[]>(
    137     fn: (...args: TArgs) => void,
    138     delay: number
    139 ): ((...args: TArgs) => void) => {
    140     let timeout: ReturnType<typeof setTimeout> | undefined;
    141     return (...args: TArgs) => {
    142         if (timeout) clearTimeout(timeout);
    143         timeout = setTimeout(() => fn(...args), delay);
    144     };
    145 };
    146 
    147 export const create_router = <T extends string>(): ((nav_route: T, params?: NavigationRouteParamTuple[]) => Promise<void>) => {
    148     const router = async (nav_route: T, params: NavigationRouteParamTuple[] = []): Promise<void> => {
    149         try {
    150             if (params.length) await goto(encode_route<T, NavigationRouteParamKey>(nav_route, params));
    151             else await goto(nav_route);
    152         } catch (e) {
    153             handle_err(e, `route`);
    154         };
    155     };
    156     return router;
    157 };
    158 
    159 export const get_locale = (locales: string[]): string => {
    160     if (!browser) return (locales[0] ?? `en`).toLowerCase();
    161     const { language: navigator_locale } = navigator;
    162     let locale = `en`;
    163     if (locales.some(i => i === navigator_locale.toLowerCase())) locale = navigator.language;
    164     else if (locales.some(i => i === navigator_locale.slice(0, 2).toLowerCase())) locale = navigator_locale.slice(0, 2);
    165     return locale.toLowerCase();
    166 };
    167 
    168 export const callback_route = async <T extends string>(callback_route: CallbackRoute<T>): Promise<void> => {
    169     if (`route` in callback_route) {
    170         if (typeof callback_route.route === `string`) return void await goto(callback_route.route);
    171         else return void await goto(
    172             encode_route<string, NavigationRouteParamKey>(
    173                 callback_route.route[0],
    174                 callback_route.route[1],
    175             ),
    176         );
    177     }
    178     return void await callback_route();
    179 };
    180 
    181 export const to_arr_buf = (u8: Uint8Array): ArrayBuffer => {
    182     if (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength && u8.buffer instanceof ArrayBuffer) return u8.buffer;
    183     if (u8.buffer instanceof ArrayBuffer) return u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
    184     const copy = new Uint8Array(u8.byteLength);
    185     copy.set(u8);
    186     return copy.buffer;
    187 };
    188 
    189 export const parse_file_path = (file_path: string): WebFilePath | undefined => {
    190     if (file_path.startsWith("blob:")) return { blob_path: file_path, blob_name: file_path.replaceAll("blob:", "").replaceAll("http://", "") };
    191     const file_path_spl = file_path.split(`/`);
    192     const file_path_file = file_path_spl[file_path_spl.length - 1] || ``;
    193     const [file_name, mime_type] = file_path_file.split(`.`);
    194     if (!file_name || !mime_type) return undefined;
    195     return {
    196         file_path,
    197         file_name,
    198         mime_type
    199     };
    200 };
    201 
    202 export const download_json = <T>(data: T, filename: string): void => {
    203     if (!browser) return;
    204     const json = JSON.stringify(data, null, 2);
    205     const blob = new Blob([json], { type: "application/json" });
    206     const url = URL.createObjectURL(blob);
    207     const anchor = document.createElement("a");
    208     anchor.href = url;
    209     anchor.download = filename;
    210     anchor.click();
    211     URL.revokeObjectURL(url);
    212 };
    213 
    214 export const select_file = async (): Promise<File | undefined> => {
    215     if (!browser) return undefined;
    216     return new Promise((resolve) => {
    217         const input = document.createElement("input");
    218         input.type = "file";
    219         input.accept = "*/*";
    220         input.style.display = "none";
    221         const cleanup = () => {
    222             input.remove();
    223         };
    224         input.addEventListener("change", async () => {
    225             const file = input.files?.[0];
    226             cleanup();
    227             resolve(file ?? undefined);
    228         });
    229         document.body.appendChild(input);
    230         input.click();
    231     });
    232 };
    233 
    234 export const get_file_text = async (file: File | null): Promise<string | undefined> => {
    235     if (!file) return undefined;
    236     const text = await file.text();
    237     return text;
    238 };
    239 
    240 export type ParseJsonResult = { ok: true; value: unknown } | { ok: false; error: Error };
    241 
    242 export const parse_file_json = async (file: File | null): Promise<ParseJsonResult> => {
    243     const contents = await get_file_text(file);
    244     if (!contents) return { ok: false, error: new Error("empty_file") };
    245     try {
    246         const parsed: unknown = JSON.parse(contents);
    247         return { ok: true, value: parsed };
    248     } catch (error) {
    249         const err = error instanceof Error ? error : new Error("invalid_json");
    250         return { ok: false, error: err };
    251     }
    252 };