wacom
Version:
Module which has common services, pipes, directives and interfaces which can be used on all projects.
1 lines • 268 kB
Source Map (JSON)
{"version":3,"file":"wacom.mjs","sources":["../../../projects/wacom/src/interfaces/config.interface.ts","../../../projects/wacom/src/interfaces/http.interface.ts","../../../projects/wacom/src/interfaces/network.interface.ts","../../../projects/wacom/src/services/meta.service.ts","../../../projects/wacom/src/guard/meta.guard.ts","../../../projects/wacom/src/services/core.service.ts","../../../projects/wacom/src/crud/crud.component.ts","../../../projects/wacom/src/directives/click-outside.directive.ts","../../../projects/wacom/src/directives/manual-disabled.directive.ts","../../../projects/wacom/src/directives/manual-name.directive.ts","../../../projects/wacom/src/directives/manual-readonly.directive.ts","../../../projects/wacom/src/directives/manual-type.directive.ts","../../../projects/wacom/src/pipes/arr.pipe.ts","../../../projects/wacom/src/pipes/mongodate.pipe.ts","../../../projects/wacom/src/pipes/number.pipe.ts","../../../projects/wacom/src/pipes/pagination.pipe.ts","../../../projects/wacom/src/pipes/safe.pipe.ts","../../../projects/wacom/src/pipes/search.pipe.ts","../../../projects/wacom/src/pipes/splice.pipe.ts","../../../projects/wacom/src/pipes/split.pipe.ts","../../../projects/wacom/src/services/emitter.service.ts","../../../projects/wacom/src/services/http.service.ts","../../../projects/wacom/src/services/network.service.ts","../../../projects/wacom/src/services/store.service.ts","../../../projects/wacom/src/crud/crud.service.ts","../../../projects/wacom/src/services/dom.service.ts","../../../projects/wacom/src/services/rtc.service.ts","../../../projects/wacom/src/services/socket.service.ts","../../../projects/wacom/src/services/time.service.ts","../../../projects/wacom/src/services/util.service.ts","../../../projects/wacom/src/provide-wacom.ts","../../../projects/wacom/src/wacom.module.ts","../../../projects/wacom/public-api.ts","../../../projects/wacom/wacom.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\r\nimport { HttpConfig } from './http.interface';\r\nimport { MetaConfig } from './meta.interface';\r\nimport { NetworkConfig } from './network.interface';\r\nimport { StoreConfig } from './store.interface';\r\n\r\n/**\r\n * Root configuration object used to initialize the library.\r\n * Each property allows consumers to override the default\r\n * behavior of the corresponding service.\r\n */\r\nexport interface Config {\r\n\t/** Options for the key‑value storage service. */\r\n\tstore?: StoreConfig;\r\n\t/** Defaults applied to page metadata handling. */\r\n\tmeta?: MetaConfig;\r\n\t/** Base HTTP settings such as API URL and headers. */\r\n\thttp?: HttpConfig;\r\n\t/** Optional socket connection configuration. */\r\n\tsocket?: any;\r\n\t/** Raw Socket.IO client instance, if used. */\r\n\tio?: any;\r\n\tnetwork?: NetworkConfig;\r\n}\r\n\r\nexport const CONFIG_TOKEN = new InjectionToken<Config>('config');\r\n\r\nexport const DEFAULT_CONFIG: Config = {\r\n\tstore: {\r\n\t\tprefix: 'waStore',\r\n\t},\r\n\tmeta: {\r\n\t\tuseTitleSuffix: false,\r\n\t\twarnMissingGuard: false,\r\n\t\tdefaults: { links: {} },\r\n\t},\r\n\tsocket: false,\r\n\thttp: {\r\n\t\turl: '',\r\n\t\theaders: {},\r\n\t},\r\n};\r\n","export type HttpHeaderType = string | number | (string | number)[];\r\n\r\n/**\r\n * Configuration values used by the HTTP service when\r\n * issuing requests to a backend API.\r\n */\r\nexport interface HttpConfig {\r\n\t/** Map of default headers appended to each request. */\r\n\theaders?: Record<string, HttpHeaderType>;\r\n\t/** Base URL for all HTTP requests. */\r\n\turl?: string;\r\n}\r\n\r\nexport const DEFAULT_HTTP_CONFIG: HttpConfig = {\r\n\theaders: {},\r\n\turl: '',\r\n};\r\n","import { InjectionToken } from '@angular/core';\r\n\r\nexport type NetworkStatus = 'good' | 'poor' | 'none';\r\n\r\nexport interface NetworkConfig {\r\n\t/** Ordered list of endpoints to probe (first that succeeds wins). */\r\n\tendpoints: string[];\r\n\t/** Periodic re-check interval (ms). */\r\n\tintervalMs: number;\r\n\t/** Per-request timeout (ms). */\r\n\ttimeoutMs: number;\r\n\t/** Latency threshold (ms) to classify as \"good\". */\r\n\tgoodLatencyMs: number;\r\n\t/** Consecutive failures to flip status to \"none\". */\r\n\tmaxConsecutiveFails: number;\r\n}\r\n\r\nexport const DEFAULT_NETWORK_CONFIG: NetworkConfig = {\r\n\tendpoints: [\r\n\t\t'https://api.webart.work/status',\r\n\t\t// Opaque but useful reachability fallbacks:\r\n\t\t'https://www.google.com/generate_204',\r\n\t\t'https://www.gstatic.com/generate_204',\r\n\t\t'https://www.cloudflare.com/cdn-cgi/trace',\r\n\t],\r\n\tintervalMs: 30_000,\r\n\ttimeoutMs: 2_500,\r\n\tgoodLatencyMs: 300,\r\n\tmaxConsecutiveFails: 3,\r\n};\r\n\r\nexport const NETWORK_CONFIG = new InjectionToken<NetworkConfig>(\r\n\t'NETWORK_CONFIG',\r\n\t{\r\n\t\tfactory: () => DEFAULT_NETWORK_CONFIG,\r\n\t},\r\n);\r\n","import { Inject, Injectable, Optional } from '@angular/core';\r\nimport { Meta, Title } from '@angular/platform-browser';\r\nimport { Route, Router } from '@angular/router';\r\nimport {\r\n\tCONFIG_TOKEN,\r\n\tConfig,\r\n\tDEFAULT_CONFIG,\r\n} from '../interfaces/config.interface';\r\nimport { MetaConfig, MetaDefaults } from '../interfaces/meta.interface';\r\n\r\nconst isDefined = (val: any) => typeof val !== 'undefined';\r\n\r\n@Injectable({\r\n\tprovidedIn: 'root',\r\n})\r\nexport class MetaService {\r\n\tprivate _meta: MetaConfig;\r\n\r\n\tconstructor(\r\n\t\t@Inject(CONFIG_TOKEN) @Optional() private config: Config,\r\n\t\tprivate router: Router,\r\n\t\tprivate meta: Meta,\r\n\t\tprivate titleService: Title,\r\n\t) {\r\n\t\tthis.config = this.config || DEFAULT_CONFIG;\r\n\r\n\t\tthis._meta = this.config.meta || {};\r\n\r\n\t\tthis._warnMissingGuard();\r\n\t}\r\n\r\n\t/**\r\n\t * Sets the default meta tags.\r\n\t *\r\n\t * @param defaults - The default meta tags.\r\n\t */\r\n\tsetDefaults(defaults: MetaDefaults) {\r\n\t\tthis._meta.defaults = {\r\n\t\t\t...this._meta.defaults,\r\n\t\t\t...defaults,\r\n\t\t};\r\n\t}\r\n\r\n\t/**\r\n\t * Sets the title and optional title suffix.\r\n\t *\r\n\t * @param title - The title to set.\r\n\t * @param titleSuffix - The title suffix to append.\r\n\t * @returns The MetaService instance.\r\n\t */\r\n\tsetTitle(title?: string, titleSuffix?: string): MetaService {\r\n\t\tlet titleContent = isDefined(title)\r\n\t\t\t? title || ''\r\n\t\t\t: this._meta.defaults?.['title'] || '';\r\n\r\n\t\tif (this._meta.useTitleSuffix) {\r\n\t\t\ttitleContent += isDefined(titleSuffix)\r\n\t\t\t\t? titleSuffix\r\n\t\t\t\t: this._meta.defaults?.['titleSuffix'] || '';\r\n\t\t}\r\n\r\n\t\tthis._updateMetaTag('title', titleContent);\r\n\r\n\t\tthis._updateMetaTag('og:title', titleContent);\r\n\r\n\t\tthis._updateMetaTag('twitter:title', titleContent);\r\n\r\n\t\tthis.titleService.setTitle(titleContent);\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/**\r\n\t * Sets link tags.\r\n\t *\r\n\t * @param links - The links to set.\r\n\t * @returns The MetaService instance.\r\n\t */\r\n\tsetLink(links: { [key: string]: string }): MetaService {\r\n\t\tObject.keys(links).forEach((rel) => {\r\n\t\t\tlet link: HTMLLinkElement = document.createElement('link');\r\n\r\n\t\t\tlink.setAttribute('rel', rel);\r\n\r\n\t\t\tlink.setAttribute('href', links[rel]);\r\n\r\n\t\t\tdocument.head.appendChild(link);\r\n\t\t});\r\n\r\n\t\treturn this;\r\n\t}\r\n\r\n\t/**\r\n\t * Sets a meta tag.\r\n\t *\r\n\t * @param tag - The meta tag name.\r\n\t * @param value - The meta tag value.\r\n\t * @param prop - The meta tag property.\r\n\t */\r\n\tsetTag(tag: string, value: string, prop?: string) {\r\n\t\tif (tag === 'title' || tag === 'titleSuffix') {\r\n\t\t\tthrow new Error(\r\n\t\t\t\t`Attempt to set ${tag} through 'setTag': 'title' and 'titleSuffix' are reserved. Use 'MetaService.setTitle' instead.`,\r\n\t\t\t);\r\n\t\t}\r\n\r\n\t\tconst content =\r\n\t\t\t(isDefined(value)\r\n\t\t\t\t? value || ''\r\n\t\t\t\t: this._meta.defaults?.[tag] || '') + '';\r\n\r\n\t\tthis._updateMetaTag(tag, content, prop);\r\n\r\n\t\tif (tag === 'description') {\r\n\t\t\tthis._updateMetaTag('og:description', content, prop);\r\n\r\n\t\t\tthis._updateMetaTag('twitter:description', content, prop);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Updates a meta tag.\r\n\t *\r\n\t * @param tag - The meta tag name.\r\n\t * @param value - The meta tag value.\r\n\t * @param prop - The meta tag property.\r\n\t */\r\n\tprivate _updateMetaTag(tag: string, value: string, prop?: string): void {\r\n\t\tprop =\r\n\t\t\tprop ||\r\n\t\t\t(tag.startsWith('og:') || tag.startsWith('twitter:')\r\n\t\t\t\t? 'property'\r\n\t\t\t\t: 'name');\r\n\r\n\t\tthis.meta.updateTag({ [prop]: tag, content: value });\r\n\t}\r\n\r\n\t/**\r\n\t * Removes a meta tag.\r\n\t *\r\n\t * @param tag - The meta tag name.\r\n\t * @param prop - The meta tag property.\r\n\t */\r\n\tremoveTag(tag: string, prop?: string): void {\r\n\t\tprop =\r\n\t\t\tprop ||\r\n\t\t\t(tag.startsWith('og:') || tag.startsWith('twitter:')\r\n\t\t\t\t? 'property'\r\n\t\t\t\t: 'name');\r\n\r\n\t\tthis.meta.removeTag(`${prop}=\"${tag}\"`);\r\n\t}\r\n\r\n\t/**\r\n\t * Warns about missing meta guards in routes.\r\n\t */\r\n\tprivate _warnMissingGuard(): void {\r\n\t\tif (\r\n\t\t\tisDefined(this._meta.warnMissingGuard) &&\r\n\t\t\t!this._meta.warnMissingGuard\r\n\t\t) {\r\n\t\t\treturn;\r\n\t\t}\r\n\r\n\t\tconst hasDefaultMeta = !!Object.keys(this._meta.defaults ?? {}).length;\r\n\r\n\t\tconst hasMetaGuardInArr = (it: any) =>\r\n\t\t\tit && it.IDENTIFIER === 'MetaGuard';\r\n\r\n\t\tlet hasShownWarnings = false;\r\n\r\n\t\tconst checkRoute = (route: Route) => {\r\n\t\t\tconst hasRouteMeta = route.data && route.data['meta'];\r\n\r\n\t\t\tconst showWarning =\r\n\t\t\t\t!isDefined(route.redirectTo) &&\r\n\t\t\t\t(hasDefaultMeta || hasRouteMeta) &&\r\n\t\t\t\t!(route.canActivate || []).some(hasMetaGuardInArr);\r\n\r\n\t\t\tif (showWarning) {\r\n\t\t\t\tconsole.warn(\r\n\t\t\t\t\t`Route with path \"${route.path}\" has ${\r\n\t\t\t\t\t\thasRouteMeta ? '' : 'default '\r\n\t\t\t\t\t}meta tags, but does not use MetaGuard. Please add MetaGuard to the canActivate array in your route configuration`,\r\n\t\t\t\t);\r\n\r\n\t\t\t\thasShownWarnings = true;\r\n\t\t\t}\r\n\r\n\t\t\t(route.children || []).forEach(checkRoute);\r\n\t\t};\r\n\r\n\t\tthis.router.config.forEach(checkRoute);\r\n\r\n\t\tif (hasShownWarnings) {\r\n\t\t\tconsole.warn(\r\n\t\t\t\t`To disable these warnings, set metaConfig.warnMissingGuard: false in your MetaConfig passed to MetaModule.forRoot()`,\r\n\t\t\t);\r\n\t\t}\r\n\t}\r\n}\r\n","import { Inject, Injectable, Optional } from '@angular/core';\r\nimport { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';\r\nimport {\r\n\tCONFIG_TOKEN,\r\n\tConfig,\r\n\tDEFAULT_CONFIG,\r\n} from '../interfaces/config.interface';\r\nimport { MetaService } from '../services/meta.service';\r\n\r\n@Injectable({ providedIn: 'root' })\r\nexport class MetaGuard {\r\n\tpublic static IDENTIFIER = 'MetaGuard';\r\n\tprivate _meta: any;\r\n\tpublic constructor(\r\n\t\tprivate metaService: MetaService,\r\n\t\t@Inject(CONFIG_TOKEN) @Optional() private config: Config,\r\n\t) {\r\n\t\tif (!this.config) this.config = DEFAULT_CONFIG;\r\n\t\tthis._meta = this.config.meta || {};\r\n\t\tthis._meta.defaults = this._meta.defaults || {};\r\n\t}\r\n\tpublic canActivate(\r\n\t\troute: ActivatedRouteSnapshot,\r\n\t\tstate: RouterStateSnapshot,\r\n\t): boolean {\r\n\t\tthis._processRouteMetaTags(route.data && route.data['meta']);\r\n\t\treturn true;\r\n\t}\r\n\tprivate _processRouteMetaTags(meta: any = {}) {\r\n\t\tif (meta.disableUpdate) {\r\n\t\t\treturn;\r\n\t\t}\r\n\t\tif (meta.title) {\r\n\t\t\tthis.metaService.setTitle(meta.title, meta.titleSuffix);\r\n\t\t}\r\n\t\tif (meta.links && Object.keys(meta.links).length) {\r\n\t\t\tthis.metaService.setLink(meta.links);\r\n\t\t}\r\n\t\tif (\r\n\t\t\tthis._meta.defaults?.links &&\r\n\t\t\tObject.keys(this._meta.defaults.links).length\r\n\t\t) {\r\n\t\t\tthis.metaService.setLink(this._meta.defaults.links);\r\n\t\t}\r\n\t\tObject.keys(meta).forEach((prop) => {\r\n\t\t\tif (\r\n\t\t\t\tprop === 'title' ||\r\n\t\t\t\tprop === 'titleSuffix' ||\r\n\t\t\t\tprop === 'links'\r\n\t\t\t) {\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tObject.keys(meta[prop]).forEach((key) => {\r\n\t\t\t\tthis.metaService.setTag(key, meta[prop][key], prop);\r\n\t\t\t});\r\n\t\t});\r\n\t\tObject.keys(this._meta.defaults).forEach((key) => {\r\n\t\t\tif (\r\n\t\t\t\tkey in meta ||\r\n\t\t\t\tkey === 'title' ||\r\n\t\t\t\tkey === 'titleSuffix' ||\r\n\t\t\t\tkey === 'links'\r\n\t\t\t) {\r\n\t\t\t\treturn;\r\n\t\t\t}\r\n\t\t\tthis.metaService.setTag(key, this._meta.defaults[key] as string);\r\n\t\t});\r\n\t}\r\n}\r\n","// Core utilities and helpers for the Wacom app\r\nimport { Injectable, Signal, WritableSignal, signal } from '@angular/core';\r\n\r\n// Add capitalize method to String prototype if it doesn't already exist\r\nif (!String.prototype.capitalize) {\r\n\tString.prototype.capitalize = function (): string {\r\n\t\tif (this.length > 0) {\r\n\t\t\treturn this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();\r\n\t\t}\r\n\t\treturn '';\r\n\t};\r\n}\r\n\r\n// Extend the String interface to include the new method\r\ndeclare global {\r\n\tinterface String {\r\n\t\tcapitalize(): string;\r\n\t}\r\n}\r\n\r\n@Injectable({\r\n\tprovidedIn: 'root',\r\n})\r\nexport class CoreService {\r\n\tdeviceID =\r\n\t\tlocalStorage.getItem('deviceID') ||\r\n\t\t(typeof crypto?.randomUUID === 'function'\r\n\t\t\t? crypto.randomUUID()\r\n\t\t\t: this.UUID());\r\n\r\n\tconstructor() {\r\n\t\tlocalStorage.setItem('deviceID', this.deviceID);\r\n\r\n\t\tthis.detectDevice();\r\n\t}\r\n\r\n\t/**\r\n\t * Generates a UUID (Universally Unique Identifier) version 4.\r\n\t *\r\n\t * This implementation uses `Math.random()` to generate random values,\r\n\t * making it suitable for general-purpose identifiers, but **not** for\r\n\t * cryptographic or security-sensitive use cases.\r\n\t *\r\n\t * The format follows the UUID v4 standard: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`\r\n\t * where:\r\n\t * - `x` is a random hexadecimal digit (0–f)\r\n\t * - `4` indicates UUID version 4\r\n\t * - `y` is one of 8, 9, A, or B\r\n\t *\r\n\t * Example: `f47ac10b-58cc-4372-a567-0e02b2c3d479`\r\n\t *\r\n\t * @returns A string containing a UUID v4.\r\n\t */\r\n\tUUID(): string {\r\n\t\treturn 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(\r\n\t\t\t/[xy]/g,\r\n\t\t\t(c: string) => {\r\n\t\t\t\tconst r = (Math.random() * 16) | 0;\r\n\t\t\t\tconst v = c === 'x' ? r : (r & 0x3) | 0x8;\r\n\t\t\t\treturn v.toString(16);\r\n\t\t\t},\r\n\t\t);\r\n\t}\r\n\r\n\t/**\r\n\t * Converts an object to an array. Optionally holds keys instead of values.\r\n\t *\r\n\t * @param {any} obj - The object to be converted.\r\n\t * @param {boolean} [holder=false] - If true, the keys will be held in the array; otherwise, the values will be held.\r\n\t * @returns {any[]} The resulting array.\r\n\t */\r\n\tota(obj: any, holder: boolean = false): any[] {\r\n\t\tif (Array.isArray(obj)) return obj;\r\n\t\tif (typeof obj !== 'object' || obj === null) return [];\r\n\t\tconst arr = [];\r\n\t\tfor (const each in obj) {\r\n\t\t\tif (\r\n\t\t\t\tobj.hasOwnProperty(each) &&\r\n\t\t\t\t(obj[each] ||\r\n\t\t\t\t\ttypeof obj[each] === 'number' ||\r\n\t\t\t\t\ttypeof obj[each] === 'boolean')\r\n\t\t\t) {\r\n\t\t\t\tif (holder) {\r\n\t\t\t\t\tarr.push(each);\r\n\t\t\t\t} else {\r\n\t\t\t\t\tarr.push(obj[each]);\r\n\t\t\t\t}\r\n\t\t\t}\r\n\t\t}\r\n\t\treturn arr;\r\n\t}\r\n\r\n\t/**\r\n\t * Removes elements from `fromArray` that are present in `removeArray` based on a comparison field.\r\n\t *\r\n\t * @param {any[]} removeArray - The array of elements to remove.\r\n\t * @param {any[]} fromArray - The array from which to remove elements.\r\n\t * @param {string} [compareField='_id'] - The field to use for comparison.\r\n\t * @returns {any[]} The modified `fromArray` with elements removed.\r\n\t */\r\n\tsplice(\r\n\t\tremoveArray: any[],\r\n\t\tfromArray: any[],\r\n\t\tcompareField: string = '_id',\r\n\t): any[] {\r\n\t\tif (!Array.isArray(removeArray) || !Array.isArray(fromArray)) {\r\n\t\t\treturn fromArray;\r\n\t\t}\r\n\r\n\t\tconst removeSet = new Set(\r\n\t\t\tremoveArray.map((item) => item[compareField]),\r\n\t\t);\r\n\t\treturn fromArray.filter((item) => !removeSet.has(item[compareField]));\r\n\t}\r\n\r\n\t/**\r\n\t * Unites multiple _id values into a single unique _id.\r\n\t * The resulting _id is unique regardless of the order of the input _id values.\r\n\t *\r\n\t * @param {...string[]} args - The _id values to be united.\r\n\t * @returns {string} The unique combined _id.\r\n\t */\r\n\tids2id(...args: string[]): string {\r\n\t\targs.sort((a, b) => {\r\n\t\t\tif (\r\n\t\t\t\tNumber(a.toString().substring(0, 8)) >\r\n\t\t\t\tNumber(b.toString().substring(0, 8))\r\n\t\t\t) {\r\n\t\t\t\treturn 1;\r\n\t\t\t}\r\n\t\t\treturn -1;\r\n\t\t});\r\n\r\n\t\treturn args.join();\r\n\t}\r\n\r\n\t// After While\r\n\tprivate _afterWhile: Record<string, number> = {};\r\n\t/**\r\n\t * Delays the execution of a callback function for a specified amount of time.\r\n\t * If called again within that time, the timer resets.\r\n\t *\r\n\t * @param {string | object | (() => void)} doc - A unique identifier for the timer, an object to host the timer, or the callback function.\r\n\t * @param {() => void} [cb] - The callback function to execute after the delay.\r\n\t * @param {number} [time=1000] - The delay time in milliseconds.\r\n\t */\r\n\tafterWhile(\r\n\t\tdoc: string | object | (() => void),\r\n\t\tcb?: () => void,\r\n\t\ttime: number = 1000,\r\n\t): void {\r\n\t\tif (typeof doc === 'function') {\r\n\t\t\tcb = doc as () => void;\r\n\t\t\tdoc = 'common';\r\n\t\t}\r\n\r\n\t\tif (typeof cb === 'function' && typeof time === 'number') {\r\n\t\t\tif (typeof doc === 'string') {\r\n\t\t\t\tclearTimeout(this._afterWhile[doc]);\r\n\t\t\t\tthis._afterWhile[doc] = window.setTimeout(cb, time);\r\n\t\t\t} else if (typeof doc === 'object') {\r\n\t\t\t\tclearTimeout((doc as { __afterWhile: number }).__afterWhile);\r\n\t\t\t\t(doc as { __afterWhile: number }).__afterWhile =\r\n\t\t\t\t\twindow.setTimeout(cb, time);\r\n\t\t\t} else {\r\n\t\t\t\tconsole.warn('badly configured after while');\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Recursively copies properties from one object to another.\r\n\t * Handles nested objects, arrays, and Date instances appropriately.\r\n\t *\r\n\t * @param from - The source object from which properties are copied.\r\n\t * @param to - The target object to which properties are copied.\r\n\t */\r\n\tcopy(from: any, to: any) {\r\n\t\tfor (const each in from) {\r\n\t\t\tif (\r\n\t\t\t\ttypeof from[each] !== 'object' ||\r\n\t\t\t\tfrom[each] instanceof Date ||\r\n\t\t\t\tArray.isArray(from[each]) ||\r\n\t\t\t\tfrom[each] === null\r\n\t\t\t) {\r\n\t\t\t\tto[each] = from[each];\r\n\t\t\t} else {\r\n\t\t\t\tif (\r\n\t\t\t\t\ttypeof to[each] !== 'object' ||\r\n\t\t\t\t\tto[each] instanceof Date ||\r\n\t\t\t\t\tArray.isArray(to[each]) ||\r\n\t\t\t\t\tto[each] === null\r\n\t\t\t\t) {\r\n\t\t\t\t\tto[each] = {};\r\n\t\t\t\t}\r\n\r\n\t\t\t\tthis.copy(from[each], to[each]);\r\n\t\t\t}\r\n\t\t}\r\n\t}\r\n\r\n\t// Device management\r\n\tdevice = '';\r\n\t/**\r\n\t * Detects the device type based on the user agent.\r\n\t */\r\n\tdetectDevice(): void {\r\n\t\tconst userAgent =\r\n\t\t\tnavigator.userAgent || navigator.vendor || (window as any).opera;\r\n\t\tif (/windows phone/i.test(userAgent)) {\r\n\t\t\tthis.device = 'Windows Phone';\r\n\t\t} else if (/android/i.test(userAgent)) {\r\n\t\t\tthis.device = 'Android';\r\n\t\t} else if (\r\n\t\t\t/iPad|iPhone|iPod/.test(userAgent) &&\r\n\t\t\t!(window as any).MSStream\r\n\t\t) {\r\n\t\t\tthis.device = 'iOS';\r\n\t\t} else {\r\n\t\t\tthis.device = 'Web';\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is a mobile device.\r\n\t * @returns {boolean} - Returns true if the device is a mobile device.\r\n\t */\r\n\tisMobile(): boolean {\r\n\t\treturn (\r\n\t\t\tthis.device === 'Windows Phone' ||\r\n\t\t\tthis.device === 'Android' ||\r\n\t\t\tthis.device === 'iOS'\r\n\t\t);\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is a tablet.\r\n\t * @returns {boolean} - Returns true if the device is a tablet.\r\n\t */\r\n\tisTablet(): boolean {\r\n\t\treturn this.device === 'iOS' && /iPad/.test(navigator.userAgent);\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is a web browser.\r\n\t * @returns {boolean} - Returns true if the device is a web browser.\r\n\t */\r\n\tisWeb(): boolean {\r\n\t\treturn this.device === 'Web';\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is an Android device.\r\n\t * @returns {boolean} - Returns true if the device is an Android device.\r\n\t */\r\n\tisAndroid(): boolean {\r\n\t\treturn this.device === 'Android';\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if the device is an iOS device.\r\n\t * @returns {boolean} - Returns true if the device is an iOS device.\r\n\t */\r\n\tisIos(): boolean {\r\n\t\treturn this.device === 'iOS';\r\n\t}\r\n\r\n\t// Version management\r\n\tversion = '1.0.0';\r\n\r\n\tappVersion = '';\r\n\r\n\tdateVersion = '';\r\n\r\n\t/**\r\n\t * Sets the combined version string based on appVersion and dateVersion.\r\n\t */\r\n\tsetVersion(): void {\r\n\t\tthis.version = this.appVersion || '';\r\n\r\n\t\tthis.version += this.version && this.dateVersion ? ' ' : '';\r\n\r\n\t\tthis.version += this.dateVersion || '';\r\n\t}\r\n\r\n\t/**\r\n\t * Sets the app version and updates the combined version string.\r\n\t *\r\n\t * @param {string} appVersion - The application version to set.\r\n\t */\r\n\tsetAppVersion(appVersion: string): void {\r\n\t\tthis.appVersion = appVersion;\r\n\r\n\t\tthis.setVersion();\r\n\t}\r\n\r\n\t/**\r\n\t * Sets the date version and updates the combined version string.\r\n\t *\r\n\t * @param {string} dateVersion - The date version to set.\r\n\t */\r\n\tsetDateVersion(dateVersion: string): void {\r\n\t\tthis.dateVersion = dateVersion;\r\n\r\n\t\tthis.setVersion();\r\n\t}\r\n\r\n\t// Locking management\r\n\tprivate _locked: Record<string, boolean> = {};\r\n\tprivate _unlockResolvers: Record<string, (() => void)[]> = {};\r\n\r\n\t/**\r\n\t * Locks a resource to prevent concurrent access.\r\n\t * @param which - The resource to lock, identified by a string.\r\n\t */\r\n\tlock(which: string): void {\r\n\t\tthis._locked[which] = true;\r\n\r\n\t\tif (!this._unlockResolvers[which]) {\r\n\t\t\tthis._unlockResolvers[which] = [];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Unlocks a resource, allowing access.\r\n\t * @param which - The resource to unlock, identified by a string.\r\n\t */\r\n\tunlock(which: string): void {\r\n\t\tthis._locked[which] = false;\r\n\r\n\t\tif (this._unlockResolvers[which]) {\r\n\t\t\tthis._unlockResolvers[which].forEach((resolve) => resolve());\r\n\r\n\t\t\tthis._unlockResolvers[which] = [];\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Returns a Promise that resolves when the specified resource is unlocked.\r\n\t * @param which - The resource to watch for unlocking, identified by a string.\r\n\t * @returns A Promise that resolves when the resource is unlocked.\r\n\t */\r\n\tonUnlock(which: string): Promise<void> {\r\n\t\tif (!this._locked[which]) {\r\n\t\t\treturn Promise.resolve();\r\n\t\t}\r\n\r\n\t\treturn new Promise((resolve) => {\r\n\t\t\tif (!this._unlockResolvers[which]) {\r\n\t\t\t\tthis._unlockResolvers[which] = [];\r\n\t\t\t}\r\n\r\n\t\t\tthis._unlockResolvers[which].push(resolve);\r\n\t\t});\r\n\t}\r\n\r\n\t/**\r\n\t * Checks if a resource is locked.\r\n\t * @param which - The resource to check, identified by a string.\r\n\t * @returns True if the resource is locked, false otherwise.\r\n\t */\r\n\tlocked(which: string): boolean {\r\n\t\treturn !!this._locked[which];\r\n\t}\r\n\r\n\t// Angular Signals //\r\n\t/**\r\n\t * Converts a plain object into a signal-wrapped object.\r\n\t * Optionally wraps specific fields of the object as individual signals,\r\n\t * and merges them into the returned signal for fine-grained reactivity.\r\n\t *\r\n\t * @template Document - The type of the object being wrapped.\r\n\t * @param {Document} document - The plain object to wrap into a signal.\r\n\t * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -\r\n\t * Optional map where each key is a field name and the value is a function\r\n\t * to extract the initial value for that field. These fields will be wrapped\r\n\t * as separate signals and embedded in the returned object.\r\n\t *\r\n\t * @returns {WritableSignal<Document>} A signal-wrapped object, possibly containing\r\n\t * nested field signals for more granular control.\r\n\t *\r\n\t * @example\r\n\t * const user = { _id: '1', name: 'Alice', score: 42 };\r\n\t * const sig = toSignal(user, { score: (u) => u.score });\r\n\t * console.log(sig().name); // 'Alice'\r\n\t * console.log(sig().score()); // 42 — field is now a signal\r\n\t */\r\n\ttoSignal<Document>(\r\n\t\tdocument: Document,\r\n\t\tsignalFields: Record<string, (doc: Document) => unknown> = {},\r\n\t): WritableSignal<Document> {\r\n\t\tif (Object.keys(signalFields).length) {\r\n\t\t\tconst fields: Record<string, Signal<unknown>> = {};\r\n\r\n\t\t\tfor (const key in signalFields) {\r\n\t\t\t\tfields[key] = signal(signalFields[key](document));\r\n\t\t\t}\r\n\r\n\t\t\treturn signal({ ...document, ...fields });\r\n\t\t} else {\r\n\t\t\treturn signal(document);\r\n\t\t}\r\n\t}\r\n\r\n\t/**\r\n\t * Converts an array of objects into an array of Angular signals.\r\n\t * Optionally wraps specific fields of each object as individual signals.\r\n\t *\r\n\t * @template Document - The type of each object in the array.\r\n\t * @param {Document[]} arr - Array of plain objects to convert into signals.\r\n\t * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -\r\n\t * Optional map where keys are field names and values are functions that extract the initial value\r\n\t * from the object. These fields will be turned into separate signals.\r\n\t *\r\n\t * @returns {WritableSignal<Document>[]} An array where each item is a signal-wrapped object,\r\n\t * optionally with individual fields also wrapped in signals.\r\n\t *\r\n\t * @example\r\n\t * toSignalsArray(users, {\r\n\t * name: (u) => u.name,\r\n\t * score: (u) => u.score,\r\n\t * });\r\n\t */\r\n\ttoSignalsArray<Document>(\r\n\t\tarr: Document[],\r\n\t\tsignalFields: Record<string, (doc: Document) => unknown> = {},\r\n\t): WritableSignal<Document>[] {\r\n\t\treturn arr.map((obj) => this.toSignal(obj, signalFields));\r\n\t}\r\n\r\n\t/**\r\n\t * Adds a new object to the signals array.\r\n\t * Optionally wraps specific fields of the object as individual signals before wrapping the whole object.\r\n\t *\r\n\t * @template Document - The type of the object being added.\r\n\t * @param {WritableSignal<Document>[]} signals - The signals array to append to.\r\n\t * @param {Document} item - The object to wrap and push as a signal.\r\n\t * @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -\r\n\t * Optional map of fields to be wrapped as signals within the object.\r\n\t *\r\n\t * @returns {void}\r\n\t */\r\n\tpushSignal<Document>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\titem: Document,\r\n\t\tsignalFields: Record<string, (doc: Document) => unknown> = {},\r\n\t): void {\r\n\t\tsignals.push(this.toSignal(item, signalFields));\r\n\t}\r\n\r\n\t/**\r\n\t * Removes the first signal from the array whose object's field matches the provided value.\r\n\t * @template Document\r\n\t * @param {WritableSignal<Document>[]} signals - The signals array to modify.\r\n\t * @param {unknown} value - The value to match.\r\n\t * @param {string} [field='_id'] - The object field to match against.\r\n\t * @returns {void}\r\n\t */\r\n\tremoveSignalByField<Document extends Record<string, unknown>>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\tvalue: unknown,\r\n\t\tfield: string = '_id',\r\n\t): void {\r\n\t\tconst idx = signals.findIndex((sig) => sig()[field] === value);\r\n\r\n\t\tif (idx > -1) signals.splice(idx, 1);\r\n\t}\r\n\r\n\t/**\r\n\t * Returns a generic trackBy function for *ngFor, tracking by the specified object field.\r\n\t * @template Document\r\n\t * @param {string} field - The object field to use for tracking (e.g., '_id').\r\n\t * @returns {(index: number, sig: Signal<Document>) => unknown} TrackBy function for Angular.\r\n\t */\r\n\ttrackBySignalField<Document extends Record<string, unknown>>(\r\n\t\tfield: string,\r\n\t) {\r\n\t\treturn (_: number, sig: Signal<Document>) => sig()[field];\r\n\t}\r\n\r\n\t/**\r\n\t * Finds the first signal in the array whose object's field matches the provided value.\r\n\t * @template Document\r\n\t * @param {Signal<Document>[]} signals - Array of signals to search.\r\n\t * @param {unknown} value - The value to match.\r\n\t * @param {string} [field='_id'] - The object field to match against.\r\n\t * @returns {Signal<Document> | undefined} The found signal or undefined if not found.\r\n\t */\r\n\tfindSignalByField<Document extends Record<string, unknown>>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\tvalue: unknown,\r\n\t\tfield = '_id',\r\n\t): WritableSignal<Document> | undefined {\r\n\t\treturn signals.find(\r\n\t\t\t(sig) => sig()[field] === value,\r\n\t\t) as WritableSignal<Document>;\r\n\t}\r\n\r\n\t/**\r\n\t * Updates the first writable signal in the array whose object's field matches the provided value.\r\n\t * @template Document\r\n\t * @param {WritableSignal<Document>[]} signals - Array of writable signals to search.\r\n\t * @param {unknown} value - The value to match.\r\n\t * @param {(val: Document) => Document} updater - Function to produce the updated object.\r\n\t * @param {string} field - The object field to match against.\r\n\t * @returns {void}\r\n\t */\r\n\tupdateSignalByField<Document extends Record<string, unknown>>(\r\n\t\tsignals: WritableSignal<Document>[],\r\n\t\tvalue: unknown,\r\n\t\tupdater: (val: Document) => Document,\r\n\t\tfield: string,\r\n\t): void {\r\n\t\tconst sig = this.findSignalByField<Document>(\r\n\t\t\tsignals,\r\n\t\t\tvalue,\r\n\t\t\tfield,\r\n\t\t) as WritableSignal<Document>;\r\n\r\n\t\tif (sig) sig.update(updater);\r\n\t}\r\n}\r\n","import {\r\n\tChangeDetectorRef,\r\n\tinject,\r\n\tSignal,\r\n\tsignal,\r\n\tWritableSignal,\r\n} from '@angular/core';\r\nimport { firstValueFrom, take } from 'rxjs';\r\nimport { CoreService } from '../services/core.service';\r\nimport {\r\n\tCrudDocument,\r\n\tCrudOptions,\r\n\tCrudServiceInterface,\r\n\tTableConfig,\r\n} from './crud.interface';\r\n\r\n/**\r\n * Interface representing the shape of a form service used by the CrudComponent.\r\n * The consuming app must provide a service that implements this structure.\r\n */\r\ninterface FormServiceInterface<FormInterface> {\r\n\tmodal: <T>(form: FormInterface, options?: any, doc?: T) => Promise<T>;\r\n\tmodalDocs: <T>(docs: T[]) => Promise<T[]>;\r\n\tmodalUnique: <T>(collection: string, key: string, doc: T) => void;\r\n}\r\n\r\n/**\r\n * Abstract reusable base class for CRUD list views.\r\n * It encapsulates pagination, modals, and document handling logic.\r\n *\r\n * @template Service - A service implementing CrudServiceInterface for a specific document type\r\n * @template Document - The data model extending CrudDocument\r\n */\r\nexport abstract class CrudComponent<\r\n\tService extends CrudServiceInterface<Document>,\r\n\tDocument extends CrudDocument<Document>,\r\n\tFormInterface,\r\n> {\r\n\t/** Service responsible for data fetching, creating, updating, deleting */\r\n\tprotected crudService: Service;\r\n\r\n\t/** The array of documents currently loaded and shown */\r\n\tprotected documents = signal<Signal<Document>[]>([]);\r\n\r\n\t/** Form schema/config used by the FormService modals */\r\n\tprotected form: FormInterface;\r\n\r\n\t/** Current pagination page */\r\n\tprotected page = 1;\r\n\r\n\t/** CoreService handles timing and copying helpers */\r\n\tprivate __core = inject(CoreService);\r\n\r\n\t/** ChangeDetectorRef handles on push strategy */\r\n\tprivate __cdr = inject(ChangeDetectorRef);\r\n\r\n\t/** Internal reference to form service matching FormServiceInterface */\r\n\tprivate __form: FormServiceInterface<FormInterface>;\r\n\r\n\t/**\r\n\t * Constructor\r\n\t *\r\n\t * @param formConfig - Object describing form title and its component structure\r\n\t * @param formService - Any service that conforms to FormServiceInterface (usually casted)\r\n\t * @param crudService - CRUD service implementing get/create/update/delete\r\n\t */\r\n\tconstructor(\r\n\t\tformConfig: unknown,\r\n\t\tformService: unknown,\r\n\t\tcrudService: Service,\r\n\t\tmodule = '',\r\n\t) {\r\n\t\tconst form = formConfig as FormInterface;\r\n\r\n\t\tthis.__form = formService as FormServiceInterface<FormInterface>;\r\n\r\n\t\tthis.form = form;\r\n\r\n\t\tthis.crudService = crudService;\r\n\r\n\t\tthis._module = module;\r\n\t}\r\n\r\n\t/**\r\n\t * Loads documents for a given page.\r\n\t */\r\n\tprotected setDocuments(page = this.page, query = ''): Promise<void> {\r\n\t\treturn new Promise((resolve) => {\r\n\t\t\tif (this.configType === 'server') {\r\n\t\t\t\tthis.page = page;\r\n\r\n\t\t\t\tthis.__core.afterWhile(\r\n\t\t\t\t\tthis,\r\n\t\t\t\t\t() => {\r\n\t\t\t\t\t\tthis.crudService\r\n\t\t\t\t\t\t\t.get({ page, query }, this.getOptions())\r\n\t\t\t\t\t\t\t.subscribe((docs: Document[]) => {\r\n\t\t\t\t\t\t\t\tthis.documents.update(() =>\r\n\t\t\t\t\t\t\t\t\tdocs.map((doc) =>\r\n\t\t\t\t\t\t\t\t\t\tthis.crudService.getSignal(doc),\r\n\t\t\t\t\t\t\t\t\t),\r\n\t\t\t\t\t\t\t\t);\r\n\r\n\t\t\t\t\t\t\t\tresolve();\r\n\r\n\t\t\t\t\t\t\t\tthis.__cdr.markForCheck();\r\n\t\t\t\t\t\t\t});\r\n\t\t\t\t\t},\r\n\t\t\t\t\t250,\r\n\t\t\t\t);\r\n\t\t\t} else {\r\n\t\t\t\tthis.documents.update(() =>\r\n\t\t\t\t\tthis.crudService\r\n\t\t\t\t\t\t.getDocs()\r\n\t\t\t\t\t\t.map((doc) => this.crudService.getSignal(doc)),\r\n\t\t\t\t);\r\n\r\n\t\t\t\tthis.crudService.loaded.pipe(take(1)).subscribe(() => {\r\n\t\t\t\t\tresolve();\r\n\r\n\t\t\t\t\tthis.__cdr.markForCheck();\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t});\r\n\t}\r\n\r\n\t/** Fields considered when performing bulk updates. */\r\n\tprotected updatableFields = ['_id', 'name', 'description', 'data'];\r\n\r\n\t/**\r\n\t * Clears temporary metadata before document creation.\r\n\t */\r\n\tprotected preCreate(doc: Document): void {\r\n\t\tdelete doc.__creating;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which controls whether the create functionality is available.\r\n\t */\r\n\tprotected allowCreate(): boolean {\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which controls whether the update and delete functionality is available.\r\n\t */\r\n\tprotected allowMutate(): boolean {\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which controls whether the unique url functionality is available.\r\n\t */\r\n\tprotected allowUrl(): boolean {\r\n\t\treturn true;\r\n\t}\r\n\r\n\t/** Determines whether manual sorting controls are available. */\r\n\tprotected allowSort(): boolean {\r\n\t\treturn false;\r\n\t}\r\n\r\n\t/**\r\n\t * Funciton which prepare get crud options.\r\n\t */\r\n\tprotected getOptions(): CrudOptions<Document> {\r\n\t\treturn {} as CrudOptions<Document>;\r\n\t}\r\n\r\n\t/**\r\n\t * Handles bulk creation and updating of documents.\r\n\t * In creation mode, adds new documents.\r\n\t * In update mode, syncs changes and deletes removed entries.\r\n\t */\r\n\tprotected bulkManagement(isCreateFlow = true): () => void {\r\n\t\treturn (): void => {\r\n\t\t\tthis.__form\r\n\t\t\t\t.modalDocs<Document>(\r\n\t\t\t\t\tisCreateFlow\r\n\t\t\t\t\t\t? []\r\n\t\t\t\t\t\t: this.documents().map(\r\n\t\t\t\t\t\t\t\t(obj: any) =>\r\n\t\t\t\t\t\t\t\t\tObject.fromEntries(\r\n\t\t\t\t\t\t\t\t\t\tthis.updatableFields.map((key) => [\r\n\t\t\t\t\t\t\t\t\t\t\tkey,\r\n\t\t\t\t\t\t\t\t\t\t\tobj()[key],\r\n\t\t\t\t\t\t\t\t\t\t]),\r\n\t\t\t\t\t\t\t\t\t) as Document,\r\n\t\t\t\t\t\t\t),\r\n\t\t\t\t)\r\n\t\t\t\t.then(async (docs: Document[]) => {\r\n\t\t\t\t\tif (isCreateFlow) {\r\n\t\t\t\t\t\tfor (const doc of docs) {\r\n\t\t\t\t\t\t\tthis.preCreate(doc);\r\n\r\n\t\t\t\t\t\t\tawait firstValueFrom(this.crudService.create(doc));\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t} else {\r\n\t\t\t\t\t\tfor (const document of this.documents()) {\r\n\t\t\t\t\t\t\tif (!docs.find((d) => d._id === document()._id)) {\r\n\t\t\t\t\t\t\t\tawait firstValueFrom(\r\n\t\t\t\t\t\t\t\t\tthis.crudService.delete(document()),\r\n\t\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\r\n\t\t\t\t\t\tfor (const doc of docs) {\r\n\t\t\t\t\t\t\tconst local = this.documents().find(\r\n\t\t\t\t\t\t\t\t(document) => document()._id === doc._id,\r\n\t\t\t\t\t\t\t);\r\n\r\n\t\t\t\t\t\t\tif (local) {\r\n\t\t\t\t\t\t\t\t(local as WritableSignal<Document>).update(\r\n\t\t\t\t\t\t\t\t\t(document) => {\r\n\t\t\t\t\t\t\t\t\t\tthis.__core.copy(doc, document);\r\n\r\n\t\t\t\t\t\t\t\t\t\treturn document;\r\n\t\t\t\t\t\t\t\t\t},\r\n\t\t\t\t\t\t\t\t);\r\n\r\n\t\t\t\t\t\t\t\tawait firstValueFrom(\r\n\t\t\t\t\t\t\t\t\tthis.crudService.update(local()),\r\n\t\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t\t} else {\r\n\t\t\t\t\t\t\t\tthis.preCreate(doc);\r\n\r\n\t\t\t\t\t\t\t\tawait firstValueFrom(\r\n\t\t\t\t\t\t\t\t\tthis.crudService.create(doc),\r\n\t\t\t\t\t\t\t\t);\r\n\t\t\t\t\t\t\t}\r\n\t\t\t\t\t\t}\r\n\t\t\t\t\t}\r\n\r\n\t\t\t\t\tthis.setDocuments();\r\n\t\t\t\t});\r\n\t\t};\r\n\t}\r\n\r\n\t/** Opens a modal to create a new document. */\r\n\tprotected create() {\r\n\t\tthis.__form.modal<Document>(this.form, {\r\n\t\t\tlabel: 'Create',\r\n\t\t\tclick: async (created: unknown, close: () => void) => {\r\n\t\t\t\tclose();\r\n\r\n\t\t\t\tthis.preCreate(created as Document);\r\n\r\n\t\t\t\tawait firstValueFrom(\r\n\t\t\t\t\tthis.crudService.create(created as Document),\r\n\t\t\t\t);\r\n\r\n\t\t\t\tthis.setDocuments();\r\n\t\t\t},\r\n\t\t});\r\n\t}\r\n\r\n\t/** Displays a modal to edit an existing document. */\r\n\tprotected update(doc: Document) {\r\n\t\tthis.__form.modal<Document>(\r\n\t\t\tthis.form,\r\n\t\t\t{\r\n\t\t\t\tlabel: 'Update',\r\n\t\t\t\tclick: (updated: unknown, close: () => void) => {\r\n\t\t\t\t\tclose();\r\n\r\n\t\t\t\t\tthis.__core.copy(updated, doc);\r\n\r\n\t\t\t\t\tthis.crudService.update(doc);\r\n\r\n\t\t\t\t\tthis.__cdr.markForCheck();\r\n\t\t\t\t},\r\n\t\t\t},\r\n\t\t\tdoc,\r\n\t\t);\r\n\t}\r\n\r\n\t/** Requests confirmation before deleting the provided document. */\r\n\tprotected async delete(doc: Document) {\r\n\t\tthis.crudService.delete(doc).subscribe(() => {\r\n\t\t\tthis.setDocuments();\r\n\t\t});\r\n\t}\r\n\r\n\t/** Opens a modal to edit the document's unique URL. */\r\n\tprotected mutateUrl(doc: Document) {\r\n\t\tthis.__form.modalUnique<Document>(this._module, 'url', doc);\r\n\t}\r\n\r\n\t/** Moves the given document one position up and updates ordering. */\r\n\tprotected moveUp(doc: Document) {\r\n\t\tconst index = this.documents().findIndex(\r\n\t\t\t(document) => document()._id === doc._id,\r\n\t\t);\r\n\r\n\t\tif (index) {\r\n\t\t\tthis.documents.update((documents) => {\r\n\t\t\t\tdocuments.splice(index, 1);\r\n\r\n\t\t\t\tdocuments.splice(index - 1, 0, this.crudService.getSignal(doc));\r\n\r\n\t\t\t\treturn documents;\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tfor (let i = 0; i < this.documents().length; i++) {\r\n\t\t\tif (this.documents()[i]().order !== i) {\r\n\t\t\t\tthis.documents()[i]().order = i;\r\n\r\n\t\t\t\tthis.crudService.update(this.documents()[i]());\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\tthis.__cdr.markForCheck();\r\n\t}\r\n\r\n\t/** Data source mode used for document retrieval. */\r\n\tprotected configType: 'server' | 'local' = 'server';\r\n\r\n\t/** Number of documents fetched per page when paginating. */\r\n\tprotected perPage = 20;\r\n\r\n\t/**\r\n\t * Configuration object used by the UI for rendering table and handling actions.\r\n\t */\r\n\tprotected getConfig(): TableConfig<Document> {\r\n\t\tconst config: TableConfig<Document> = {\r\n\t\t\tcreate: this.allowCreate()\r\n\t\t\t\t? (): void => {\r\n\t\t\t\t\t\tthis.create();\r\n\t\t\t\t\t}\r\n\t\t\t\t: null,\r\n\r\n\t\t\tupdate: this.allowMutate()\r\n\t\t\t\t? (doc: Document): void => {\r\n\t\t\t\t\t\tthis.update(doc);\r\n\t\t\t\t\t}\r\n\t\t\t\t: null,\r\n\r\n\t\t\tdelete: this.allowMutate()\r\n\t\t\t\t? (doc: Document): void => {\r\n\t\t\t\t\t\tthis.delete(doc);\r\n\t\t\t\t\t}\r\n\t\t\t\t: null,\r\n\r\n\t\t\tbuttons: [],\r\n\t\t\theaderButtons: [],\r\n\t\t\tallDocs: true,\r\n\t\t};\r\n\r\n\t\tif (this.allowUrl()) {\r\n\t\t\tconfig.buttons.push({\r\n\t\t\t\ticon: 'cloud_download',\r\n\t\t\t\tclick: (doc: Document) => {\r\n\t\t\t\t\tthis.mutateUrl(doc);\r\n\t\t\t\t},\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tif (this.allowSort()) {\r\n\t\t\tconfig.buttons.push({\r\n\t\t\t\ticon: 'arrow_upward',\r\n\t\t\t\tclick: (doc: Document) => {\r\n\t\t\t\t\tthis.moveUp(doc);\r\n\t\t\t\t},\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\tif (this.allowCreate()) {\r\n\t\t\tconfig.headerButtons.push({\r\n\t\t\t\ticon: 'playlist_add',\r\n\t\t\t\tclick: this.bulkManagement(),\r\n\t\t\t\tclass: 'playlist',\r\n\t\t\t});\r\n\t\t}\r\n\t\tif (this.allowMutate()) {\r\n\t\t\tconfig.headerButtons.push({\r\n\t\t\t\ticon: 'edit_note',\r\n\t\t\t\tclick: this.bulkManagement(false),\r\n\t\t\t\tclass: 'edit',\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\treturn this.configType === 'server'\r\n\t\t\t? {\r\n\t\t\t\t\t...config,\r\n\t\t\t\t\tpaginate: this.setDocuments.bind(this),\r\n\t\t\t\t\tperPage: this.perPage,\r\n\t\t\t\t\tsetPerPage: this.crudService.setPerPage?.bind(\r\n\t\t\t\t\t\tthis.crudService,\r\n\t\t\t\t\t),\r\n\t\t\t\t\tallDocs: false,\r\n\t\t\t\t}\r\n\t\t\t: config;\r\n\t}\r\n\r\n\t/** Name of the collection or module used for contextual actions. */\r\n\tprivate _module = '';\r\n}\r\n","import {\r\n\tChangeDetectorRef,\r\n\tDestroyRef,\r\n\tDirective,\r\n\tElementRef,\r\n\tinject,\r\n\toutput,\r\n} from '@angular/core';\r\n\r\n/**\r\n * Stand-alone “click outside” directive (zoneless-safe).\r\n *\r\n * Usage:\r\n * <div (clickOutside)=\"close()\">…</div>\r\n */\r\n@Directive({\r\n\tselector: '[clickOutside]',\r\n})\r\nexport class ClickOutsideDirective {\r\n\treadonly clickOutside = output<MouseEvent>();\r\n\r\n\tconstructor() {\r\n\t\tdocument.addEventListener('pointerdown', this.handler, true);\r\n\r\n\t\t// cleanup\r\n\t\tthis._dref.onDestroy(() =>\r\n\t\t\tdocument.removeEventListener('pointerdown', this.handler, true),\r\n\t\t);\r\n\t}\r\n\r\n\tprivate _host = inject(ElementRef<HTMLElement>);\r\n\r\n\tprivate _cdr = inject(ChangeDetectorRef);\r\n\r\n\tprivate _dref = inject(DestroyRef);\r\n\r\n\tprivate handler = (e: MouseEvent): void => {\r\n\t\tif (!this._host.nativeElement.contains(e.target as Node)) {\r\n\t\t\tthis.clickOutside.emit(e); // notify parent\r\n\t\t\tthis._cdr.markForCheck(); // trigger CD for OnPush comps\r\n\t\t}\r\n\t};\r\n}\r\n","import { Directive, ElementRef, effect, inject, input } from '@angular/core';\r\n\r\n@Directive({\r\n\tselector: 'input[manualDisabled], textarea[manualDisabled]',\r\n})\r\nexport class ManualDisabledDirective {\r\n\tprivate readonly el = inject(ElementRef) as ElementRef<HTMLInputElement>;\r\n\r\n\t// Bind as: [manualDisabled]=\"isDisabled\"\r\n\treadonly manualDisabled = input<boolean | null>(null, {\r\n\t\talias: 'manualDisabled',\r\n\t});\r\n\r\n\tprivate readonly syncDisabledEffect = effect(() => {\r\n\t\tconst disabled = this.manualDisabled();\r\n\t\tif (disabled == null) return;\r\n\r\n\t\tconst native = this.el.nativeElement;\r\n\t\tif (!native) return;\r\n\r\n\t\tnative.disabled = !!disabled;\r\n\t});\r\n}\r\n","import { Directive, ElementRef, effect, inject, input } from '@angular/core';\r\n\r\n@Directive({\r\n\tselector: 'input[manualName], textarea[manualName]',\r\n})\r\nexport class ManualNameDirective {\r\n\tprivate readonly el = inject(ElementRef) as ElementRef<HTMLInputElement>;\r\n\r\n\t// Bind as: manualName=\"email\" or [manualName]=\"expr\"\r\n\treadonly manualName = input<string | null>(null, { alias: 'manualName' });\r\n\r\n\tprivate readonly syncNameEffect = effect(() => {\r\n\t\tconst name = this.manualName();\r\n\t\tif (name == null) return;\r\n\r\n\t\tconst native = this.el.nativeElement;\r\n\t\tif (!native) return;\r\n\r\n\t\tif (native.name !== name) {\r\n\t\t\tnative.name = name;\r\n\t\t}\r\n\t});\r\n}\r\n","import { Directive, ElementRef, effect, inject, input } from '@angular/core';\r\n\r\n@Directive({\r\n\tselector: 'input[manualReadonly], textarea[manualReadonly]',\r\n})\r\nexport class ManualReadonlyDirective {\r\n\tprivate readonly el = inject(ElementRef) as ElementRef<HTMLInputElement>;\r\n\r\n\t// Bind as: [manualReadonly]=\"true\"\r\n\treadonly manualReadonly = input<boolean | null>(null, {\r\n\t\talias: 'manualReadonly',\r\n\t});\r\n\r\n\tprivate readonly syncReadonlyEffect = effect(() => {\r\n\t\tconst readonly = this.manualReadonly();\r\n\t\tif (readonly == null) return;\r\n\r\n\t\tconst native = this.el.nativeElement;\r\n\t\tif (!native) return;\r\n\r\n\t\tnative.readOnly = !!readonly;\r\n\t});\r\n}\r\n","import { Directive, ElementRef, effect, inject, input } from '@angular/core';\r\n\r\n@Directive({\r\n\tselector: 'input[manualType], textarea[manualType]',\r\n})\r\nexport class ManualTypeDirective {\r\n\tprivate readonly el = inject(ElementRef) as ElementRef<HTMLInputElement>;\r\n\r\n\t// Bind as: manualType=\"password\" or [manualType]=\"expr\"\r\n\treadonly manualType = input<string | null>(null, { alias: 'manualType' });\r\n\r\n\tprivate readonly syncTypeEffect = effect(() => {\r\n\t\tconst t = this.manualType();\r\n\t\tif (!t) return;\r\n\r\n\t\tconst native = this.el.nativeElement;\r\n\t\tif (!native) return;\r\n\r\n\t\tif (native.type !== t) {\r\n\t\t\tnative.type = t;\r\n\t\t}\r\n\t});\r\n}\r\n","import { Pipe, PipeTransform } from '@angular/core';\r\n\r\n@Pipe({\r\n\tname: 'arr',\r\n})\r\nexport class ArrPipe implements PipeTransform {\r\n\ttransform(data: any, type?: any, refresh?: any): any {\r\n\t\tif (!data) {\r\n\t\t\treturn [];\r\n\t\t}\r\n\r\n\t\tif (typeof data == 'string') return data.split(type || ' ');\r\n\r\n\t\tif (Array.isArray(data)) {\r\n\t\t\treturn data;\r\n\t\t}\r\n\r\n\t\tif (typeof data != 'object') {\r\n\t\t\treturn [];\r\n\t\t}\r\n\r\n\t\tlet arr = [];\r\n\r\n\t\tfor (let each in data) {\r\n\t\t\tif (!data[each]) continue;\r\n\r\n\t\t\tif (type == 'prop') {\r\n\t\t\t\tarr.push(each);\r\n\t\t\t} else if (type == 'value') {\r\n\t\t\t\tarr.push(data[each]);\r\n\t\t\t} else {\r\n\t\t\t\tarr.push({\r\n\t\t\t\t\tprop: each,\r\n\t\t\t\t\tvalue: data[each],\r\n\t\t\t\t});\r\n\t\t\t}\r\n\t\t}\r\n\r\n\t\treturn arr;\r\n\t}\r\n}\r\n","import { Pipe, PipeTransform } from '@angular/core';\r\n\r\n@Pipe({\r\n\tname: 'mongodate',\r\n})\r\nexport class MongodatePipe implements PipeTransform {\r\n\ttransform(_id: any) {\r\n\t\tif (!_id) return new Date();\r\n\r\n\t\tlet timestamp = _id.toString().substring(0, 8);\r\n\r\n\t\treturn new Date(parseInt(timestamp, 16) * 1000);\r\n\t}\r\n}\r\n","import { Pipe, PipeTransform } from '@angular/core';\r\n\r\n@Pipe({\r\n\tname: 'number',\r\n})\r\nexport class NumberPipe implements PipeTransform {\r\n\ttransform(value: unknown): number {\r\n\t\tconst result = Number(value); // Convert value to a number\r\n\r\n\t\treturn isNaN(result) ? 0 : result; // Return 0 if conversion fails\r\n\t}\r\n}\r\n","import { Pipe, PipeTransform } from '@angular/core';\r\n\r\n@Pipe({\r\n\tname: 'page',\r\n\tpure: false,\r\n})\r\nexport class PaginationPipe implements PipeTransform {\r\n\ttransform(arr: any, config: any, sort: any, search = ''): any {\r\n\t\tif (!Array.isArray(arr)) return [];\r\n\r\n\t\tarr = arr.slice();\r\n\r\n\t\tfor (let i = 0; i < arr.length; i++) {\r\n\t\t\tarr[i].num = i + 1;\r\n\t\t}\r\n\r\n\t\tif (sort.direction) {\r\n\t\t\tarr.sort((a: any, b: any) => {\r\n\t\t\t\tif (a[sort.title] < b[sort.title]) {\r\n\t\t\t\t\treturn sort.direction == 'desc' ? 1 : -1;\r\n\t\t\t\t}\r\n\r\n\t\t\t\tif (a[sort.title] > b[sort.title]) {\r\n\t\t\t\t\treturn sort.direction == 'desc' ? -1 : 1;\r\n\t\t\t\t}\r\n\r\n\t\t\t\treturn 0;\r\n\t\t\t});\r\n\t\t}\r\n\r\n\t\treturn arr.slice(\r\n\t\t\t(config.page - 1) * config.perPage,\r\n\t\t\tconfig.page * config.perPage,\r\n\t\t);\r\n\t}\r\n}\r\n","import { inject, Pipe } from '@angular/core';\r\nimport { DomSanitizer } from '@angular/platform-browser';\r\n@Pipe({\r\n\tname: 'safe',\r\n})\r\nexport class SafePipe {\r\n\ttransform(html: any) {\r\n\t\treturn this._sanitizer.bypassSecurityTrustResourceUrl(html);\r\n\t}\r\n\r\n\tprivate _sanitizer = inject(DomSanitizer);\r\n}\r\n","import { Pipe, PipeTransform, Signal, isSignal } from '@angular/core';\r\n\r\ntype Query =\r\n\t| string\r\n\t| string[]\r\n\t| Record<string, unknown>\r\n\t| Signal<string | string[] | Record<string, unknown> | undefined>;\r\n\r\ntype Field =\r\n\t| string\r\n\t| string[]\r\n\t| number\r\n\t| Signal<string | string[] | number | undefined>;\r\n\r\n@Pipe({ name: 'search', pure: true })\r\nexport class SearchPipe implements PipeTransform {\r\n\ttransform<T>(\r\n\t\titems: T[] | Record<string, T>,\r\n\t\tquery?: Query,\r\n\t\tfields?: Field,\r\n\t\tlimit?: number,\r\n\t\tignore = false,\r\n\t\t_reload?: unknown,\r\n\t): T[] {\r\n\t\t/* unwrap signals */\r\n\t\tconst q = isSignal(query) ? query() : query;\r\n\r\n\t\tlet f = isSignal(fields) ? fields() : fields;\r\n\r\n\t\t/* allow “fields” to be a number (=limit) */\r\n\t\tif (typeof f === 'number') {\r\n\t\t\tlimit = f;\r\n\r\n\t\t\tf = undefined;\r\n\t\t}\r\n\r\n\t\tconst docs = Array.isArray(items) ? items : Object.values(items);\r\n\r\n\t\tif (ignore || !q) return limit ? docs.slice(0, limit) : docs;\r\n\r\n\t\t/* normalise fields */\r\n\t\tconst paths: string[] = !f\r\n\t\t\t? ['name']\r\n\t\t\t: Array.isArray(f)\r\n\t\t\t\t? f\r\n\t\t\t\t: f.trim().split(/\\s+/);\r\n\r\n\t\t/* normalise query */\r\n\t\tconst needles: string[] = Array.isArray(q)\r\n\t\t\t? q.map((s) => s.toLowerCase())\r\n\t\t\t: typeof q === 'object'\r\n\t\t\t\t? Object.keys(q