wacom
Version:
Module which has common services, pipes, directives and interfaces which can be used on all projects.
4,122 lines • 156 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Inject, Optional, Injectable, signal, inject, ChangeDetectorRef, output, ElementRef, DestroyRef, Directive, input, effect, Pipe, isSignal, ApplicationRef, EnvironmentInjector, createComponent, makeEnvironmentProviders, NgModule } from '@angular/core';
import * as i1 from '@angular/router';
import * as i2 from '@angular/platform-browser';
import { DomSanitizer } from '@angular/platform-browser';
import { take, firstValueFrom, Subject, skip, takeUntil, share, filter, map, Observable, merge, combineLatest, timeout, ReplaySubject, EMPTY } from 'rxjs';
import { toObservable } from '@angular/core/rxjs-interop';
import * as i1$1 from '@angular/common/http';
import { HttpHeaders, HttpErrorResponse, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { first, catchError } from 'rxjs/operators';
import * as i1$2 from '@angular/common';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
const CONFIG_TOKEN = new InjectionToken('config');
const DEFAULT_CONFIG = {
store: {
prefix: 'waStore',
},
meta: {
useTitleSuffix: false,
warnMissingGuard: false,
defaults: { links: {} },
},
socket: false,
http: {
url: '',
headers: {},
},
};
const DEFAULT_HTTP_CONFIG = {
headers: {},
url: '',
};
const DEFAULT_NETWORK_CONFIG = {
endpoints: [
'https://api.webart.work/status',
// Opaque but useful reachability fallbacks:
'https://www.google.com/generate_204',
'https://www.gstatic.com/generate_204',
'https://www.cloudflare.com/cdn-cgi/trace',
],
intervalMs: 30_000,
timeoutMs: 2_500,
goodLatencyMs: 300,
maxConsecutiveFails: 3,
};
const NETWORK_CONFIG = new InjectionToken('NETWORK_CONFIG', {
factory: () => DEFAULT_NETWORK_CONFIG,
});
const isDefined = (val) => typeof val !== 'undefined';
class MetaService {
constructor(config, router, meta, titleService) {
this.config = config;
this.router = router;
this.meta = meta;
this.titleService = titleService;
this.config = this.config || DEFAULT_CONFIG;
this._meta = this.config.meta || {};
this._warnMissingGuard();
}
/**
* Sets the default meta tags.
*
* @param defaults - The default meta tags.
*/
setDefaults(defaults) {
this._meta.defaults = {
...this._meta.defaults,
...defaults,
};
}
/**
* Sets the title and optional title suffix.
*
* @param title - The title to set.
* @param titleSuffix - The title suffix to append.
* @returns The MetaService instance.
*/
setTitle(title, titleSuffix) {
let titleContent = isDefined(title)
? title || ''
: this._meta.defaults?.['title'] || '';
if (this._meta.useTitleSuffix) {
titleContent += isDefined(titleSuffix)
? titleSuffix
: this._meta.defaults?.['titleSuffix'] || '';
}
this._updateMetaTag('title', titleContent);
this._updateMetaTag('og:title', titleContent);
this._updateMetaTag('twitter:title', titleContent);
this.titleService.setTitle(titleContent);
return this;
}
/**
* Sets link tags.
*
* @param links - The links to set.
* @returns The MetaService instance.
*/
setLink(links) {
Object.keys(links).forEach((rel) => {
let link = document.createElement('link');
link.setAttribute('rel', rel);
link.setAttribute('href', links[rel]);
document.head.appendChild(link);
});
return this;
}
/**
* Sets a meta tag.
*
* @param tag - The meta tag name.
* @param value - The meta tag value.
* @param prop - The meta tag property.
*/
setTag(tag, value, prop) {
if (tag === 'title' || tag === 'titleSuffix') {
throw new Error(`Attempt to set ${tag} through 'setTag': 'title' and 'titleSuffix' are reserved. Use 'MetaService.setTitle' instead.`);
}
const content = (isDefined(value)
? value || ''
: this._meta.defaults?.[tag] || '') + '';
this._updateMetaTag(tag, content, prop);
if (tag === 'description') {
this._updateMetaTag('og:description', content, prop);
this._updateMetaTag('twitter:description', content, prop);
}
}
/**
* Updates a meta tag.
*
* @param tag - The meta tag name.
* @param value - The meta tag value.
* @param prop - The meta tag property.
*/
_updateMetaTag(tag, value, prop) {
prop =
prop ||
(tag.startsWith('og:') || tag.startsWith('twitter:')
? 'property'
: 'name');
this.meta.updateTag({ [prop]: tag, content: value });
}
/**
* Removes a meta tag.
*
* @param tag - The meta tag name.
* @param prop - The meta tag property.
*/
removeTag(tag, prop) {
prop =
prop ||
(tag.startsWith('og:') || tag.startsWith('twitter:')
? 'property'
: 'name');
this.meta.removeTag(`${prop}="${tag}"`);
}
/**
* Warns about missing meta guards in routes.
*/
_warnMissingGuard() {
if (isDefined(this._meta.warnMissingGuard) &&
!this._meta.warnMissingGuard) {
return;
}
const hasDefaultMeta = !!Object.keys(this._meta.defaults ?? {}).length;
const hasMetaGuardInArr = (it) => it && it.IDENTIFIER === 'MetaGuard';
let hasShownWarnings = false;
const checkRoute = (route) => {
const hasRouteMeta = route.data && route.data['meta'];
const showWarning = !isDefined(route.redirectTo) &&
(hasDefaultMeta || hasRouteMeta) &&
!(route.canActivate || []).some(hasMetaGuardInArr);
if (showWarning) {
console.warn(`Route with path "${route.path}" has ${hasRouteMeta ? '' : 'default '}meta tags, but does not use MetaGuard. Please add MetaGuard to the canActivate array in your route configuration`);
hasShownWarnings = true;
}
(route.children || []).forEach(checkRoute);
};
this.router.config.forEach(checkRoute);
if (hasShownWarnings) {
console.warn(`To disable these warnings, set metaConfig.warnMissingGuard: false in your MetaConfig passed to MetaModule.forRoot()`);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaService, deps: [{ token: CONFIG_TOKEN, optional: true }, { token: i1.Router }, { token: i2.Meta }, { token: i2.Title }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [CONFIG_TOKEN]
}, {
type: Optional
}] }, { type: i1.Router }, { type: i2.Meta }, { type: i2.Title }] });
class MetaGuard {
static { this.IDENTIFIER = 'MetaGuard'; }
constructor(metaService, config) {
this.metaService = metaService;
this.config = config;
if (!this.config)
this.config = DEFAULT_CONFIG;
this._meta = this.config.meta || {};
this._meta.defaults = this._meta.defaults || {};
}
canActivate(route, state) {
this._processRouteMetaTags(route.data && route.data['meta']);
return true;
}
_processRouteMetaTags(meta = {}) {
if (meta.disableUpdate) {
return;
}
if (meta.title) {
this.metaService.setTitle(meta.title, meta.titleSuffix);
}
if (meta.links && Object.keys(meta.links).length) {
this.metaService.setLink(meta.links);
}
if (this._meta.defaults?.links &&
Object.keys(this._meta.defaults.links).length) {
this.metaService.setLink(this._meta.defaults.links);
}
Object.keys(meta).forEach((prop) => {
if (prop === 'title' ||
prop === 'titleSuffix' ||
prop === 'links') {
return;
}
Object.keys(meta[prop]).forEach((key) => {
this.metaService.setTag(key, meta[prop][key], prop);
});
});
Object.keys(this._meta.defaults).forEach((key) => {
if (key in meta ||
key === 'title' ||
key === 'titleSuffix' ||
key === 'links') {
return;
}
this.metaService.setTag(key, this._meta.defaults[key]);
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaGuard, deps: [{ token: MetaService }, { token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaGuard, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MetaGuard, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: MetaService }, { type: undefined, decorators: [{
type: Inject,
args: [CONFIG_TOKEN]
}, {
type: Optional
}] }] });
// Core utilities and helpers for the Wacom app
// Add capitalize method to String prototype if it doesn't already exist
if (!String.prototype.capitalize) {
String.prototype.capitalize = function () {
if (this.length > 0) {
return this.charAt(0).toUpperCase() + this.slice(1).toLowerCase();
}
return '';
};
}
class CoreService {
constructor() {
this.deviceID = localStorage.getItem('deviceID') ||
(typeof crypto?.randomUUID === 'function'
? crypto.randomUUID()
: this.UUID());
// After While
this._afterWhile = {};
// Device management
this.device = '';
// Version management
this.version = '1.0.0';
this.appVersion = '';
this.dateVersion = '';
// Locking management
this._locked = {};
this._unlockResolvers = {};
localStorage.setItem('deviceID', this.deviceID);
this.detectDevice();
}
/**
* Generates a UUID (Universally Unique Identifier) version 4.
*
* This implementation uses `Math.random()` to generate random values,
* making it suitable for general-purpose identifiers, but **not** for
* cryptographic or security-sensitive use cases.
*
* The format follows the UUID v4 standard: `xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx`
* where:
* - `x` is a random hexadecimal digit (0–f)
* - `4` indicates UUID version 4
* - `y` is one of 8, 9, A, or B
*
* Example: `f47ac10b-58cc-4372-a567-0e02b2c3d479`
*
* @returns A string containing a UUID v4.
*/
UUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
/**
* Converts an object to an array. Optionally holds keys instead of values.
*
* @param {any} obj - The object to be converted.
* @param {boolean} [holder=false] - If true, the keys will be held in the array; otherwise, the values will be held.
* @returns {any[]} The resulting array.
*/
ota(obj, holder = false) {
if (Array.isArray(obj))
return obj;
if (typeof obj !== 'object' || obj === null)
return [];
const arr = [];
for (const each in obj) {
if (obj.hasOwnProperty(each) &&
(obj[each] ||
typeof obj[each] === 'number' ||
typeof obj[each] === 'boolean')) {
if (holder) {
arr.push(each);
}
else {
arr.push(obj[each]);
}
}
}
return arr;
}
/**
* Removes elements from `fromArray` that are present in `removeArray` based on a comparison field.
*
* @param {any[]} removeArray - The array of elements to remove.
* @param {any[]} fromArray - The array from which to remove elements.
* @param {string} [compareField='_id'] - The field to use for comparison.
* @returns {any[]} The modified `fromArray` with elements removed.
*/
splice(removeArray, fromArray, compareField = '_id') {
if (!Array.isArray(removeArray) || !Array.isArray(fromArray)) {
return fromArray;
}
const removeSet = new Set(removeArray.map((item) => item[compareField]));
return fromArray.filter((item) => !removeSet.has(item[compareField]));
}
/**
* Unites multiple _id values into a single unique _id.
* The resulting _id is unique regardless of the order of the input _id values.
*
* @param {...string[]} args - The _id values to be united.
* @returns {string} The unique combined _id.
*/
ids2id(...args) {
args.sort((a, b) => {
if (Number(a.toString().substring(0, 8)) >
Number(b.toString().substring(0, 8))) {
return 1;
}
return -1;
});
return args.join();
}
/**
* Delays the execution of a callback function for a specified amount of time.
* If called again within that time, the timer resets.
*
* @param {string | object | (() => void)} doc - A unique identifier for the timer, an object to host the timer, or the callback function.
* @param {() => void} [cb] - The callback function to execute after the delay.
* @param {number} [time=1000] - The delay time in milliseconds.
*/
afterWhile(doc, cb, time = 1000) {
if (typeof doc === 'function') {
cb = doc;
doc = 'common';
}
if (typeof cb === 'function' && typeof time === 'number') {
if (typeof doc === 'string') {
clearTimeout(this._afterWhile[doc]);
this._afterWhile[doc] = window.setTimeout(cb, time);
}
else if (typeof doc === 'object') {
clearTimeout(doc.__afterWhile);
doc.__afterWhile =
window.setTimeout(cb, time);
}
else {
console.warn('badly configured after while');
}
}
}
/**
* Recursively copies properties from one object to another.
* Handles nested objects, arrays, and Date instances appropriately.
*
* @param from - The source object from which properties are copied.
* @param to - The target object to which properties are copied.
*/
copy(from, to) {
for (const each in from) {
if (typeof from[each] !== 'object' ||
from[each] instanceof Date ||
Array.isArray(from[each]) ||
from[each] === null) {
to[each] = from[each];
}
else {
if (typeof to[each] !== 'object' ||
to[each] instanceof Date ||
Array.isArray(to[each]) ||
to[each] === null) {
to[each] = {};
}
this.copy(from[each], to[each]);
}
}
}
/**
* Detects the device type based on the user agent.
*/
detectDevice() {
const userAgent = navigator.userAgent || navigator.vendor || window.opera;
if (/windows phone/i.test(userAgent)) {
this.device = 'Windows Phone';
}
else if (/android/i.test(userAgent)) {
this.device = 'Android';
}
else if (/iPad|iPhone|iPod/.test(userAgent) &&
!window.MSStream) {
this.device = 'iOS';
}
else {
this.device = 'Web';
}
}
/**
* Checks if the device is a mobile device.
* @returns {boolean} - Returns true if the device is a mobile device.
*/
isMobile() {
return (this.device === 'Windows Phone' ||
this.device === 'Android' ||
this.device === 'iOS');
}
/**
* Checks if the device is a tablet.
* @returns {boolean} - Returns true if the device is a tablet.
*/
isTablet() {
return this.device === 'iOS' && /iPad/.test(navigator.userAgent);
}
/**
* Checks if the device is a web browser.
* @returns {boolean} - Returns true if the device is a web browser.
*/
isWeb() {
return this.device === 'Web';
}
/**
* Checks if the device is an Android device.
* @returns {boolean} - Returns true if the device is an Android device.
*/
isAndroid() {
return this.device === 'Android';
}
/**
* Checks if the device is an iOS device.
* @returns {boolean} - Returns true if the device is an iOS device.
*/
isIos() {
return this.device === 'iOS';
}
/**
* Sets the combined version string based on appVersion and dateVersion.
*/
setVersion() {
this.version = this.appVersion || '';
this.version += this.version && this.dateVersion ? ' ' : '';
this.version += this.dateVersion || '';
}
/**
* Sets the app version and updates the combined version string.
*
* @param {string} appVersion - The application version to set.
*/
setAppVersion(appVersion) {
this.appVersion = appVersion;
this.setVersion();
}
/**
* Sets the date version and updates the combined version string.
*
* @param {string} dateVersion - The date version to set.
*/
setDateVersion(dateVersion) {
this.dateVersion = dateVersion;
this.setVersion();
}
/**
* Locks a resource to prevent concurrent access.
* @param which - The resource to lock, identified by a string.
*/
lock(which) {
this._locked[which] = true;
if (!this._unlockResolvers[which]) {
this._unlockResolvers[which] = [];
}
}
/**
* Unlocks a resource, allowing access.
* @param which - The resource to unlock, identified by a string.
*/
unlock(which) {
this._locked[which] = false;
if (this._unlockResolvers[which]) {
this._unlockResolvers[which].forEach((resolve) => resolve());
this._unlockResolvers[which] = [];
}
}
/**
* Returns a Promise that resolves when the specified resource is unlocked.
* @param which - The resource to watch for unlocking, identified by a string.
* @returns A Promise that resolves when the resource is unlocked.
*/
onUnlock(which) {
if (!this._locked[which]) {
return Promise.resolve();
}
return new Promise((resolve) => {
if (!this._unlockResolvers[which]) {
this._unlockResolvers[which] = [];
}
this._unlockResolvers[which].push(resolve);
});
}
/**
* Checks if a resource is locked.
* @param which - The resource to check, identified by a string.
* @returns True if the resource is locked, false otherwise.
*/
locked(which) {
return !!this._locked[which];
}
// Angular Signals //
/**
* Converts a plain object into a signal-wrapped object.
* Optionally wraps specific fields of the object as individual signals,
* and merges them into the returned signal for fine-grained reactivity.
*
* @template Document - The type of the object being wrapped.
* @param {Document} document - The plain object to wrap into a signal.
* @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -
* Optional map where each key is a field name and the value is a function
* to extract the initial value for that field. These fields will be wrapped
* as separate signals and embedded in the returned object.
*
* @returns {WritableSignal<Document>} A signal-wrapped object, possibly containing
* nested field signals for more granular control.
*
* @example
* const user = { _id: '1', name: 'Alice', score: 42 };
* const sig = toSignal(user, { score: (u) => u.score });
* console.log(sig().name); // 'Alice'
* console.log(sig().score()); // 42 — field is now a signal
*/
toSignal(document, signalFields = {}) {
if (Object.keys(signalFields).length) {
const fields = {};
for (const key in signalFields) {
fields[key] = signal(signalFields[key](document));
}
return signal({ ...document, ...fields });
}
else {
return signal(document);
}
}
/**
* Converts an array of objects into an array of Angular signals.
* Optionally wraps specific fields of each object as individual signals.
*
* @template Document - The type of each object in the array.
* @param {Document[]} arr - Array of plain objects to convert into signals.
* @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -
* Optional map where keys are field names and values are functions that extract the initial value
* from the object. These fields will be turned into separate signals.
*
* @returns {WritableSignal<Document>[]} An array where each item is a signal-wrapped object,
* optionally with individual fields also wrapped in signals.
*
* @example
* toSignalsArray(users, {
* name: (u) => u.name,
* score: (u) => u.score,
* });
*/
toSignalsArray(arr, signalFields = {}) {
return arr.map((obj) => this.toSignal(obj, signalFields));
}
/**
* Adds a new object to the signals array.
* Optionally wraps specific fields of the object as individual signals before wrapping the whole object.
*
* @template Document - The type of the object being added.
* @param {WritableSignal<Document>[]} signals - The signals array to append to.
* @param {Document} item - The object to wrap and push as a signal.
* @param {Record<string, (doc: Document) => unknown>} [signalFields={}] -
* Optional map of fields to be wrapped as signals within the object.
*
* @returns {void}
*/
pushSignal(signals, item, signalFields = {}) {
signals.push(this.toSignal(item, signalFields));
}
/**
* Removes the first signal from the array whose object's field matches the provided value.
* @template Document
* @param {WritableSignal<Document>[]} signals - The signals array to modify.
* @param {unknown} value - The value to match.
* @param {string} [field='_id'] - The object field to match against.
* @returns {void}
*/
removeSignalByField(signals, value, field = '_id') {
const idx = signals.findIndex((sig) => sig()[field] === value);
if (idx > -1)
signals.splice(idx, 1);
}
/**
* Returns a generic trackBy function for *ngFor, tracking by the specified object field.
* @template Document
* @param {string} field - The object field to use for tracking (e.g., '_id').
* @returns {(index: number, sig: Signal<Document>) => unknown} TrackBy function for Angular.
*/
trackBySignalField(field) {
return (_, sig) => sig()[field];
}
/**
* Finds the first signal in the array whose object's field matches the provided value.
* @template Document
* @param {Signal<Document>[]} signals - Array of signals to search.
* @param {unknown} value - The value to match.
* @param {string} [field='_id'] - The object field to match against.
* @returns {Signal<Document> | undefined} The found signal or undefined if not found.
*/
findSignalByField(signals, value, field = '_id') {
return signals.find((sig) => sig()[field] === value);
}
/**
* Updates the first writable signal in the array whose object's field matches the provided value.
* @template Document
* @param {WritableSignal<Document>[]} signals - Array of writable signals to search.
* @param {unknown} value - The value to match.
* @param {(val: Document) => Document} updater - Function to produce the updated object.
* @param {string} field - The object field to match against.
* @returns {void}
*/
updateSignalByField(signals, value, updater, field) {
const sig = this.findSignalByField(signals, value, field);
if (sig)
sig.update(updater);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CoreService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CoreService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: CoreService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
/**
* Abstract reusable base class for CRUD list views.
* It encapsulates pagination, modals, and document handling logic.
*
* @template Service - A service implementing CrudServiceInterface for a specific document type
* @template Document - The data model extending CrudDocument
*/
class CrudComponent {
/**
* Constructor
*
* @param formConfig - Object describing form title and its component structure
* @param formService - Any service that conforms to FormServiceInterface (usually casted)
* @param crudService - CRUD service implementing get/create/update/delete
*/
constructor(formConfig, formService, crudService, module = '') {
/** The array of documents currently loaded and shown */
this.documents = signal([], ...(ngDevMode ? [{ debugName: "documents" }] : []));
/** Current pagination page */
this.page = 1;
/** CoreService handles timing and copying helpers */
this.__core = inject(CoreService);
/** ChangeDetectorRef handles on push strategy */
this.__cdr = inject(ChangeDetectorRef);
this.localDocumentsFilter = () => true;
/** Fields considered when performing bulk updates. */
this.updatableFields = ['_id', 'name', 'description', 'data'];
/** Data source mode used for document retrieval. */
this.configType = 'server';
/** Number of documents fetched per page when paginating. */
this.perPage = 20;
/** Name of the collection or module used for contextual actions. */
this._module = '';
const form = formConfig;
this.__form = formService;
this.form = form;
this.crudService = crudService;
this._module = module;
}
/**
* Loads documents for a given page.
*/
setDocuments(page = this.page, query = '') {
return new Promise((resolve) => {
if (this.configType === 'server') {
this.page = page;
this.__core.afterWhile(this, () => {
this.crudService
.get({ page, query }, this.getOptions())
.subscribe((docs) => {
this.documents.update(() => docs.map((doc) => this.crudService.getSignal(doc)));
resolve();
this.__cdr.markForCheck();
});
}, 250);
}
else {
this.documents.update(() => this.crudService
.getDocs()
.filter(this.localDocumentsFilter)
.map((doc) => this.crudService.getSignal(doc)));
this.crudService.loaded.pipe(take(1)).subscribe(() => {
resolve();
this.__cdr.markForCheck();
});
}
});
}
/**
* Clears temporary metadata before document creation.
*/
preCreate(doc) {
delete doc.__creating;
}
/**
* Funciton which controls whether the create functionality is available.
*/
allowCreate() {
return true;
}
/**
* Funciton which controls whether the update and delete functionality is available.
*/
allowMutate() {
return true;
}
/**
* Funciton which controls whether the unique url functionality is available.
*/
allowUrl() {
return true;
}
/** Determines whether manual sorting controls are available. */
allowSort() {
return false;
}
/**
* Funciton which prepare get crud options.
*/
getOptions() {
return {};
}
/**
* Handles bulk creation and updating of documents.
* In creation mode, adds new documents.
* In update mode, syncs changes and deletes removed entries.
*/
bulkManagement(isCreateFlow = true) {
return () => {
this.__form
.modalDocs(isCreateFlow
? []
: this.documents().map((obj) => Object.fromEntries(this.updatableFields.map((key) => [
key,
obj()[key],
]))))
.then(async (docs) => {
if (isCreateFlow) {
for (const doc of docs) {
this.preCreate(doc);
await firstValueFrom(this.crudService.create(doc));
}
}
else {
for (const document of this.documents()) {
if (!docs.find((d) => d._id === document()._id)) {
await firstValueFrom(this.crudService.delete(document()));
}
}
for (const doc of docs) {
const local = this.documents().find((document) => document()._id === doc._id);
if (local) {
local.update((document) => {
this.__core.copy(doc, document);
return document;
});
await firstValueFrom(this.crudService.update(local()));
}
else {
this.preCreate(doc);
await firstValueFrom(this.crudService.create(doc));
}
}
}
this.setDocuments();
});
};
}
/** Opens a modal to create a new document. */
create() {
this.__form.modal(this.form, {
label: 'Create',
click: async (created, close) => {
close();
this.preCreate(created);
await firstValueFrom(this.crudService.create(created));
this.setDocuments();
},
});
}
/** Displays a modal to edit an existing document. */
update(doc) {
this.__form.modal(this.form, {
label: 'Update',
click: (updated, close) => {
close();
this.__core.copy(updated, doc);
this.crudService.update(doc);
this.__cdr.markForCheck();
},
}, doc);
}
/** Requests confirmation before deleting the provided document. */
async delete(doc) {
this.crudService.delete(doc).subscribe(() => {
this.setDocuments();
});
}
/** Opens a modal to edit the document's unique URL. */
mutateUrl(doc) {
this.__form.modalUnique(this._module, 'url', doc);
}
/** Moves the given document one position up and updates ordering. */
moveUp(doc) {
const index = this.documents().findIndex((document) => document()._id === doc._id);
if (index) {
this.documents.update((documents) => {
documents.splice(index, 1);
documents.splice(index - 1, 0, this.crudService.getSignal(doc));
return documents;
});
}
for (let i = 0; i < this.documents().length; i++) {
if (this.documents()[i]().order !== i) {
this.documents()[i]().order = i;
this.crudService.update(this.documents()[i]());
}
}
this.__cdr.markForCheck();
}
/**
* Configuration object used by the UI for rendering table and handling actions.
*/
getConfig() {
const config = {
create: this.allowCreate()
? () => {
this.create();
}
: null,
update: this.allowMutate()
? (doc) => {
this.update(doc);
}
: null,
delete: this.allowMutate()
? (doc) => {
this.delete(doc);
}
: null,
buttons: [],
headerButtons: [],
allDocs: true,
};
if (this.allowUrl()) {
config.buttons.push({
icon: 'cloud_download',
click: (doc) => {
this.mutateUrl(doc);
},
});
}
if (this.allowSort()) {
config.buttons.push({
icon: 'arrow_upward',
click: (doc) => {
this.moveUp(doc);
},
});
}
if (this.allowCreate()) {
config.headerButtons.push({
icon: 'playlist_add',
click: this.bulkManagement(),
class: 'playlist',
});
}
if (this.allowMutate()) {
config.headerButtons.push({
icon: 'edit_note',
click: this.bulkManagement(false),
class: 'edit',
});
}
return this.configType === 'server'
? {
...config,
paginate: this.setDocuments.bind(this),
perPage: this.perPage,
setPerPage: this.crudService.setPerPage?.bind(this.crudService),
allDocs: false,
}
: config;
}
}
/**
* Stand-alone “click outside” directive (zoneless-safe).
*
* Usage:
* <div (clickOutside)="close()">…</div>
*/
class ClickOutsideDirective {
constructor() {
this.clickOutside = output();
this._host = inject((ElementRef));
this._cdr = inject(ChangeDetectorRef);
this._dref = inject(DestroyRef);
this.handler = (e) => {
if (!this._host.nativeElement.contains(e.target)) {
this.clickOutside.emit(e); // notify parent
this._cdr.markForCheck(); // trigger CD for OnPush comps
}
};
document.addEventListener('pointerdown', this.handler, true);
// cleanup
this._dref.onDestroy(() => document.removeEventListener('pointerdown', this.handler, true));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ClickOutsideDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.0", type: ClickOutsideDirective, isStandalone: true, selector: "[clickOutside]", outputs: { clickOutside: "clickOutside" }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ClickOutsideDirective, decorators: [{
type: Directive,
args: [{
selector: '[clickOutside]',
}]
}], ctorParameters: () => [], propDecorators: { clickOutside: [{ type: i0.Output, args: ["clickOutside"] }] } });
class ManualDisabledDirective {
constructor() {
this.el = inject(ElementRef);
// Bind as: [manualDisabled]="isDisabled"
this.manualDisabled = input(null, { ...(ngDevMode ? { debugName: "manualDisabled" } : {}), alias: 'manualDisabled' });
this.syncDisabledEffect = effect(() => {
const disabled = this.manualDisabled();
if (disabled == null)
return;
const native = this.el.nativeElement;
if (!native)
return;
native.disabled = !!disabled;
}, ...(ngDevMode ? [{ debugName: "syncDisabledEffect" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualDisabledDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualDisabledDirective, isStandalone: true, selector: "input[manualDisabled], textarea[manualDisabled]", inputs: { manualDisabled: { classPropertyName: "manualDisabled", publicName: "manualDisabled", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualDisabledDirective, decorators: [{
type: Directive,
args: [{
selector: 'input[manualDisabled], textarea[manualDisabled]',
}]
}], propDecorators: { manualDisabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualDisabled", required: false }] }] } });
class ManualNameDirective {
constructor() {
this.el = inject(ElementRef);
// Bind as: manualName="email" or [manualName]="expr"
this.manualName = input(null, { ...(ngDevMode ? { debugName: "manualName" } : {}), alias: 'manualName' });
this.syncNameEffect = effect(() => {
const name = this.manualName();
if (name == null)
return;
const native = this.el.nativeElement;
if (!native)
return;
if (native.name !== name) {
native.name = name;
}
}, ...(ngDevMode ? [{ debugName: "syncNameEffect" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualNameDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualNameDirective, isStandalone: true, selector: "input[manualName], textarea[manualName]", inputs: { manualName: { classPropertyName: "manualName", publicName: "manualName", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualNameDirective, decorators: [{
type: Directive,
args: [{
selector: 'input[manualName], textarea[manualName]',
}]
}], propDecorators: { manualName: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualName", required: false }] }] } });
class ManualReadonlyDirective {
constructor() {
this.el = inject(ElementRef);
// Bind as: [manualReadonly]="true"
this.manualReadonly = input(null, { ...(ngDevMode ? { debugName: "manualReadonly" } : {}), alias: 'manualReadonly' });
this.syncReadonlyEffect = effect(() => {
const readonly = this.manualReadonly();
if (readonly == null)
return;
const native = this.el.nativeElement;
if (!native)
return;
native.readOnly = !!readonly;
}, ...(ngDevMode ? [{ debugName: "syncReadonlyEffect" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualReadonlyDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualReadonlyDirective, isStandalone: true, selector: "input[manualReadonly], textarea[manualReadonly]", inputs: { manualReadonly: { classPropertyName: "manualReadonly", publicName: "manualReadonly", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualReadonlyDirective, decorators: [{
type: Directive,
args: [{
selector: 'input[manualReadonly], textarea[manualReadonly]',
}]
}], propDecorators: { manualReadonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualReadonly", required: false }] }] } });
class ManualTypeDirective {
constructor() {
this.el = inject(ElementRef);
// Bind as: manualType="password" or [manualType]="expr"
this.manualType = input(null, { ...(ngDevMode ? { debugName: "manualType" } : {}), alias: 'manualType' });
this.syncTypeEffect = effect(() => {
const t = this.manualType();
if (!t)
return;
const native = this.el.nativeElement;
if (!native)
return;
if (native.type !== t) {
native.type = t;
}
}, ...(ngDevMode ? [{ debugName: "syncTypeEffect" }] : []));
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualTypeDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.0", type: ManualTypeDirective, isStandalone: true, selector: "input[manualType], textarea[manualType]", inputs: { manualType: { classPropertyName: "manualType", publicName: "manualType", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ManualTypeDirective, decorators: [{
type: Directive,
args: [{
selector: 'input[manualType], textarea[manualType]',
}]
}], propDecorators: { manualType: [{ type: i0.Input, args: [{ isSignal: true, alias: "manualType", required: false }] }] } });
class ArrPipe {
transform(data, type, refresh) {
if (!data) {
return [];
}
if (typeof data == 'string')
return data.split(type || ' ');
if (Array.isArray(data)) {
return data;
}
if (typeof data != 'object') {
return [];
}
let arr = [];
for (let each in data) {
if (!data[each])
continue;
if (type == 'prop') {
arr.push(each);
}
else if (type == 'value') {
arr.push(data[each]);
}
else {
arr.push({
prop: each,
value: data[each],
});
}
}
return arr;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ArrPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: ArrPipe, isStandalone: true, name: "arr" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: ArrPipe, decorators: [{
type: Pipe,
args: [{
name: 'arr',
}]
}] });
class MongodatePipe {
transform(_id) {
if (!_id)
return new Date();
let timestamp = _id.toString().substring(0, 8);
return new Date(parseInt(timestamp, 16) * 1000);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MongodatePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: MongodatePipe, isStandalone: true, name: "mongodate" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: MongodatePipe, decorators: [{
type: Pipe,
args: [{
name: 'mongodate',
}]
}] });
class NumberPipe {
transform(value) {
const result = Number(value); // Convert value to a number
return isNaN(result) ? 0 : result; // Return 0 if conversion fails
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NumberPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: NumberPipe, isStandalone: true, name: "number" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NumberPipe, decorators: [{
type: Pipe,
args: [{
name: 'number',
}]
}] });
class PaginationPipe {
transform(arr, config, sort, search = '') {
if (!Array.isArray(arr))
return [];
arr = arr.slice();
for (let i = 0; i < arr.length; i++) {
arr[i].num = i + 1;
}
if (sort.direction) {
arr.sort((a, b) => {
if (a[sort.title] < b[sort.title]) {
return sort.direction == 'desc' ? 1 : -1;
}
if (a[sort.title] > b[sort.title]) {
return sort.direction == 'desc' ? -1 : 1;
}
return 0;
});
}
return arr.slice((config.page - 1) * config.perPage, config.page * config.perPage);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PaginationPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: PaginationPipe, isStandalone: true, name: "page", pure: false }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: PaginationPipe, decorators: [{
type: Pipe,
args: [{
name: 'page',
pure: false,
}]
}] });
class SafePipe {
constructor() {
this._sanitizer = inject(DomSanitizer);
}
transform(html) {
return this._sanitizer.bypassSecurityTrustResourceUrl(html);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SafePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SafePipe, isStandalone: true, name: "safe" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SafePipe, decorators: [{
type: Pipe,
args: [{
name: 'safe',
}]
}] });
class SearchPipe {
transform(items, query, fields, limit, ignore = false, _reload) {
/* unwrap signals */
const q = isSignal(query) ? query() : query;
let f = isSignal(fields) ? fields() : fields;
/* allow “fields” to be a number (=limit) */
if (typeof f === 'number') {
limit = f;
f = undefined;
}
const docs = Array.isArray(items) ? items : Object.values(items);
if (ignore || !q)
return limit ? docs.slice(0, limit) : docs;
/* normalise fields */
const paths = !f
? ['name']
: Array.isArray(f)
? f
: f.trim().split(/\s+/);
/* normalise query */
const needles = Array.isArray(q)
? q.map((s) => s.toLowerCase())
: typeof q === 'object'
? Object.keys(q)
.filter((k) => q[k])
.map((k) => k.toLowerCase())
: [q.toLowerCase()];
const txtMatches = (val) => {
if (val == null)
return false;
const hay = val.toString().toLowerCase();
return needles.some((n) => hay.includes(n) || n.includes(hay));
};
const walk = (obj, parts) => {
if (!obj)
return false;
const [head, ...rest] = parts;
const next = obj[head];
if (Array.isArray(next))
return next.some((v) => rest.length ? walk(v, rest) : txtMatches(v));
return rest.length ? walk(next, rest) : txtMatches(next);
};
const out = [];
const seen = new Set();
const check = (doc, key) => {
for (const p of paths) {
if (walk(doc, p.split('.'))) {
if (!seen.has(key)) {
out.push(doc);
seen.add(key);
}
break;
}
}
};
Array.isArray(items)
? docs.forEach((d, i) => check(d, i))
: Object.entries(items).forEach(([k, v]) => check(v, k));
return limit ? out.slice(0, limit) : out;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SearchPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SearchPipe, isStandalone: true, name: "search" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SearchPipe, decorators: [{
type: Pipe,
args: [{ name: 'search', pure: true }]
}] });
class SplicePipe {
transform(from, which, refresh) {
if (Array.isArray(from))
from = { arr: from, prop: '_id' };
let arr = (which.keep && []) || from.arr.slice();
if (Array.isArray(which))
which = { arr: which, prop: '_id' };
for (let i = from.arr.length - 1; i >= 0; i--) {
for (let j = 0; j < which.arr.length; j++) {
if (from.prop && which.prop) {
if (from.arr[i][from.prop] == which.arr[j][which.prop]) {
if (which.keep) {
arr.push(from.arr[i]);
}
else {
arr.splice(i, 1);
}
break;
}
}
else if (from.prop) {
if (from.arr[i][from.prop] == which.arr[j]) {
if (which.keep) {
arr.push(from.arr[i]);
}
else {
arr.splice(i, 1);
}
break;
}
}
else if (which.prop) {
if (from.arr[i] == which.arr[j][which.prop]) {
if (which.keep) {
arr.push(from.arr[i]);
}
else {
arr.splice(i, 1);
}
break;
}
}
else if (from.arr[i] == which.arr[j]) {
if (which.keep) {
arr.push(from.arr[i]);
}
else {
arr.splice(i, 1);
}
break;
}
}
}
return arr;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplicePipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SplicePipe, isStandalone: true, name: "splice" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplicePipe, decorators: [{
type: Pipe,
args: [{
name: 'splice',
}]
}] });
class SplitPipe {
transform(value, index = 0, devider = ':') {
const arr = value.split(devider);
return arr.length > index ? arr[index] : '';
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplitPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: SplitPipe, isStandalone: true, name: "split" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SplitPipe, decorators: [{
type: Pipe,
args: [{
name: 'split',
}]
}] });
class EmitterService {
constructor() {
this._signals = new Map();
this._closers = new Map();
this._streams = new Map();
this._done = new Map();
}
_getSignal(id) {
let s = this._signals.get(id);
if (!s) {
// emit even if same payload repeats
s = signal(undefined, { equal: () => false });
this._signals.set(id, s);
}
return s;
}
_getCloser(id) {
let c = this._closers.get(id);
if (!c) {
c = new Subject();
this._closers.set(id, c);
}
return c;
}
_getStream(id) {
let obs$ = this._streams.get(id);
if (!obs$) {
const sig = this._getSignal(id);
const closed$ = this._getCloser(id);
obs$ = toObservable(sig).pipe(
// Subject-like: don't replay the current value on subscribe
skip(1), takeUntil(closed$), share());
this._streams.set(id, obs$);
}
return obs$;
}
/** Emit an event */
emit(id, data) {
this._getSignal(id).set(data);
}
/** Listen for events (hot, completes when off(id) is called) */
on(id) {
return this._getStream(id);
}
/** Complete and remove a channel */
off(id) {
const closer = this._closers.get(id);
if (closer) {
closer.next();
closer.complete();
this._closers.delete(id);
}
this._signals.delete(id);
this._streams.delete(id);
}
offAll() {
for (const id of Array.from(this._closers.keys()))
this.off(id);
}
has(id) {
return this._signals.has(id);
}
_getDoneSignal(id) {
let s = this._done.get(id);
if (!s) {
s = signal(undefined);
this._done.set(id, s);
}
return s;
}
/** Mark task as completed with a payload (default: true) */
complete(task, value = true) {
this._getDoneSignal(task).set(value);
}
/** Clear completion so it can be awaited again */
clearCompleted(task) {
const s = this._done.get(task) ?? this._getDoneSignal(task);
s.set(undefined);
}
/** Read current completion payload (undefined => not completed) */
completed(task) {
return this._getDoneSignal(task)();
}
isCompleted(task) {
return this._getDoneSignal(task)() !== undefined;
}
onComplete(tasks, opts) {
const list = (Array.isArray(tasks) ? tasks : [tasks]).filter(Boolean);
const streams = list.map((id) => toObservable(this._getDoneSignal(id)).pipe(filter((v) => v !== undefined), map((v) => v)));
let source$;
if (list.length <= 1) {
// single-task await
source$ = streams[0]?.pipe(take(1)) ?? new Observable();
}
else if (opts?.mode === 'any') {
source$ = merge(...streams).pipe(take(1));
}
else {
source$ = combineLatest(streams).pipe(take(1));
}
if (opts?.timeoutMs && Number.isFinite(opts.timeoutMs)) {
source$ = source$.pipe(timeout({ first: opts.timeoutMs }));
}
if (opts?.abort) {
const abort$ = new Observable((sub) => {
const handler = () => {
sub.next();
sub.complete();
};
opts.abort.addEventListener('abort', handler);
return () => opts.abort.removeEventListener('abort', handler);
});
source$ = source$.pipe(takeUntil(abort$));
}
return source$;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EmitterService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EmitterService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: EmitterService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class HttpService {
constructor(config, _http) {
this._http = _http;
// An array of error handling callbacks
this.errors = [];
// Base URL for HTTP requests
this.url = localStorage.getItem('wacom-http.url') || '';
// Flag to lock the service to prevent multiple requests
this.locked = false;
// Array to store setTimeout IDs for managing request locks
this.awaitLocked = [];
// Object to store HTTP headers
this._headers = localStorage.getItem('wacom-http.headers')
? JSON.parse(localStorage.getItem('wacom-http.headers'))
: {};
// Instance of HttpHeaders with current headers
this._http_headers = new HttpHeaders(this._headers);
// Initialize HTTP configuration and headers from injected config
this._config = {
...DEFAULT_HTTP_CONFIG,
...(config.http || {}),
};
if (typeof this._config.url === 'string') {
this.setUrl(this._config.url);
}
if (typeof this._config.headers === 'object') {
for (const header in this._config.headers) {
this._headers[header] = this._config.headers[header];
}
this._http_headers = new HttpHeaders(this._headers);
}
}
// Set a new base URL and save it in the store
setUrl(url) {
this.url = url;
localStorage.setItem('wacom-http.url', url);
}
// Remove the base URL and revert to the default or stored one
removeUrl() {
this.url = this._config.url || '';
localStorage.removeItem('wacom-http.url');
}
// Set a new HTTP header and update the stored headers
set(key, value) {
this._headers[key] = value;
localStorage.setItem('wacom-http.headers', JSON.stringify(this._headers));
this._http_headers = new HttpHeaders(this._headers);
}
// Get the value of a specific HTTP header
header(key) {
return this._headers[key];
}
// Remove a specific HTTP header and update the stored headers
remove(key) {
delete this._headers[key];
localStorage.setItem('wacom-http.headers', JSON.stringify(this._headers));
this._http_headers = new HttpHeaders(this._headers);
}
// Internal method to make HTTP requests based on the method type
_httpMethod(method, _url, doc, headers) {
if (method === 'post') {
return this._http.post(_url, doc, headers);
}
else if (method === 'put') {
return this._http.put(_url, doc, headers);
}
else if (method === 'patch') {
return this._http.patch(_url, doc, headers);
}
else if (method === 'delete') {
return this._http.delete(_url, headers);
}
else {
return this._http.get(_url, headers);
}
}
/**
* Internal method to handle HTTP requests for various methods (POST, PUT, PATCH, DELETE, GET).
*
* Features:
* - **Request Locking**: Manages request locking to prevent simultaneous requests.
* - **Acceptance Check**: Validates the server response against a user-defined `acceptance` function.
* If the check fails, the response is rejected with an error.
* - **Replace Logic**: Allows modification of specific parts of the response object, determined by a user-defined `replace` function.
* Can handle both objects and arrays within the response.
* - **Field Filtering**: Supports extracting specific fields from response objects or arrays.
* - **Legacy Support**: Compatible with callback-based usage alongside Observables.
* - **ReplaySubject**: Ensures that the response can be shared across multiple subscribers.
*
* @param url - The endpoint to send the HTTP request to (relative to the base URL).
* @param doc - The request payload for methods like POST, PUT, and PATCH.
* @param callback - A legacy callback function to handle the response.
* @param opts - Additional options:
* - `err`: Error handling callback.
* - `acceptance`: Function to validate the server response. Should return `true` for valid responses.
* - `replace`: Function to modify specific parts of the response data.
* - `fields`: Array of fields to extract from the response object(s).
* - `data`: Path in the response where the data resides for `replace` and `fields` operations.
* - `skipLock`: If `true`, bypasses request locking.
* - `url`: Overrides the base URL for this request.
* @param method - The HTTP method (e.g., 'post', 'put', 'patch', 'delete', 'get').
* @returns An Observable that emits the processed HTTP response or an error.
*/
_post(url, doc, callback = (resp) => { }, opts = {}, method = 'post') {
if (typeof opts === 'function') {
opts = { err: opts };
}
if (!opts.err) {
opts.err = (err) => { };
}
// Handle request locking to avoid multiple simultaneous requests
if (this.locked && !opts.skipLock) {
return new Observable((observer) => {
const wait = setTimeout(() => {
this._post(url, doc, callback, opts, method).subscribe(observer);
}, 100);
this.awaitLocked.push(wait);
});
}
const _url = (opts.url || this.url) + url;
this.prepare_handle(_url, doc);
// Using ReplaySubject to allow multiple subscriptions without re-triggering the HTTP request
const responseSubject = new ReplaySubject(1);
this._httpMethod(method, _url, doc, { headers: this._http_headers })
.pipe(first(), catchError((error) => {
this.handleError(opts.err, () => {
this._post(url, doc, callback, opts, method).subscribe(responseSubject);
})(error);
responseSubject.error(error);
return EMPTY;
}))
.subscribe({
next: (resp) => {
if (opts.acceptance &&
typeof opts.acceptance === 'function') {
if (!opts.acceptance(resp)) {
const error = new HttpErrorResponse({
error: 'Acceptance failed',
status: 400,
});
this.handleError(opts.err, () => { })(error);
responseSubject.error(error);
return;
}
}
if (opts.replace && typeof opts.replace === 'function') {
if (Array.isArray(this._getObjectToReplace(resp, opts.data))) {
this._getObjectToReplace(resp, opts.data).map((item) => opts.replace(item));
}
else if (this._getObjectToReplace(resp, opts.data)) {
opts.replace(this._getObjectToReplace(resp, opts.data));
}
}
if (Array.isArray(opts.fields)) {
if (Array.isArray(this._getObjectToReplace(resp, opts.data))) {
this._getObjectToReplace(resp, opts.data).map((item) => {
return this._newDoc(item, opts.fields);
});
}
else if (this._getObjectToReplace(resp, opts.data)) {
const newDoc = this._newDoc(this._getObjectToReplace(resp, opts.data), opts.fields);
if (opts.data) {
this._setObjectToReplace(resp, opts.data, newDoc);
}
else {
resp = newDoc;
}
}
}
this.response_handle(_url, resp, () => callback(resp));
responseSubject.next(resp);
responseSubject.complete();
},
error: (err) => responseSubject.error(err),
complete: () => responseSubject.complete(),
});
return responseSubject.asObservable();
}
/**
* Public method to perform a POST request.
* - Supports legacy callback usage.
* - Returns an Observable for reactive programming.
*/
post(url, doc, callback = (resp) => { }, opts = {}) {
return this._post(url, doc, callback, opts);
}
/**
* Public method to perform a PUT request.
* - Supports legacy callback usage.
* - Returns an Observable for reactive programming.
*/
put(url, doc, callback = (resp) => { }, opts = {}) {
return this._post(url, doc, callback, opts, 'put');
}
/**
* Public method to perform a PATCH request.
* - Supports legacy callback usage.
* - Returns an Observable for reactive programming.
*/
patch(url, doc, callback = (resp) => { }, opts = {}) {
return this._post(url, doc, callback, opts, 'patch');
}
/**
* Public method to perform a DELETE request.
* - Supports legacy callback usage.
* - Returns an Observable for reactive programming.
*/
delete(url, callback = (resp) => { }, opts = {}) {
return this._post(url, null, callback, opts, 'delete');
}
/**
* Public method to perform a GET request.
* - Supports legacy callback usage.
* - Returns an Observable for reactive programming.
*/
get(url, callback = (resp) => { }, opts = {}) {
return this._post(url, null, callback, opts, 'get');
}
// Clear all pending request locks
clearLocked() {
for (const awaitLocked of this.awaitLocked) {
clearTimeout(awaitLocked);
}
this.awaitLocked = [];
}
// Lock the service to prevent multiple simultaneous requests
lock() {
this.locked = true;
}
// Unlock the service to allow new requests
unlock() {
this.locked = false;
}
/**
* Handles HTTP errors.
* - Calls provided error callback and retries the request if needed.
*/
handleError(callback, retry) {
return (error) => {
return new Promise((resolve) => {
this.err_handle(error, callback, retry);
resolve();
});
};
}
/**
* Internal method to trigger error handling callbacks.
*/
err_handle(err, next, retry) {
if (typeof next === 'function') {
next(err);
}
for (const callback of this.errors) {
if (typeof callback === 'function') {
callback(err, retry);
}
}
}
// Placeholder method for handling request preparation (can be customized)
prepare_handle(url, body) { }
// Placeholder method for handling the response (can be customized)
response_handle(url, body, next) {
if (typeof next === 'function') {
next();
}
}
/**
* Retrieves a nested object or property from the response based on a dot-separated path.
*
* @param resp - The response object to retrieve data from.
* @param base - A dot-separated string indicating the path to the desired property within the response.
* - Example: `'data.items'` will navigate through `resp.data.items`.
* - If empty, the entire response is returned.
* @returns The object or property located at the specified path within the response.
*/
_getObjectToReplace(resp, base = '') {
if (base.includes('.')) {
const newBase = base.split('');
const currentBase = newBase.pop() || '';
return this._getObjectToReplace(resp[currentBase] || {}, newBase.join('.'));
}
else if (base) {
return resp[base];
}
else {
return resp;
}
}
/**
* Sets or replaces a nested object or property in the response based on a dot-separated path.
*
* @param resp - The response object to modify.
* @param base - A dot-separated string indicating the path to the property to replace.
* - Example: `'data.items'` will navigate through `resp.data.items`.
* @param doc - The new data or object to set at the specified path.
* @returns `void`.
*/
_setObjectToReplace(resp, base = '', doc) {
while (base.includes('.')) {
const newBase = base.split('');
const currentBase = newBase.pop() || '';
resp = resp[currentBase] || {};
base = newBase.join('.');
}
resp[base] = doc;
}
/**
* Creates a new object containing only specified fields from the input item.
*
* @param item - The input object to extract fields from.
* @param fields - An array of field names to include in the new object.
* - Example: `['id', 'name']` will create a new object with only the `id` and `name` properties from `item`.
* @returns A new object containing only the specified fields.
*/
_newDoc(item, fields) {
const newDoc = {};
for (const field of fields) {
newDoc[field] = item[field];
}
return newDoc;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HttpService, deps: [{ token: CONFIG_TOKEN, optional: true }, { token: i1$1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HttpService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: HttpService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [CONFIG_TOKEN]
}, {
type: Optional
}] }, { type: i1$1.HttpClient }] });
// network.service.ts — Angular 20+ (zoneless) signal-based connectivity checker
class NetworkService {
/**
* Creates the network monitor, binds browser/Capacitor events,
* performs an immediate check, and starts periodic polling.
*/
constructor(config) {
/** Internal mutable signals. */
this._status = signal(navigator.onLine ? 'poor' : 'none', ...(ngDevMode ? [{ debugName: "_status" }] : []));
this._latencyMs = signal(null, ...(ngDevMode ? [{ debugName: "_latencyMs" }] : []));
this._isOnline = signal(navigator.onLine, ...(ngDevMode ? [{ debugName: "_isOnline" }] : []));
/** Public read-only signals. */
this.status = this._status.asReadonly();
this.latencyMs = this._latencyMs.asReadonly();
this.isOnline = this._isOnline.asReadonly();
/** Failure counter to decide "none". */
this.fails = 0;
this._emitterService = inject(EmitterService);
this._config = {
...DEFAULT_NETWORK_CONFIG,
...(config.network || {}),
};
this._bindEvents();
this.recheckNow(); // fire once on start
window.setInterval(() => this.recheckNow(), this._config.intervalMs);
}
/**
* Manually trigger a connectivity check.
* - Measures latency against the first reachable endpoint.
* - Updates `isOnline`, `latencyMs`, and `status` accordingly.
*/
async recheckNow() {
const res = await this._pingAny();
if (res.ok && res.latency != null) {
this.fails = 0;
this._latencyMs.set(res.latency);
this._isOnline.set(true);
}
else {
this.fails++;
this._latencyMs.set(null);
// `isOnline` may still be true per OS; we let online/offline events keep it in sync.
}
this._updateClassification();
}
// ─────────────────────────── Internals ───────────────────────────
/**
* Classifies current state into 'good' | 'poor' | 'none'.
* - 'none' if offline or too many consecutive failures.
* - 'good' if latency ≤ goodLatencyMs.
* - otherwise 'poor'.
*/
_updateClassification() {
if (!this._isOnline() ||
this.fails >= this._config.maxConsecutiveFails) {
if (this._status() !== 'none') {
this._status.set('none');
this._emitterService.emit('wacom_offline');
}
return;
}
const l = this._latencyMs();
if (l != null && l <= this._config.goodLatencyMs) {
if (this._status() !== 'good') {
this._status.set('good');
this._emitterService.emit('wacom_online');
}
}
else if (this._status() !== 'poor') {
this._status.set('poor');
this._emitterService.emit('wacom_online');
}
}
/**
* Binds browser events that can affect connectivity:
* - online/offline (OS connectivity)
* - visibilitychange (recheck on focus)
* - NetworkInformation 'change' (if supported)
*/
_bindEvents() {
window.addEventListener('online', () => {
this._isOnline.set(true);
this.recheckNow();
});
window.addEventListener('offline', () => {
this._isOnline.set(false);
this._updateClassification();
});
navigator.connection?.addEventListener?.('change', () => this.recheckNow());
}
/**
* Tries endpoints in order until one responds (CORS or opaque).
* Returns success with measured latency, or a failure result.
*/
async _pingAny() {
for (const url of this._config.endpoints) {
const noCors = !url.includes('api.webart.work'); // treat public fallbacks as opaque checks
const r = await this._measure(url, this._config.timeoutMs, noCors).catch(() => null);
if (r?.ok)
return r;
}
return { ok: false, latency: null };
}
/**
* Measures a single fetch:
* - Appends a timestamp to bypass caches.
* - Uses `no-store` to avoid intermediaries caching.
* - When `noCors` is true, uses `mode:'no-cors'` and treats a resolved fetch as reachable.
*/
async _measure(url, timeoutMs, noCors = false) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
const t0 = performance.now();
try {
const res = await fetch(url + (url.includes('?') ? '&' : '?') + 't=' + Date.now(), {
method: 'GET',
cache: 'no-store',
credentials: 'omit',
signal: ctrl.signal,
mode: noCors ? 'no-cors' : 'cors',
});
clearTimeout(timer);
const latency = Math.round(performance.now() - t0);
// In no-cors, Response is opaque; treat as success if the fetch resolved.
const ok = noCors ? true : res.ok;
return { ok, latency };
}
catch {
clearTimeout(timer);
return { ok: false, latency: null };
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NetworkService, deps: [{ token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NetworkService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: NetworkService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [CONFIG_TOKEN]
}, {
type: Optional
}] }] });
class StoreService {
constructor(config) {
this._prefix = '';
this._config = {
...DEFAULT_CONFIG,
...(config?.store || {}),
};
}
/**
* Sets the prefix for storage keys.
*
* @param prefix - The prefix to set.
*/
setPrefix(prefix) {
this._prefix = prefix;
}
/**
* Sets a value in storage asynchronously.
*
* @param key - The storage key.
* @param value - The value to store.
* @returns A promise that resolves to a boolean indicating success.
*/
async set(key, value, callback = () => { }, errCallback = () => { }) {
key = this._applyPrefix(key);
try {
if (this._config.set) {
await this._config.set(key, value, callback, errCallback);
}
else {
localStorage.setItem(key, value);
callback();
}
return true;
}
catch (err) {
console.error(err);
errCallback(err);
return false;
}
}
/**
* Gets a value from storage asynchronously.
*
* @param key - The storage key.
* @returns A promise that resolves to the retrieved value or `null` if the key is missing.
*/
async get(key, callback, errCallback = () => { }) {
key = this._applyPrefix(key);
try {
if (this._config.get) {
const value = await this._config.get(key, (val) => {
callback?.(val ?? null);
}, errCallback);
return value ?? null;
}
else {
const value = localStorage.getItem(key);
callback?.(value ?? null);
return value ?? null;
}
}
catch (err) {
console.error(err);
errCallback(err);
return null;
}
}
/**
* Sets a JSON value in storage asynchronously.
*
* @param key - The storage key.
* @param value - The value to store.
* @returns A promise that resolves to a boolean indicating success.
*/
async setJson(key, value, callback = () => { }, errCallback = () => { }) {
return await this.set(key, JSON.stringify(value), callback, errCallback);
}
/**
* Gets a JSON value from storage asynchronously.
*
* @param key - The storage key.
* @returns A promise that resolves to the retrieved value.
*/
async getJson(key, callback, errCallback = () => { }) {
const value = await this.get(key);
if (value === null) {
return null;
}
try {
const result = JSON.parse(value);
callback?.(result);
return result;
}
catch (err) {
errCallback?.(err);
console.error(err);
return null;
}
}
/**
* Removes a value from storage.
*
* @param key - The storage key.
* @param callback - The callback to execute on success.
* @param errCallback - The callback to execute on error.
* @returns A promise that resolves to a boolean indicating success.
*/
async remove(key, callback = () => { }, errCallback = () => { }) {
key = this._applyPrefix(key);
try {
if (this._config.remove) {
return await this._config.remove(key, callback, errCallback);
}
else {
localStorage.removeItem(key);
callback();
return true;
}
}
catch (err) {
console.error(err);
errCallback(err);
return false;
}
}
/**
* Clears all values from storage.
*
* @param callback - The callback to execute on success.
* @param errCallback - The callback to execute on error.
* @returns A promise that resolves to a boolean indicating success.
*/
async clear(callback, errCallback) {
try {
if (this._config.clear) {
await this._config.clear();
}
else {
localStorage.clear();
}
callback?.();
return true;
}
catch (err) {
console.error(err);
errCallback?.(err);
return false;
}
}
/**
* Applies the configured prefix to a storage key.
*
* @param key - The storage key.
* @returns The prefixed storage key.
*/
_applyPrefix(key) {
if (this._config.prefix) {
key = this._config.prefix + key;
}
if (this._prefix) {
key = this._prefix + key;
}
return key;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StoreService, deps: [{ token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StoreService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: StoreService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [CONFIG_TOKEN]
}, {
type: Optional
}] }] });
/**
* Abstract class representing a CRUD (Create, Read, Update, Delete) service.
*
* This class provides methods for managing documents, interacting with an API,
* and storing/retrieving data from local storage. It is designed to be extended
* for specific document types.
*
* @template Document - The type of the document the service handles.
*/
class CrudService {
constructor(_config) {
this._config = _config;
/**
* Base URL for the API collection associated with this service.
*/
this._url = '/api/';
/**
* In-memory cache with all documents currently known by the service.
*/
this._docs = [];
/**
* Number of documents per page for paginated `get()` calls.
*/
this._perPage = 20;
/**
* Registered callbacks that recompute filtered document views.
*/
this._filteredDocumentsCallbacks = [];
/**
* HTTP client wrapper used for API communication.
*/
this.__httpService = inject(HttpService);
/**
* Key–value storage service used to persist documents locally.
*/
this.__storeService = inject(StoreService);
/**
* Core helper service with utility methods (copy, debounce, toSignal, etc.).
*/
this.__coreService = inject(CoreService);
/**
* Global event bus for cross-service communication.
*/
this.__emitterService = inject(EmitterService);
/**
* Network status service used to queue work while offline.
*/
this.__networkService = inject(NetworkService);
/**
* Cache of per-document signals indexed by document _id.
* Prevents creating multiple signals for the same document.
*/
this._signal = {};
/**
* Cache of per (field,value) lists of document signals.
* Key format: `${field}_${JSON.stringify(value)}`.
*/
this._signals = {};
/**
* Cache of per-field maps: fieldValue -> array of document signals.
*/
this._fieldSignals = {};
/**
* Track pending fetch-by-id requests to avoid duplicate calls.
*/
this._fetchingId = {};
/**
* Queue of operations that must be retried when network comes back online.
*/
this._onOnline = [];
/**
* Local counter used to build unique local identifiers together with Date.now().
*/
this._randomCount = 0;
this._config.signalFields = this._config.signalFields || {};
this._url += this._config.name;
this.loaded = this.__emitterService.onComplete(this._config.name + '_loaded');
this.getted = this.__emitterService.onComplete(this._config.name + '_getted');
if (this._config.unauthorized) {
this.restoreDocs();
}
else if (localStorage.getItem('waw_user')) {
const user = JSON.parse(localStorage.getItem('waw_user'));
if (user._id ===
localStorage.getItem(this._config.name + 'waw_user_id')) {
this.restoreDocs();
}
}
this.__emitterService.on('wipe').subscribe(() => {
this.clearDocs();
this._filterDocuments();
});
this.__emitterService.on('wacom_online').subscribe(() => {
for (const callback of this._onOnline) {
callback();
}
this._onOnline.length = 0;
});
}
/**
* Returns a WritableSignal for a document by _id, creating it if absent.
* Caches the signal to avoid redundant instances and initializes it
* with the current snapshot of the document.
* Work very carefully with this and localId, better avoid such flows.
*
* @param _id - Document identifier or a document instance.
*/
getSignal(_id) {
if (typeof _id !== 'string') {
_id = this._id(_id);
}
// Reuse existing signal if present
if (this._signal[_id]) {
return this._signal[_id];
}
// Always base the signal on the current canonical doc()
const doc = this.doc(_id);
this._signal[_id] = this.__coreService.toSignal(doc, this._config.signalFields);
return this._signal[_id];
}
/**
* Returns a signal with an array of document signals that match
* a given field/value pair.
*
* Example:
* const activitiesSig = service.getSignals('userId', currentUserId);
*/
getSignals(field, value) {
const id = field + '_' + JSON.stringify(value);
if (!this._signals[id]) {
this._signals[id] = signal(this._getSignals(id));
}
return this._signals[id];
}
/**
* Builds the array of document signals for a given (field,value) key.
* Only documents with a real _id are included.
*/
_getSignals(id) {
const sep = id.indexOf('_');
if (sep === -1) {
return [];
}
const field = id.slice(0, sep);
const valueJson = id.slice(sep + 1);
const list = [];
for (const doc of this.getDocs()) {
if (JSON.stringify(doc[field]) !== valueJson) {
continue;
}
const docId = this._id(doc);
if (!docId)
continue;
list.push(this.getSignal(docId));
}
return list;
}
/**
* Returns a signal with a map: fieldValue -> array of document signals.
*
* Example:
* const byStatusSig = service.getFieldSignals('status');
* byStatusSig() might be { active: [sig1, sig2], draft: [sig3] }.
*/
getFieldSignals(field) {
if (!this._fieldSignals[field]) {
this._fieldSignals[field] = signal(this._getFieldSignals(field));
}
return this._fieldSignals[field];
}
/**
* Builds the map for a given field.
* Only documents with a real _id are included.
*/
_getFieldSignals(field) {
const byFields = {};
for (const doc of this.getDocs()) {
const docId = this._id(doc);
if (!docId)
continue;
const value = String(doc[field]);
if (!byFields[value]) {
byFields[value] = [];
}
byFields[value].push(this.getSignal(docId));
}
return byFields;
}
/**
* Clears cached document signals except those explicitly preserved.
* Useful when changing routes or contexts to reduce memory.
*
* @param exceptIds - List of ids whose signals should be kept.
*/
removeSignals(exceptIds = []) {
for (const _id in this._signal) {
if (!exceptIds.includes(_id)) {
delete this._signal[_id];
}
}
}
/**
* Restores documents from local storage (if present) and syncs
* all existing signals with the restored data.
*/
async restoreDocs() {
const docs = await this.__storeService.getJson('docs_' + this._config.name);
if (docs?.length) {
this._docs.length = 0;
this._docs.push(...docs);
this._filterDocuments();
for (const doc of this._docs) {
if (doc.__deleted) {
this.delete(doc, doc.__options?.['delete'] || {});
}
else if (!doc._id) {
this.create(doc, doc.__options?.['create'] || {});
}
else if (doc.__modified?.length) {
for (const id of doc.__modified) {
if (id.startsWith('up')) {
this.update(doc, doc.__options?.[id] || {});
}
else {
this.unique(doc, doc.__options?.[id] || {});
}
}
}
}
this.__emitterService.complete(this._config.name + '_loaded', this._docs);
}
}
/**
* Saves the current set of documents to local storage.
*/
setDocs() {
this.__storeService.setJson('docs_' + this._config.name, this._docs);
}
/**
* Retrieves the current list of documents.
*
* @returns The list of documents.
*/
getDocs(filter = () => true) {
return this._docs.filter(filter);
}
/**
* Retrieves the first document that matches the given predicate.
*
* @param find - Predicate used to locate a specific document.
*/
getDoc(find) {
return this._docs.find(find);
}
/**
* Clears the current list of documents, persists the empty state
* and recomputes all derived signals.
*
* Empties the internal documents array and saves the updated state to local storage.
*/
clearDocs() {
this._docs.splice(0, this._docs.length);
this.setDocs();
this._updateSignals();
}
/**
* Adds multiple documents to the service and saves them to local storage.
*
* @param docs - An array of documents to add.
*/
addDocs(docs) {
if (Array.isArray(docs)) {
for (const doc of docs) {
this.addDoc(doc);
}
}
}
/**
* Adds a single document to the service. If it already exists, it will be updated.
*
* @param doc - The document to add.
*/
addDoc(doc) {
if (this._config.replace) {
this._config.replace(doc);
}
const existingDoc = this._docs.find((d) => (this._id(doc) && this._id(d) === this._id(doc)) ||
(doc._localId && d._localId === doc._localId));
if (existingDoc) {
this.__coreService.copy(doc, existingDoc);
this.__coreService.copy(existingDoc, doc);
this._syncSignalForDoc(existingDoc);
}
else {
this._docs.push(doc);
this._syncSignalForDoc(doc);
}
this.setDocs();
}
/**
* Creates a new document with a temporary ID and status flags.
*
* @param doc - Optional base document to use for the new document.
* @returns A new document instance with default properties.
*/
new(doc = {}) {
return {
...doc,
_id: undefined,
_localId: this._localId(),
__created: false,
__modified: false,
};
}
/**
* Retrieves a document by its unique ID or creates a new one if it doesn't exist.
*
* @param _id - The document ID to search for.
* @returns The found document or a new document if not found.
*/
doc(_id) {
// If we already have a signal for this id, use its current value
if (this._signal[_id]) {
return this._signal[_id]();
}
let doc = this._docs.find((d) => this._id(d) === _id ||
(d._localId && d._localId === Number(_id))) || null;
// If doc not found, create + push into _docs so it is not detached
if (!doc) {
doc = this.new({ _id });
this._docs.push(doc);
this.setDocs();
}
if (!this._docs.find((d) => this._id(d) === _id) &&
!this._fetchingId[_id]) {
this._fetchingId[_id] = true;
setTimeout(() => {
this.fetch({ _id }).subscribe((_doc) => {
this._fetchingId[_id] = false;
if (_doc) {
this.__coreService.copy(_doc, doc);
this._syncSignalForDoc(doc);
}
});
});
}
return doc;
}
/**
* Sets the number of documents to display per page.
*
* @param _perPage - Number of documents per page.
*/
setPerPage(_perPage) {
this._perPage = _perPage;
}
/**
* Fetches a list of documents from the API with optional pagination.
*
* @param config - Optional pagination configuration.
* @param options - Optional callback and error handling configuration.
* @returns An observable that resolves with the list of documents.
*/
get(config = {}, options = {}) {
if (!this.__networkService.isOnline()) {
return new Observable((observer) => {
this._onOnline.push(() => {
this.get(config, options).subscribe(observer);
});
});
}
if (!this._config.unauthorized && localStorage.getItem('waw_user')) {
const user = JSON.parse(localStorage.getItem('waw_user'));
localStorage.setItem(this._config.name + 'waw_user_id', user._id);
}
const url = `${this._url}/get${options.name || ''}`;
const params = (typeof config.page === 'number' || config.query ? '?' : '') +
(config.query || '') +
(typeof config.page === 'number'
? `&skip=${this._perPage * (config.page - 1)}&limit=${this._perPage}`
: '');
const obs = this.__httpService.get(`${url}${params}`);
obs.subscribe({
next: (resp) => {
resp = resp || [];
if (typeof config.page !== 'number') {
this.clearDocs();
}
resp.forEach((doc) => this.addDoc(doc));
if (options.callback) {
options.callback(resp);
}
if (typeof config.page !== 'number') {
this._filterDocuments();
this.__emitterService.complete(this._config.name + '_getted', this._docs);
}
this.__emitterService.emit(`${this._config.name}_get`, this._docs);
this.__emitterService.emit(`${this._config.name}_changed`, this._docs);
},
error: (err) => {
if (options.errCallback) {
options.errCallback(err);
}
},
});
return obs;
}
/**
* Sends a request to the API to create a new document.
*
* @param doc - The document to create.
* @param options - Optional callback and error handling configuration.
* @returns An observable that resolves with the created document, or emits an error if already created.
*/
create(doc = {}, options = {}) {
if (doc._id) {
return this.update(doc, options);
}
doc._localId ||= this._localId();
doc.__options ||= {};
doc.__options['create'] = options;
this.addDoc(doc);
this._filterDocuments();
if (!this.__networkService.isOnline()) {
return new Observable((observer) => {
this._onOnline.push(() => {
this.create(doc, options).subscribe(observer);
});
});
}
if (doc.__creating) {
// Emit an error observable if the document is already created
return new Observable((observer) => {
observer.error(new Error('Document is currently already creating.'));
});
}
if (this._config.appId) {
doc.appId = this._config.appId;
}
doc.__creating = true;
const obs = this.__httpService.post(`${this._url}/create${options.name || ''}`, doc);
obs.subscribe({
next: (resp) => {
if (resp) {
this.__coreService.copy(resp, doc);
this.addDoc(doc);
this._filterDocuments();
if (options.callback) {
options.callback(doc);
}
}
else {
doc.__creating = false;
if (options.errCallback) {
options.errCallback(resp);
}
}
this.__emitterService.emit(`${this._config.name}_create`, doc);
this.__emitterService.emit(`${this._config.name}_list`, doc);
this.__emitterService.emit(`${this._config.name}_changed`, doc);
},
error: (err) => {
doc.__creating = false;
if (options.errCallback)
options.errCallback(err);
},
});
return obs;
}
/**
* Fetches a document from the API based on a query.
*
* @param query - The query object used to filter documents.
* @param options - Optional callback and error handling configuration.
* @returns An observable that resolves with the fetched document.
*/
fetch(query = {}, options = {}) {
if (!this.__networkService.isOnline()) {
return new Observable((observer) => {
this._onOnline.push(() => {
this.fetch(query, options).subscribe(observer);
});
});
}
const obs = this.__httpService.post(`${this._url}/fetch${options.name || ''}`, query);
obs.subscribe({
next: (doc) => {
if (doc) {
this.addDoc(doc);
this._filterDocuments();
if (options.callback)
options.callback(doc);
this.__emitterService.emit(`${this._config.name}_changed`, doc);
}
else {
if (options.errCallback) {
options.errCallback(doc);
}
}
},
error: (err) => {
if (options.errCallback) {
options.errCallback(err);
}
},
});
return obs;
}
/**
* Updates a document after a specified delay and returns an observable.
*
* @param doc - The document to update.
* @param options - Optional callback and error handling configuration.
* @returns An observable that emits the updated document.
*/
updateAfterWhile(doc, options = {}) {
return new Observable((observer) => {
this.__coreService.afterWhile(this._id(doc), () => {
this.update(doc, options).subscribe({
next: (updatedDoc) => {
observer.next(updatedDoc); // Emit the updated document
},
error: (err) => {
observer.error(err); // Forward the error
},
complete: () => {
observer.complete(); // Complete the observable
},
});
});
});
}
/**
* Updates a document in the API.
*
* @param doc - The document to update.
* @param options - Optional callback and error handling configuration.
* @returns An observable that resolves with the updated document.
*/
update(doc, options = {}) {
this._updateModified(doc, 'up' + (options.name || ''), options);
if (!this.__networkService.isOnline()) {
return new Observable((observer) => {
this._onOnline.push(() => {
this.update(doc, options).subscribe(observer);
});
});
}
const obs = this.__httpService.post(`${this._url}/update${options.name || ''}`, doc);
obs.subscribe({
next: (resp) => {
if (resp) {
this._removeModified(doc, 'up' + (options.name || ''));
const storedDoc = this.doc(doc._id);
this.__coreService.copy(resp, storedDoc);
this.__coreService.copy(resp, doc);
this._syncSignalForDoc(storedDoc);
if (options.callback) {
options.callback(doc);
}
}
else {
if (options.errCallback) {
options.errCallback(resp);
}
}
this.__emitterService.emit(`${this._config.name}_update`, doc);
this.__emitterService.emit(`${this._config.name}_changed`, doc);
},
error: (err) => {
if (options.errCallback) {
options.errCallback(err);
}
},
});
return obs;
}
/**
* Unique update a document field in the API.
*
* @param doc - The document to update.
* @param options - Optional callback and error handling configuration.
* @returns An observable that resolves with the updated document.
*/
unique(doc, options = {}) {
this._updateModified(doc, 'un' + (options.name || ''), options);
if (!this.__networkService.isOnline()) {
return new Observable((observer) => {
this._onOnline.push(() => {
this.unique(doc, options).subscribe(observer);
});
});
}
const obs = this.__httpService.post(`${this._url}/unique${options.name || ''}`, doc);
obs.subscribe({
next: (resp) => {
if (resp) {
this._removeModified(doc, 'un' + (options.name || ''));
doc[options.name] = resp;
this._syncSignalForDoc(doc);
if (options.callback) {
options.callback(doc);
}
}
else {
if (options.errCallback) {
options.errCallback(resp);
}
}
this.__emitterService.emit(`${this._config.name}_unique`, doc);
this.__emitterService.emit(`${this._config.name}_changed`, doc);
},
error: (err) => {
if (options.errCallback) {
options.errCallback(err);
}
},
});
return obs;
}
/**
* Deletes a document from the API.
*
* @param doc - The document to delete.
* @param options - Optional callback and error handling configuration.
* @returns An observable that resolves with the deleted document.
*/
delete(doc, options = {}) {
doc.__deleted = true;
doc.__options ||= {};
doc.__options['delete'] = options;
this.addDoc(doc);
this._filterDocuments();
if (!this.__networkService.isOnline()) {
return new Observable((observer) => {
this._onOnline.push(() => {
this.delete(doc, options).subscribe(observer);
});
});
}
const obs = this.__httpService.post(`${this._url}/delete${options.name || ''}`, doc);
obs.subscribe({
next: (resp) => {
if (resp) {
const idx = this._docs.findIndex((d) => this._id(d) === this._id(doc));
if (idx !== -1) {
this._docs.splice(idx, 1);
}
this.setDocs();
// We keep signal but mark it deleted and recompute mappings.
this._syncSignalForDoc({
...doc,
__deleted: true,
});
this._filterDocuments();
if (options.callback) {
options.callback(doc);
}
}
else {
if (options.errCallback) {
options.errCallback(resp);
}
}
this.__emitterService.emit(`${this._config.name}_delete`, doc);
this.__emitterService.emit(`${this._config.name}_changed`, doc);
},
error: (err) => {
if (options.errCallback) {
options.errCallback(err);
}
},
});
return obs;
}
/**
* Registers a filtered view of documents and returns the recompute callback.
*
* The callback is called automatically whenever `_filterDocuments()` runs.
*/
filteredDocuments(storeObjectOrArray, config = {}) {
const callback = () => {
if (Array.isArray(storeObjectOrArray)) {
let result = this._docs
.filter((doc) => !doc.__deleted)
.filter(config.valid ?? (() => true));
storeObjectOrArray.length = 0;
if (typeof config.sort === 'function') {
result = result.sort(config.sort);
}
storeObjectOrArray.push(...result);
}
else {
const storeObject = storeObjectOrArray;
/* remove docs if they were removed */
for (const parentId in storeObject) {
for (let i = storeObject[parentId].length - 1; i >= 0; i--) {
const _field = typeof config.field === 'function'
? config.field(storeObject[parentId][i])
: config.field || 'author';
const _doc = storeObject[parentId][i];
if (!this._docs.find((doc) => Array.isArray(doc[_field])
? doc[_field].includes(_doc[this._id(doc)])
: doc[_field] === _doc[this._id(doc)])) {
storeObject[parentId].splice(i, 1);
}
}
}
/* add docs if they are not added */
for (const doc of this._docs) {
if (doc.__deleted)
continue;
const _field = typeof config.field === 'function'
? config.field(doc)
: config.field || 'author';
if (typeof config.valid === 'function'
? !config.valid(doc)
: Array.isArray(doc[_field])
? !doc[_field]?.length
: !doc[_field]) {
continue;
}
if (typeof config.field === 'function') {
if (config.field(doc) &&
!storeObject[doc[_field]].find((c) => c._id === doc._id)) {
storeObject[doc[_field]].push(doc);
}
}
else if (Array.isArray(doc[_field])) {
doc[_field].forEach((_field) => {
storeObject[_field] = storeObject[_field] || [];
if (!storeObject[_field].find((c) => c._id === doc._id)) {
storeObject[_field].push(doc);
}
});
}
else {
storeObject[doc[_field]] =
storeObject[doc[_field]] || [];
if (!storeObject[doc[_field]].find((c) => c._id === doc._id)) {
storeObject[doc[_field]].push(doc);
}
}
}
/* sort the array's */
if (typeof config.sort === 'function') {
for (const parentId in storeObject) {
storeObject[parentId].sort(config.sort);
}
}
}
config.filtered?.(storeObjectOrArray);
};
this._filteredDocumentsCallbacks.push(callback);
return callback;
}
/**
* Generates a unique ID for a document when using local-only identifiers.
*
* @returns The unique ID as a number.
*/
_localId() {
return Number(Date.now() + '' + this._randomCount++);
}
/**
* Returns the configured identity field for the given document as string.
*
* @param doc - The document for which to generate the ID.
* @returns The unique ID as a string.
*/
_id(doc) {
return doc[this._config._id || '_id']?.toString();
}
/**
* Executes all registered filter document callbacks and emits a
* `<name>_filtered` event.
*/
_filterDocuments() {
for (const callback of this._filteredDocumentsCallbacks) {
callback();
}
this.__emitterService.emit(`${this._config.name}_filtered`);
}
/**
* Marks a document as modified for a given operation id and
* keeps the document in the store until the operation is confirmed.
*/
_updateModified(doc, id, options) {
doc.__modified ||= [];
doc.__options ||= {};
doc.__options[id] = options;
if (!doc.__modified.find((m) => m === id)) {
doc.__modified.push(id);
this.addDoc(doc);
}
}
/**
* Removes a modification mark from the document once the
* server operation is confirmed.
*/
_removeModified(doc, id) {
doc.__modified ||= [];
if (doc.__modified.find((m) => m === id)) {
doc.__modified.splice(doc.__modified.findIndex((m) => m === id), 1);
this.addDoc(doc);
}
}
/**
* Syncs a single document's signal (if exists) and refreshes all
* derived collections (field/value lists and field maps).
*/
_syncSignalForDoc(doc) {
const id = this._id(doc);
if (id && this._signal[id]) {
this._signal[id].set(doc);
}
this._updateSignals();
}
/**
* Rebuilds all derived signal collections:
* - all per (field,value) lists of document signals
* - all per-field maps value -> [signals]
*
* This keeps `getSignals()` and `getFieldSignals()` in sync after
* any mutation that touches `_docs`.
*/
_updateSignals() {
// refresh all (field,value) collections
for (const key in this._signals) {
this._signals[key].set(this._getSignals(key));
}
// refresh all per-field maps
for (const field in this._fieldSignals) {
this._fieldSignals[field].set(this._getFieldSignals(field));
}
}
}
/**
* Utility service for programmatically creating and interacting with Angular
* components within the DOM.
*/
class DomService {
constructor() {
/** Reference to the root application used for view attachment. */
this._appRef = inject(ApplicationRef);
/** Injector utilized when creating dynamic components. */
this._injector = inject(EnvironmentInjector);
/**
* Flags to ensure components with a specific `providedIn` key are only
* instantiated once at a time.
*/
this._providedIn = {};
}
/**
* Appends a component to a specified element by ID.
*
* @param component - The component to append.
* @param options - The options to project into the component.
* @param id - The ID of the element to append the component to.
* @returns An object containing the native element and the component reference.
*/
appendById(component, options = {}, id) {
const componentRef = createComponent(component, {
environmentInjector: this._injector,
});
this.projectComponentInputs(componentRef, options);
this._appRef.attachView(componentRef.hostView);
const domElem = componentRef.hostView
.rootNodes[0];
const element = document.getElementById(id);
if (element && typeof element.appendChild === 'function') {
element.appendChild(domElem);
}
componentRef.changeDetectorRef.detectChanges();
return {
nativeElement: domElem,
componentRef: componentRef,
remove: () => this.removeComponent(componentRef),
};
}
/**
* Appends a component to a specified element or to the body.
*
* @param component - The component to append.
* @param options - The options to project into the component.
* @param element - The element to append the component to. Defaults to body.
* @returns An object containing the native element and the component reference.
*/
appendComponent(component, options = {}, element = document.body) {
if (options.providedIn) {
if (this._providedIn[options.providedIn]) {
return;
}
this._providedIn[options.providedIn] = true;
}
const componentRef = createComponent(component, {
environmentInjector: this._injector,
});
this.projectComponentInputs(componentRef, options);
this._appRef.attachView(componentRef.hostView);
const domElem = componentRef.hostView
.rootNodes[0];
if (element && typeof element.appendChild === 'function') {
element.appendChild(domElem);
}
componentRef.changeDetectorRef.detectChanges();
return {
nativeElement: domElem,
componentRef: componentRef,
remove: () => this.removeComponent(componentRef, options.providedIn),
};
}
/**
* Gets a reference to a dynamically created component.
*
* @param component - The component to create.
* @param options - The options to project into the component.
* @returns The component reference.
*/
getComponentRef(component, options = {}) {
const componentRef = createComponent(component, {
environmentInjector: this._injector,
});
this.projectComponentInputs(componentRef, options);
this._appRef.attachView(componentRef.hostView);
componentRef.changeDetectorRef.detectChanges();
return componentRef;
}
/**
* Projects the inputs onto the component.
*
* @param component - The component reference.
* @param options - The options to project into the component.
* @returns The component reference with the projected inputs.
*/
projectComponentInputs(component, options) {
if (options) {
const props = Object.getOwnPropertyNames(options);
for (const prop of props) {
component.instance[prop] = options[prop];
}
}
return component;
}
/**
* Removes a previously attached component and optionally clears its
* unique `providedIn` flag.
*
* @param componentRef - Reference to the component to be removed.
* @param providedIn - Optional key used to track unique instances.
*/
removeComponent(componentRef, providedIn) {
this._appRef.detachView(componentRef.hostView);
componentRef.destroy();
if (providedIn) {
delete this._providedIn[providedIn];
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DomService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DomService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: DomService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
/**
* RtcService handles WebRTC peer connections and local media stream setup.
* It provides functionality to initialize the user's camera/microphone,
* manage multiple peer connections, and handle offer/answer negotiation.
*/
class RtcService {
constructor() {
/**
* Map of peer connections, keyed by peer ID.
*/
this.peers = new Map();
/**
* Local media stream from user's camera and microphone.
*/
this.localStream = null;
}
/**
* Initializes the local media stream (audio/video).
* Requests permissions and stores the stream internally.
*/
async initLocalStream() {
if (!this.localStream) {
this.localStream = await navigator.mediaDevices.getUserMedia({
video: true,
audio: true,
});
}
return this.localStream;
}
/**
* Creates a new RTCPeerConnection for the given ID and attaches local tracks.
*/
async createPeer(id) {
const peer = new RTCPeerConnection();
this.localStream
?.getTracks()
.forEach((track) => peer.addTrack(track, this.localStream));
this.peers.set(id, peer);
return peer;
}
/**
* Retrieves an existing peer connection by ID.
*/
getPeer(id) {
return this.peers.get(id);
}
/**
* Creates an SDP offer for the specified peer and sets it as the local description.
*/
async createOffer(id) {
const peer = this.peers.get(id);
if (!peer)
throw new Error('Peer not found');
const offer = await peer.createOffer();
await peer.setLocalDescription(offer);
return offer;
}
/**
* Accepts an SDP offer, creates an answer, and sets it as the local description.
*/
async createAnswer(id, offer) {
const peer = this.peers.get(id);
if (!peer)
throw new Error('Peer not found');
await peer.setRemoteDescription(new RTCSessionDescription(offer));
const answer = await peer.createAnswer();
await peer.setLocalDescription(answer);
return answer;
}
/**
* Sets the remote description with an SDP answer for the given peer.
*/
async setRemoteAnswer(id, answer) {
const peer = this.peers.get(id);
if (!peer)
throw new Error('Peer not found');
await peer.setRemoteDescription(new RTCSessionDescription(answer));
}
/**
* Adds an ICE candidate to the specified peer connection.
*/
addIceCandidate(id, candidate) {
const peer = this.peers.get(id);
if (peer)
peer.addIceCandidate(new RTCIceCandidate(candidate));
}
/**
* Returns the initialized local media stream.
*/
getLocalStream() {
return this.localStream;
}
/**
* Closes a specific peer connection and removes it from the map.
*/
closePeer(id) {
const peer = this.peers.get(id);
if (peer) {
peer.close();
this.peers.delete(id);
}
}
/**
* Closes all peer connections and stops the local media stream.
*/
closeAll() {
this.peers.forEach((peer) => peer.close());
this.peers.clear();
this.localStream?.getTracks().forEach((track) => track.stop());
this.localStream = null;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RtcService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RtcService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: RtcService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}] });
class SocketService {
constructor(_config) {
this._config = _config;
this._url = '';
this._connected = false;
this._opts = {};
this._emitterService = inject(EmitterService);
this._config = { ...DEFAULT_CONFIG, ...(this._config || {}) };
if (!this._config.io) {
return;
}
const url = new URL(window.location.origin);
if (typeof this._config.socket === 'object') {
if (this._config.socket.port) {
url.port = this._config.socket.port;
}
if (this._config.socket.opts) {
this._opts = this._config.socket.opts;
}
this._url = this._config.socket.url ?? url.origin;
}
else {
this._url = url.origin;
}
if (this._config.socket) {
this.load();
}
}
/**
* Sets the URL for the WebSocket connection and reloads the socket.
* @param url - The URL of the WebSocket server.
*/
setUrl(url) {
this._url = url;
if (!this._config.socket) {
this._config.socket = true;
}
this.load();
}
/**
* Loads and initializes the WebSocket connection.
*/
load() {
if (this._config.io) {
const ioFunc = this._config.io.default
? this._config.io.default
: this._config.io;
this._io = ioFunc(this._url, this._opts);
this._io.on('connect', () => {
this._connected = true;
this._emitterService.complete('socket');
});
this._io.on('disconnect', (reason) => {
this._connected = false;
this._emitterService.emit('socket_disconnect', reason);
console.warn('Socket disconnected', reason);
});
this._io.on('error', (err) => {
this._connected = false;
this._emitterService.emit('socket_error', err);
console.warn('Socket error', err);
});
}
}
/**
* Disconnects the WebSocket connection and resets the connection state.
*/
disconnect() {
if (this._io) {
this._io.disconnect();
}
this._connected = false;
}
/**
* Subscribes to a WebSocket event.
* @param to - The event to subscribe to.
* @param cb - The callback function to execute when the event is received.
*/
on(to, cb = () => { }) {
if (!this._config.socket) {
return;
}
if (!this._io) {
console.warn('Socket client not loaded.');
return;
}
if (!this._connected) {
setTimeout(() => {
this.on(to, cb);
}, 100);
return;
}
this._io.on(to, cb);
}
/**
* Emits a message to a WebSocket event.
* @param to - The event to emit the message to.
* @param message - The message to emit.
* @param room - Optional room to emit the message to.
*/
emit(to, message, room = false) {
if (!this._config.socket) {
return;
}
if (!this._io) {
console.warn('Socket client not loaded.');
return;
}
if (!this._connected) {
setTimeout(() => {
this.emit(to, message, room);
}, 100);
return;
}
this._io.emit(to, message, room);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SocketService, deps: [{ token: CONFIG_TOKEN, optional: true }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SocketService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: SocketService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [CONFIG_TOKEN]
}, {
type: Optional
}] }] });
class TimeService {
constructor(datePipe) {
this.datePipe = datePipe;
this.weekDays = [
'Sunday',
'Monday',
'Tuesday',
'Wednesday',
'Thursday',
'Friday',
'Saturday',
];
this.monthNames = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
}
/**
* Returns the name of the day of the week for a given date.
*
* @param date - The date for which to get the day of the week.
* @param format - The format in which to return the day name. Default is 'long'.
* @returns The name of the day of the week.
*/
getDayName(date, format = 'long') {
const dayIndex = date.getDay();
return format === 'short'
? this.weekDays[dayIndex].substring(0, 3)
: this.weekDays[dayIndex];
}
/**
* Returns the name of the month for a given index.
*
* @param monthIndex - The month index (0-11).
* @param format - The format in which to return the month name. Default is 'long'.
* @returns The name of the month.
*/
getMonthName(monthIndex, format = 'long') {
if (!Number.isInteger(monthIndex) ||
monthIndex < 0 ||
monthIndex > 11) {
throw new RangeError('monthIndex must be an integer between 0 and 11');
}
return format === 'short'
? this.monthNames[monthIndex].substring(0, 3)
: this.monthNames[monthIndex];
}
/**
* Formats a date according to the specified format and timezone.
*
* @param date - The date to format.
* @param format - The format string (see Angular DatePipe documentation for format options).
* @param timezone - The timezone to use for formatting.
* @returns The formatted date string.
*/
formatDate(date, format = 'mediumDate', timezone = 'UTC') {
return this.datePipe.transform(date, format, timezone) || '';
}
/**
* Converts a date to a different timezone.
*
* @param date - The date to convert.
* @param timezone - The timezone to convert to.
* @returns The date in the new timezone.
*/
convertToTimezone(date, timezone) {
return new Date(date.toLocaleString('en-US', { timeZone: timezone }));
}
/**
* Returns the start of the day for a given date.
*
* @param date - The date for which to get the start of the day.
* @returns The start of the day (midnight) for the given date.
*/
startOfDay(date) {
const newDate = new Date(date);
newDate.setHours(0, 0, 0, 0);
return newDate;
}
/**
* Returns the end of the day for a given date.
*
* @param date - The date for which to get the end of the day.
* @returns The end of the day (one millisecond before midnight) for the given date.
*/
endOfDay(date) {
const newDate = new Date(date);
newDate.setHours(23, 59, 59, 999);
return newDate;
}
/**
* Returns the start of the week for a given date.
*
* @param date - The date for which to get the start of the week.
* @param locale - A BCP 47 language tag to determine the first day of the week. Defaults to the runtime locale.
* @returns The start of the week adjusted for the locale.
*
* @example
* const date = new Date('2024-05-15');
* service.startOfWeek(date); // => Monday May 13 2024 00:00:00 for en-GB
*/
startOfWeek(date, locale) {
const newDate = this.startOfDay(date);
const dtf = new Intl.DateTimeFormat(locale);
const resolved = dtf.resolvedOptions().locale;
const region = resolved.split('-')[1]?.toUpperCase();
const sundayFirst = ['US', 'CA', 'AU', 'NZ', 'PH', 'BR'];
const firstDay = sundayFirst.includes(region) ? 0 : 1;
const day = newDate.getDay();
const diff = (day - firstDay + 7) % 7;
newDate.setDate(newDate.getDate() - diff);
return newDate;
}
/**
* Returns the end of the week for a given date.
*
* @param date - The date for which to get the end of the week.
* @param locale - A BCP 47 language tag to determine the first day of the week. Defaults to the runtime locale.
* @returns The end of the week adjusted for the locale.
*
* @example
* const date = new Date('2024-05-15');
* service.endOfWeek(date); // => Sunday May 19 2024 23:59:59.999 for en-GB
*/
endOfWeek(date, locale) {
const start = this.startOfWeek(date, locale);
const end = this.addDays(start, 6);
return this.endOfDay(end);
}
/**
* Returns the start of the month for a given date.
*
* @param date - The date for which to get the start of the month.
* @returns The start of the month.
*
* @example
* const date = new Date('2024-05-15');
* service.startOfMonth(date); // => May 1 2024 00:00:00
*/
startOfMonth(date) {
const newDate = this.startOfDay(date);
newDate.setDate(1);
return newDate;
}
/**
* Returns the end of the month for a given date.
*
* @param date - The date for which to get the end of the month.
* @returns The end of the month.
*
* @example
* const date = new Date('2024-05-15');
* service.endOfMonth(date); // => May 31 2024 23:59:59.999
*/
endOfMonth(date) {
const start = this.startOfMonth(date);
const end = new Date(start);
end.setMonth(end.getMonth() + 1);
end.setDate(0);
return this.endOfDay(end);
}
/**
* Returns the start of the year for a given date.
*
* @param date - The date for which to get the start of the year.
* @returns The start of the year.
*
* @example
* const date = new Date('2024-05-15');
* service.startOfYear(date); // => Jan 1 2024 00:00:00
*/
startOfYear(date) {
const newDate = this.startOfDay(date);
newDate.setMonth(0, 1);
return newDate;
}
/**
* Returns the end of the year for a given date.
*
* @param date - The date for which to get the end of the year.
* @returns The end of the year.
*
* @example
* const date = new Date('2024-05-15');
* service.endOfYear(date); // => Dec 31 2024 23:59:59.999
*/
endOfYear(date) {
const end = new Date(date.getFullYear(), 11, 31);
return this.endOfDay(end);
}
/**
* Returns the number of days in a given month and year.
*
* @param month - The month (0-11).
* @param year - The year.
* @returns The number of days in the month.
*/
getDaysInMonth(month, year) {
return new Date(year, month + 1, 0).getDate();
}
/**
* Checks if a given year is a leap year.
*
* @param year - The year to check.
* @returns True if the year is a leap year, false otherwise.
*/
isLeapYear(year) {
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
}
/**
* Adds a specified number of days to a date.
*
* @param date - The date to which to add days.
* @param days - The number of days to add.
* @returns The new date with the added days.
*/
addDays(date, days) {
const newDate = new Date(date);
newDate.setDate(newDate.getDate() + days);
return newDate;
}
/**
* Adds a specified number of months to a date.
*
* @param date - The date to which to add months.
* @param months - The number of months to add.
* @returns The new date with the added months.
*/
addMonths(date, months) {
const newDate = new Date(date);
newDate.setMonth(newDate.getMonth() + months);
return newDate;
}
/**
* Adds a specified number of years to a date.
*
* @param date - The date to which to add years.
* @param years - The number of years to add.
* @returns The new date with the added years.
*/
addYears(date, years) {
const newDate = new Date(date);
newDate.setFullYear(newDate.getFullYear() + years);
return newDate;
}
/**
* Adds a specified number of hours to a date.
*
* @param date - The date to which to add hours.
* @param hours - The number of hours to add.
* @returns The new date with the added hours.
*/
addHours(date, hours) {
const newDate = new Date(date);
newDate.setHours(newDate.getHours() + hours);
return newDate;
}
/**
* Adds a specified number of minutes to a date.
*
* @param date - The date to which to add minutes.
* @param minutes - The number of minutes to add.
* @returns The new date with the added minutes.
*/
addMinutes(date, minutes) {
const newDate = new Date(date);
newDate.setMinutes(newDate.getMinutes() + minutes);
return newDate;
}
/**
* Adds a specified number of seconds to a date.
*
* @param date - The date to which to add seconds.
* @param seconds - The number of seconds to add.
* @returns The new date with the added seconds.
*/
addSeconds(date, seconds) {
const newDate = new Date(date);
newDate.setSeconds(newDate.getSeconds() + seconds);
return newDate;
}
/**
* Subtracts a specified number of days from a date.
*
* @param date - The date from which to subtract days.
* @param days - The number of days to subtract.
* @returns The new date with the subtracted days.
*/
subtractDays(date, days) {
return this.addDays(date, -days);
}
/**
* Subtracts a specified number of months from a date.
*
* @param date - The date from which to subtract months.
* @param months - The number of months to subtract.
* @returns The new date with the subtracted months.
*/
subtractMonths(date, months) {
return this.addMonths(date, -months);
}
/**
* Subtracts a specified number of years from a date.
*
* @param date - The date from which to subtract years.
* @param years - The number of years to subtract.
* @returns The new date with the subtracted years.
*/
subtractYears(date, years) {
return this.addYears(date, -years);
}
/**
* Subtracts a specified number of hours from a date.
*
* @param date - The date from which to subtract hours.
* @param hours - The number of hours to subtract.
* @returns The new date with the subtracted hours.
*/
subtractHours(date, hours) {
return this.addHours(date, -hours);
}
/**
* Subtracts a specified number of minutes from a date.
*
* @param date - The date from which to subtract minutes.
* @param minutes - The number of minutes to subtract.
* @returns The new date with the subtracted minutes.
*/
subtractMinutes(date, minutes) {
return this.addMinutes(date, -minutes);
}
/**
* Subtracts a specified number of seconds from a date.
*
* @param date - The date from which to subtract seconds.
* @param seconds - The number of seconds to subtract.
* @returns The new date with the subtracted seconds.
*/
subtractSeconds(date, seconds) {
return this.addSeconds(date, -seconds);
}
/**
* Calculates the difference in days between two dates.
*
* @param date1 - The earlier date.
* @param date2 - The later date.
* @returns The number of days between the two dates.
*/
differenceInDays(date1, date2) {
const diff = date2.getTime() - date1.getTime();
return diff / (1000 * 60 * 60 * 24);
}
/**
* Calculates the difference in hours between two dates.
*
* @param date1 - The earlier date.
* @param date2 - The later date.
* @returns The number of hours between the two dates.
*/
differenceInHours(date1, date2) {
const diff = date2.getTime() - date1.getTime();
return diff / (1000 * 60 * 60);
}
/**
* Calculates the difference in minutes between two dates.
*
* @param date1 - The earlier date.
* @param date2 - The later date.
* @returns The number of minutes between the two dates.
*/
differenceInMinutes(date1, date2) {
const diff = date2.getTime() - date1.getTime();
return diff / (1000 * 60);
}
/**
* Checks if two dates are on the same day.
*
* @param date1 - The first date.
* @param date2 - The second date.
* @returns True if the dates are on the same day, false otherwise.
*/
isSameDay(date1, date2) {
return (date1.getFullYear() === date2.getFullYear() &&
date1.getMonth() === date2.getMonth() &&
date1.getDate() === date2.getDate());
}
/**
* Returns the ISO week number for a given date.
*
* @param date - The date for which to get the week number.
* @returns The ISO week number (1-53).
*/
getWeekNumber(date) {
const tempDate = new Date(date.getTime());
tempDate.setHours(0, 0, 0, 0);
// Set to nearest Thursday: current date + 4 - current day number, making Thursday day 4
tempDate.setDate(tempDate.getDate() + 4 - (tempDate.getDay() || 7));
const yearStart = new Date(tempDate.getFullYear(), 0, 1);
// Calculate full weeks to nearest Thursday
return Math.ceil(((tempDate.getTime() - yearStart.getTime()) / 86400000 + 1) / 7);
}
/**
* Returns the number of weeks in a month for a given month and year.
*
* @param month - The month (0-11).
* @param year - The year.
* @returns The number of weeks in the month.
*/
getWeeksInMonth(month, year) {
const firstDayOfMonth = new Date(year, month, 1);
const lastDayOfMonth = new Date(year, month + 1, 0);
// Get ISO week numbers for the first and last day of the month
const firstWeek = this.getWeekNumber(firstDayOfMonth);
let lastWeek = this.getWeekNumber(lastDayOfMonth);
// Special case: when January 1st is in the last week of the previous year
if (firstWeek > lastWeek) {
lastWeek = this.getWeekNumber(new Date(year, 11, 31)); // Get week of the last day of the year
}
return lastWeek - firstWeek + 1;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TimeService, deps: [{ token: i1$2.DatePipe }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TimeService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: TimeService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [{ type: i1$2.DatePipe }] });
class UtilService {
/**
* Initialize service state: load persisted CSS variables from localStorage
* and apply them to the document root. Emits the initial CSS snapshot.
*/
constructor() {
// --- CSS variables (persisted) ---
this._storageKey = 'css_variables';
this._css = {};
this._cssSig = signal({}, ...(ngDevMode ? [{ debugName: "_cssSig" }] : []));
// --- Forms store ---
this._forms = new Map();
// --- Global bag for design/debug ---
this.var = {};
this._loadCss();
// apply on boot
for (const k of Object.keys(this._css))
this._setProperty(k, this._css[k]);
this._cssSig.set({ ...this._css });
}
// ===== Forms Management =====
/** Get or create a form state as a writable signal */
formSignal(id) {
let s = this._forms.get(id);
if (!s) {
s = signal({});
this._forms.set(id, s);
}
return s;
}
/** Back-compat: returns the current form object (mutable reference). Prefer formSignal(). */
form(id) {
const s = this.formSignal(id);
const v = s();
if (v && typeof v === 'object')
return v;
const obj = {};
s.set(obj);
return obj;
}
/**
* Check whether a form signal has been created for the given id.
* @param id Unique form identifier.
* @returns True if a signal exists for the id.
*/
hasForm(id) {
return this._forms.has(id);
}
/**
* Remove form state associated with the given id.
* @param id Unique form identifier to clear.
*/
clearForm(id) {
this._forms.delete(id);
}
// ===== Validation =====
/**
* Validate a value against a specific kind.
* - email: RFC-light pattern check.
* - text: typeof string.
* - array: Array.isArray.
* - object: non-null plain object.
* - number: finite number.
* - password: strength tiers (extra 0..4).
* @param value Input to validate.
* @param kind Validation kind.
* @param extra Additional constraint for passwords: 0..4 increasing strength.
* @returns True if value passes validation.
*/
valid(value, kind = 'email', extra = 0) {
switch (kind) {
case 'email':
return /^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,10})+$/.test(value || '');
case 'text':
return typeof value === 'string';
case 'array':
return Array.isArray(value);
case 'object':
return (typeof value === 'object' &&
!Array.isArray(value) &&
value !== null);
case 'number':
return typeof value === 'number' && Number.isFinite(value);
case 'password':
if (!value)
return false;
switch (extra) {
case 1:
return /^((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9]))/.test(value);
case 2:
return /^(((?=.*[a-z])(?=.*[0-9]))|((?=.*[A-Z])(?=.*[0-9])))(?=.{8,})/.test(value);
case 3:
return /^((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]))(?=.{8,})/.test(value);
case 4:
return /^((?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[@#$%&!\-_]))(?=.{8,})/.test(value);
default:
return !!value;
}
}
}
/** Password strength: 0..5 */
level(value = '') {
if (!value)
return 0;
let lvl = 0;
if (value.length > 8)
lvl++;
if (/[a-z]/.test(value))
lvl++;
if (/[A-Z]/.test(value))
lvl++;
if (/[0-9]/.test(value))
lvl++;
if (/[`~!@#$%^&*()_\-+=\[\]{};:'",.<>/?\\|]/.test(value))
lvl++;
return lvl;
}
// ===== CSS Variables Management =====
/** Set multiple CSS vars. opts: { local?: boolean; host?: string } */
setCss(vars, opts = {}) {
if (typeof opts === 'string') {
opts = opts === 'local' ? { local: true } : { host: opts };
}
const { local = false, host } = opts;
if (host &&
typeof window !== 'undefined' &&
window.location.host !== host)
return;
for (const k of Object.keys(vars)) {
const v = vars[k];
if (local) {
this._css[k] = v;
}
else if (this._css[k]) {
// keep persisted value unless explicitly local
this._setProperty(k, this._css[k]);
continue;
}
this._setProperty(k, v);
}
if (local) {
this._saveCss();
this._cssSig.set({ ...this._css });
}
}
/** Current persisted CSS vars snapshot */
getCss() {
return { ...this._css };
}
/** Reactive signal with current persisted CSS vars */
cssSignal() {
return this._cssSig;
}
/** Remove persisted vars by key(s) and save */
removeCss(keys) {
const list = Array.isArray(keys) ? keys : keys.split(' ');
for (const k of list)
delete this._css[k];
this._saveCss();
this._cssSig.set({ ...this._css });
}
// ===== Generators =====
/**
* Generate an array of given length filled with sequential numbers (1..n),
* random text, dates from today, or a constant value.
* @param len Desired array length.
* @param type 'number' | 'text' | 'date' | any other constant value to repeat.
* @returns Generated array.
*/
arr(len = 10, type = 'number') {
const out = [];
for (let i = 0; i < len; i++) {
switch (type) {
case 'number':
out.push(i + 1);
break;
case 'text':
out.push(this.text());
break;
case 'date':
out.push(new Date(Date.now() + i * 86_400_000));
break;
default:
out.push(type);
}
}
return out;
}
/**
* Create a random alphanumeric string.
* @param length Number of characters to generate.
* @returns Random string of requested length.
*/
text(length = 10) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let res = '';
for (let i = 0; i < length; i++)
res += chars.charAt(Math.floor(Math.random() * chars.length));
return res;
}
// ===== Internals =====
/** Persist current CSS variables to localStorage. */
_saveCss() {
try {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this._storageKey, JSON.stringify(this._css));
}
}
catch { }
}
/** Load CSS variables snapshot from localStorage into memory. */
_loadCss() {
try {
if (typeof localStorage !== 'undefined') {
const raw = localStorage.getItem(this._storageKey);
this._css = raw ? JSON.parse(raw) : {};
}
}
catch {
this._css = {};
}
}
/**
* Apply a CSS variable to the :root element.
* @param key CSS custom property name (e.g., --brand-color).
* @param value CSS value to set.
*/
_setProperty(key, value) {
try {
if (typeof document !== 'undefined') {
document.documentElement.style.setProperty(key, value);
}
}
catch { }
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UtilService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UtilService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: UtilService, decorators: [{
type: Injectable,
args: [{ providedIn: 'root' }]
}], ctorParameters: () => [] });
function provideWacom(config = DEFAULT_CONFIG) {
return makeEnvironmentProviders([
{ provide: CONFIG_TOKEN, useValue: config },
provideHttpClient(withInterceptorsFromDi()),
]);
}
/* initialize */
const DIRECTIVES = [ClickOutsideDirective];
const PIPES = [
ArrPipe,
SafePipe,
SplicePipe,
SearchPipe,
MongodatePipe,
PaginationPipe,
];
/**
* @deprecated Use provideWacom instead.
*/
class WacomModule {
static forRoot(config = DEFAULT_CONFIG) {
return {
ngModule: WacomModule,
providers: [
{
provide: CONFIG_TOKEN,
useValue: config,
},
],
};
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, imports: [CommonModule, FormsModule, ArrPipe,
SafePipe,
SplicePipe,
SearchPipe,
MongodatePipe,
PaginationPipe, ClickOutsideDirective], exports: [ArrPipe,
SafePipe,
SplicePipe,
SearchPipe,
MongodatePipe,
PaginationPipe, ClickOutsideDirective] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, providers: [
{ provide: CONFIG_TOKEN, useValue: DEFAULT_CONFIG },
provideHttpClient(withInterceptorsFromDi()),
], imports: [CommonModule, FormsModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.0", ngImport: i0, type: WacomModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, FormsModule, ...PIPES, ...DIRECTIVES],
exports: [...PIPES, ...DIRECTIVES],
providers: [
{ provide: CONFIG_TOKEN, useValue: DEFAULT_CONFIG },
provideHttpClient(withInterceptorsFromDi()),
],
}]
}] });
/*
* Interfaces
*/
/*
* End of Support
*/
/**
* Generated bundle index. Do not edit.
*/
export { ArrPipe, CONFIG_TOKEN, ClickOutsideDirective, CoreService, CrudComponent, CrudService, DEFAULT_CONFIG, DEFAULT_HTTP_CONFIG, DEFAULT_NETWORK_CONFIG, DomService, EmitterService, HttpService, ManualDisabledDirective, ManualNameDirective, ManualReadonlyDirective, ManualTypeDirective, MetaGuard, MetaService, MongodatePipe, NETWORK_CONFIG, NetworkService, NumberPipe, PaginationPipe, RtcService, SafePipe, SearchPipe, SocketService, SplicePipe, SplitPipe, StoreService, TimeService, UtilService, WacomModule, provideWacom };
//# sourceMappingURL=wacom.mjs.map