@controladad/ng-base
Version:
Everything you need for Angular
1,220 lines (1,198 loc) • 548 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, inject, Injectable, signal, computed, EventEmitter, Component, ViewChild, Input, Output, HostBinding, ElementRef, ChangeDetectionStrategy, HostListener, effect, DestroyRef, input, ContentChildren, Directive, PLATFORM_ID, Inject, Optional, Self, Host, Injector, runInInjectionContext, ViewChildren, Pipe, ChangeDetectorRef, provideAppInitializer, importProvidersFrom } from '@angular/core';
import { DomSanitizer } from '@angular/platform-browser';
import * as i2 from '@angular/material/icon';
import { MatIconRegistry, MatIconModule, MatIcon } from '@angular/material/icon';
import { createStore, withProps, select, getStore, enableElfProdMode } from '@ngneat/elf';
import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { persistState, localStorageStrategy, sessionStorageStrategy } from '@ngneat/elf-persist-state';
import { Observable, of, tap, filter, pipe, map, distinctUntilChanged, Subject, debounceTime, startWith, BehaviorSubject, Subscription, merge, debounce, timer, fromEvent, take, combineLatest, switchMap as switchMap$1, catchError, NEVER, retry, throwError, interval } from 'rxjs';
import { RouterLink, Router, ResolveStart, NavigationEnd } from '@angular/router';
import * as jalali from 'date-fns-jalali';
import { addDays, addMonths, addYears, toDate, getMonth, parseISO, format, getDate, getDay, setDay, setMonth, getDaysInMonth, getYear, parse, set } from 'date-fns-jalali';
import * as dateFns from 'date-fns';
import { switchMap } from 'rxjs/operators';
import * as i1$c from '@angular/material/dialog';
import { MatDialogRef, MAT_DIALOG_DATA, MatDialog, MatDialogModule } from '@angular/material/dialog';
import * as i1$2 from '@angular/forms';
import { ReactiveFormsModule, Validators, FormGroup, FormControl, UntypedFormControl, FormsModule } from '@angular/forms';
import * as i1 from '@angular/material/button';
import { MatButtonModule } from '@angular/material/button';
import * as i2$1 from '@angular/material/progress-spinner';
import { MatProgressSpinnerModule, MatProgressSpinner } from '@angular/material/progress-spinner';
import * as i1$6 from '@angular/common';
import { NgStyle, NgTemplateOutlet, AsyncPipe, isPlatformServer, DatePipe, NgClass, DecimalPipe, formatNumber, Location, registerLocaleData } from '@angular/common';
import * as i3 from '@angular/material/tooltip';
import { MatTooltipModule } from '@angular/material/tooltip';
import * as i1$1 from '@angular/material/checkbox';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { formControl, isFormControlExtended, formGroup, Validators as Validators$1, provideFormErrors } from '@al00x/forms';
import { trigger, transition, style, animate } from '@angular/animations';
import * as i1$4 from '@angular/material/menu';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
import * as i6 from '@angular/material/datepicker';
import { MatDatepickerModule, DateRange, MAT_DATE_RANGE_SELECTION_STRATEGY, DefaultMatCalendarRangeStrategy } from '@angular/material/datepicker';
import * as i2$2 from '@angular/material/form-field';
import { MatFormFieldModule } from '@angular/material/form-field';
import * as i1$5 from '@angular/material/input';
import { MatInputModule } from '@angular/material/input';
import { MatChipsModule } from '@angular/material/chips';
import * as i8 from '@angular/material/progress-bar';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import * as i1$3 from '@angular/material/radio';
import { MatRadioButton, MatRadioModule } from '@angular/material/radio';
import tippy from 'tippy.js';
import Inputmask from 'inputmask';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import * as i9 from 'ngx-quill';
import { QuillModule } from 'ngx-quill';
import * as i3$1 from '@angular/cdk/bidi';
import * as i1$7 from '@al00x/screen-detector';
import { ScreenDetectorService } from '@al00x/screen-detector';
import * as i1$8 from '@angular/material/slider';
import { MatSliderModule } from '@angular/material/slider';
import * as i1$9 from '@angular/material/slide-toggle';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import * as i1$a from '@angular/material/tabs';
import { MatTabsModule } from '@angular/material/tabs';
import * as i1$b from 'ngx-skeleton-loader';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import * as i1$d from '@al00x/printer';
import { AlxPrintModule } from '@al00x/printer';
import * as i1$e from '@angular/material/snack-bar';
import { MAT_SNACK_BAR_DATA, MatSnackBarModule } from '@angular/material/snack-bar';
import * as i1$f from '@angular/material/table';
import { MatColumnDef, MatCellDef, MatHeaderCellDef, MatFooterCellDef, MatCell, MatHeaderCell, MatTableModule } from '@angular/material/table';
import { MatBadge, MatBadgeModule } from '@angular/material/badge';
import * as i2$3 from '@angular/cdk/a11y';
import { A11yModule } from '@angular/cdk/a11y';
import { HttpClient, HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import * as i1$g from 'ngx-file-drop';
import { NgxFileDropModule } from 'ngx-file-drop';
import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
import { faIR } from 'date-fns-jalali/locale';
import { provideAnimations } from '@angular/platform-browser/animations';
import { DateFnsAdapter } from '@angular/material-date-fns-adapter';
import { loadTranslations } from '@angular/localize';
import { enUS } from 'date-fns/locale';
import localeEn from '@angular/common/locales/en';
const API_BASEURL = new InjectionToken('API_BASEURL');
const ENVIRONMENT = new InjectionToken('ENVIRONMENT');
// prettier-ignore
const baseIcons = [
'add', 'arrow-down', 'arrow-up', 'arrow-left', 'arrow-right', 'calendar', 'camera', 'check', 'check-double',
'chevron-down', 'chevron-left', 'chevron-right', 'chevron-up', 'clock', 'close', 'delete', 'dropdown', 'edit',
'edit-box', 'error', 'excel-file', 'eye', 'eye-slash', 'filter', 'filter-filled', 'hashtag', 'info', 'info-circle',
'list', 'location', 'location-check', 'login', 'logout', 'menu', 'nav', 'numeric-down', 'numeric-up', 'paper',
'paper-details', 'password', 'phone', 'plate', 'play', 'plus', 'power', 'print', 'question-circle', 'refresh',
'reports', 'save', 'search', 'settings', 'sort', 'sort-down', 'sort-up', 'time', 'trash', 'trash-alt', 'user',
'user-circle', 'users', 'wrench'
];
let isBaseRegistered = false;
function registerIcons(icons) {
const sanitizer = inject(DomSanitizer);
const iconRegistry = inject(MatIconRegistry);
if (!isBaseRegistered) {
for (const icon of baseIcons) {
iconRegistry.addSvgIcon(icon, sanitizer.bypassSecurityTrustResourceUrl(`./assets/base/icons/${icon}.svg`));
}
isBaseRegistered = true;
}
for (const icon of icons ?? []) {
iconRegistry.addSvgIcon(icon, sanitizer.bypassSecurityTrustResourceUrl(`./assets/icons/${icon}.svg`));
}
}
class CacGlobalConfig {
// Do not change this variable manually, it's automatically updated via the provider.
static { this.defaultLang = 'en'; }
static { this.applicationName = ''; }
static { this.localization = {
langs: ['en'],
}; }
// applies application name to store keys, for example if your store key is `auth`, it will become `app_name_auth`
static { this.applyPrefixToStorageKeys = true; }
static generateStoreKey(key) {
return `${this.applyPrefixToStorageKeys && this.applicationName.length ? `${this.applicationName}_` : ''}${key}`;
}
}
class ApiInterceptor {
constructor() {
this.apiBaseUrl = inject(API_BASEURL);
}
// using inject() causes circular error, idk why...
// readonly apiBaseUrl = inject(API_BASEURL);
intercept(request, next) {
request = request.clone({
url: request.url.startsWith('/') ? this.apiBaseUrl + request.url : request.url,
});
return next.handle(request);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: ApiInterceptor, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: ApiInterceptor }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: ApiInterceptor, decorators: [{
type: Injectable
}] });
class BaseStore {
constructor(storeOpts) {
this.storeOpts = storeOpts;
const key = storeOpts.exactKey ? storeOpts.key : CacGlobalConfig.generateStoreKey(storeOpts.key);
const store = createStore({ name: key }, withProps(storeOpts.default ?? {}));
persistState(store, {
key: key,
storage: storeOpts.storageStrategy ?? localStorageStrategy,
});
this.store = store;
this.state$ = this.store.pipe();
try {
this.signal = toSignal(this.state$);
}
catch { /* empty */ }
}
get state() {
return this.get();
}
get() {
return this.store.getValue();
}
patch(value) {
return this.store.update((s) => ({
...s,
...value,
}));
}
}
class _AppBaseStore extends BaseStore {
constructor(defaults) {
super({
key: 'app',
default: defaults,
});
this.rememberMe$ = this.store.pipe(select(this.rememberMe));
this.lang$ = this.store.pipe(select(this.lang));
}
rememberMe() {
return this.get().rememberMe ?? false;
}
setRememberMe(value) {
// @ts-ignore
this.patch({
rememberMe: value,
});
}
lang() {
return this.get().lang ?? CacGlobalConfig.defaultLang;
}
setLang(value) {
// @ts-ignore
this.patch({
lang: value,
});
}
}
// This is dummy, used to make service out of the `_AppBaseStore`. For extension, `_AppBaseStore` should be used
class AppBaseStore extends _AppBaseStore {
constructor() {
super();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AppBaseStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AppBaseStore, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: AppBaseStore, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
const SERVER_ERROR = {
0: $localize `:@@base.errors.server.0:Error connecting, Check your network connection.`,
401: $localize `:@@base.errors.server.401:Your session has expired, please login again.`,
403: $localize `:@@base.errors.server.403:You do not have permission to continue.`,
404: $localize `:@@base.errors.server.404:The requested destination was not found.`,
500: $localize `:@@base.errors.server.500:An error occurred on the server.`,
503: $localize `:@@base.errors.server.503:The server is unavailable.`,
default: $localize `:@@base.errors.server.default:Error.`,
};
const API_ERROR = {
1: $localize `:@@base.errors.api.default:Username or Password is incorrect`,
};
class ErrorHelper {
static getResponseErrorMessage(serverCode, apiError) {
return serverCode === 400 && apiError ? this.getApiErrorMessage(apiError) : this.getServerErrorMessage(serverCode);
}
static getServerErrorMessage(status) {
return SERVER_ERROR[status] ?? SERVER_ERROR.default;
}
static getApiErrorMessage(error) {
return API_ERROR[error.code] ?? error.message;
}
// we pass the error object, returned by the api to this function
static parseApiErrorObject(error) {
if (!error)
return undefined;
if (typeof error === 'object') {
if ('error' in error && typeof error.error === 'string') {
return error.error;
}
if ('errors' in error && typeof error.errors === 'object') {
const message = this.extractMessageFromObjectRecord(error.errors);
return message
? {
code: -1,
message,
}
: undefined;
}
if ('error' in error && typeof error === 'object' && 'code' in error.error && 'message' in error.error) {
return this.parseApiErrorObject(error.error);
}
return error;
}
else if (typeof error === 'string') {
try {
return JSON.parse(error);
}
catch {
return {
code: -1,
message: error,
};
}
}
return undefined;
}
static extractMessageFromObjectRecord(object) {
const values = Object.values(object);
for (const value of values) {
if (value instanceof Array && value.length > 0) {
return value[0];
}
else if (typeof value === 'string') {
return value;
}
}
return undefined;
}
}
let _cachedDateFns;
// Get DateFns library based on current language
function DateFns() {
if (_cachedDateFns)
return _cachedDateFns;
if (CacGlobalConfig.localization.forceDateFnsLib === 'jalali')
_cachedDateFns = jalali;
else if (CacGlobalConfig.localization.forceDateFnsLib === 'georgian')
_cachedDateFns = dateFns;
else {
const store = getStore(CacGlobalConfig.generateStoreKey('app'));
_cachedDateFns = (store?.getValue().lang === 'fa' ? jalali : dateFns);
}
return _cachedDateFns;
}
function getDatesIntervalInHHMMSS(start, end, showSymbol) {
const hours = Math.abs(DateFns().differenceInHours(start, end));
const duration = DateFns().intervalToDuration({ start, end });
const result = {
hours,
minutes: duration.minutes,
seconds: duration.seconds,
negative: start > end,
};
return {
formatted: `${result.hours?.toFixed().padStart(2, '0')}:${result.minutes
?.toFixed()
.padStart(2, '0')}:${result.seconds?.toFixed().padStart(2, '0')}${showSymbol ? ` (${result.negative ? '-' : '+'})` : ''}`,
...result,
};
}
function getFormattedDate(date, format = 'yyyy/MM/dd') {
return !date ? '' : DateFns().format(typeof date === 'string' ? new Date(date) : date, format);
}
function parseDate(date, format) {
return DateFns().parse(date, format, new Date());
}
function getDurationInHHMM(minutes) {
const h = `${Math.floor(minutes / 60)}`.padStart(2, '0');
const m = `${minutes % 60}`.padStart(2, '0');
return `${h}:${m}`;
}
function getHHMMInDuration(text) {
const split = text.split(':');
const h = split.at(0);
const m = split.at(1);
if (!h || !m)
return 0;
const parsedH = parseInt(h);
const parsedM = parseInt(m);
if (isNaN(parsedH) || isNaN(parsedM))
return 0;
return parseInt(h) * 60 + parseInt(m);
}
function getFullName(obj, defaultValue = '') {
const firstNameKeys = ['firstName', 'first_name', 'firstname', 'FirstName'];
const lastNameKeys = ['lastName', 'last_name', 'lastname', 'LastName'];
if (!obj)
return defaultValue;
const firstNameKey = firstNameKeys.find((key) => key in obj);
const lastNameKey = lastNameKeys.find((key) => key in obj);
const name = `${firstNameKey ? (obj[firstNameKey] ?? '') : ''} ${lastNameKey ? (obj[lastNameKey] ?? '') : ''}`.trim();
return name.length ? name : defaultValue;
}
function toPascalCase(text) {
let result = text ?? '';
if (result.length) {
result = result.charAt(0).toUpperCase() + result.slice(1);
}
return result;
}
async function lazyLoad(component, selector) {
const entry = await component;
if (selector)
return selector(entry);
const props = Object.values(entry);
if (props.length)
return props[0];
console.error('LAZY LOAD ERROR', entry);
throw new Error('Entry has no exported components!!');
}
function resolveRouteChildren(route) {
if (route.children)
return route.children;
// _loadedRoutes is a private property of Route which holds the list of lazy loaded routes config. we need to use it!
// @ts-ignore
if (route._loadedRoutes)
return route._loadedRoutes;
}
function isRouteExtended(r) {
return 'layout' in r;
}
function effectDep(dep, fn, destroyRef) {
return toObservable(dep).pipe(takeUntilDestroyed(destroyRef)).subscribe(fn);
}
function clone(o) {
return structuredClone(o);
}
const objectToId = (t) => ('id' in t ? t.id : typeof t === 'object' ? Object.values(t).at(0) : t);
function omit(obj, ...keys) {
keys.forEach((key) => delete obj[key]);
return obj;
}
function isObject(item) {
return item && typeof item === 'object' && !Array.isArray(item);
}
function deepMerge(target, ...sources) {
if (!sources.length)
return target;
const source = sources.shift();
if (isObject(target) && isObject(source)) {
for (const key in source) {
if (isObject(source[key])) {
if (!target[key])
Object.assign(target, { [key]: {} });
deepMerge(target[key], source[key]);
}
else {
Object.assign(target, { [key]: source[key] });
}
}
}
return deepMerge(target, ...sources);
}
function getObservable(value) {
return value instanceof Observable ? value : of(value);
}
function startWithTap(callback) {
return (source) => of({}).pipe(tap(callback), switchMap(() => source));
}
function filterEmpty() {
return (source) => source.pipe(filter((x) => x instanceof Array
? x.filter((t) => t !== undefined && t !== null).length === x.length
: x !== undefined && x !== null));
}
function distinctUntilChangedWithTimeout(timeout, compare) {
return pipe(map((t) => ({ value: t, date: new Date() })), distinctUntilChanged((pre, cur) => (compare ? compare(pre?.value, cur?.value) : pre?.value === cur?.value) &&
pre.date.getTime() > cur.date.getTime() - timeout), map((t) => t.value));
}
function destroyRefObservable(ref) {
return new Observable((observer) => {
ref.onDestroy(observer.next.bind(observer));
});
}
function getFromItemRecord(items, value) {
return items.find((t) => t.value === value || (t.alt !== undefined && t.alt === value));
}
function flatten(array) {
return [].concat(...array);
}
function includes(array, terms) {
if (!(terms instanceof Array))
return array.includes(terms);
for (const term of terms) {
if (array.includes(term))
return true;
}
return false;
}
function arraySafeAt(array, index) {
if (index === null || index === undefined)
return null;
return array.at(index) ?? null;
}
function dedupe(array) {
return [...new Set(array)];
}
function dedupeObj(array, key) {
return array.filter((value, index) => index === array.findIndex((t) => t[key] === value[key]));
}
function subset(array, sub) {
return sub.every((val) => array.includes(val));
}
function arraysEqual(a, b) {
if (a === b)
return true;
if (a == null || b == null)
return false;
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; ++i) {
if (a[i] !== b[i])
return false;
}
return true;
}
const ACTION_TYPES = ['read', 'create', 'update', 'delete', 'print', 'export', 'output', 'other'];
function getAllActions() {
return ACTION_TYPES;
}
function permissionNameToKey(name, action) {
switch (action) {
case 'create':
return `Add${name}`;
case 'update':
return `Update${name}`;
case 'read':
return `Get${name}`;
case 'delete':
return '';
case 'print':
return `PrintListOf${name}`;
case 'export':
return `ExportAll${name}`;
case 'output':
return `${name}Output`;
case `other`:
return `${name}`;
}
}
function permissionActionToLabel(action) {
switch (action) {
case 'create':
return $localize `:@@base.permissions.crudActions.create:Add`;
case 'update':
return $localize `:@@base.permissions.crudActions.update:Edit`;
case 'read':
return $localize `:@@base.permissions.crudActions.read:View Only`;
case 'delete':
return $localize `:@@base.permissions.crudActions.delete:Delete`;
case 'print':
return $localize `:@@base.permissions.crudActions.print:Print`;
case 'export':
return $localize `:@@base.permissions.crudActions.export:Export excel`;
case 'output':
return $localize `:@@base.permissions.crudActions.output:Export`;
case `other`:
return '';
}
}
function isClass(obj) {
const isCtorClass = obj.constructor && obj.constructor.toString().substring(0, 5) === 'class';
if (obj.prototype === undefined) {
return isCtorClass;
}
const isPrototypeCtorClass = obj.prototype.constructor &&
obj.prototype.constructor.toString &&
obj.prototype.constructor.toString().substring(0, 5) === 'class';
return isCtorClass || isPrototypeCtorClass;
}
function provide(token, value, multi = false) {
// @ts-ignore
if (typeof value === 'function') {
return { provide: token, useFactory: value, multi };
}
else if (isClass(value)) {
return { provide: token, useClass: value, multi };
}
else {
return { provide: token, useValue: value, multi };
}
}
function componentWithDefaultConfig(component, token, defaultValues = {}) {
const valuesInjected = injectOptional(token);
if (!valuesInjected)
return;
const values = valuesInjected instanceof Array ? valuesInjected : [valuesInjected];
let defaults = {
...defaultValues,
};
for (const val of values) {
defaults = {
...defaults,
...val,
};
}
for (const key in defaults) {
// @ts-ignore
component[key] = defaults[key];
}
}
function injectOptional(token) {
try {
return inject(token);
}
catch {
return undefined;
}
}
class SortModel {
constructor(key, direction) {
this._key = signal(undefined);
this._direction = signal('desc');
this._changes$ = new Subject();
this.key = this._key.asReadonly();
this.direction = this._direction.asReadonly();
this.changes$ = this._changes$.pipe(debounceTime(50));
this._key.set(key);
this._direction.set(direction ?? 'desc');
}
setKey(sort) {
this._key.set(sort);
this.emit();
}
setDirection(direction) {
this._direction.set(direction);
this.emit();
}
create() {
const key = this.key();
return key
? {
key: key,
direction: this.direction(),
}
: undefined;
}
emit() {
this._changes$.next([this._key(), this._direction()]);
}
}
// TODO: Update constructor initial parameters (turn into object, it's messy currently)
// TODO: add formControl binding
// TODO: turn off multiple mode by default
class SelectionModel {
constructor(itemsCount, multiple, initial, itemToId) {
this._totalCount = 0;
this._selectedCount = 0;
this._currentViewItems = [];
this._blockBoundUpdate = false;
this.indeterminate = signal(false);
this.allSelected = signal(false);
this.selected = signal([]);
this.selectedIds = signal({});
this.selectedCount = computed(() => this.selected()?.length ?? 0);
this.hasSelection = computed(() => this.selectedCount() !== 0);
this.id = crypto.randomUUID();
this._totalCount = itemsCount ?? 0;
this._multiple = multiple ?? true;
this._itemToId = itemToId ?? ((t) => (t && typeof t === 'object' && 'id' in t ? t.id : t));
initial ? this.select(...initial) : null;
}
get isMultiple() {
return this._multiple;
}
select(...items) {
if (this._multiple) {
this.set(...[...new Set([...this.selected(), ...items])]);
}
else if (items.length) {
// this.clear();
this.set(items[0]);
}
}
deselect(...items) {
const itemsId = items.map(this._itemToId);
this.set(...this.selected().filter((t) => !itemsId.includes(this._itemToId(t))));
}
toggle(...items) {
const selected = this.selected();
if (this._multiple) {
let newItems = [...selected];
if (newItems.length > 0) {
for (const item of items) {
const index = selected.findIndex((t) => this._itemToId(t) === this._itemToId(item));
if (index !== -1) {
newItems.splice(index, 1);
}
else {
newItems.push(item);
}
}
}
else {
newItems = [...items];
}
this.set(...newItems);
}
else {
if (selected.some(t => t === items[0])) {
this.clear();
}
else {
this.set(items[0]);
}
}
}
selectAll() {
this.select(...this._currentViewItems);
}
deselectAll() {
this.deselect(...this._currentViewItems);
}
toggleAll() {
if (this._selectedCount === this._totalCount) {
this.deselectAll();
}
else {
this.selectAll();
}
}
clear() {
if (this._selectedCount === 0)
return;
this.set();
}
set(...items) {
this.selected.set(items);
this.selectedIds.set(items
.map((t) => this._itemToId(t))
.reduce((pre, cur) => ({
...pre,
[cur]: true,
}), {}));
this._selectedCount = items.length;
this.calculateSelectionState();
this.updateBoundedFormControl();
}
isSelected(item) {
return !!this.selectedIds()[this._itemToId(item)];
}
setTotalCount(count) {
this._totalCount = count;
this.calculateSelectionState();
}
setItems(items, setTotalCount = true) {
this._currentViewItems = items;
if (setTotalCount) {
this._totalCount = items.length;
}
this.calculateSelectionState();
}
setItemToIdFn(fn) {
this._itemToId = fn;
}
setMultiple(value) {
this._multiple = value === undefined ? true : value;
}
bindFormControl(control, destroyRef) {
this._boundFormControl = control;
if (!control)
return;
control.valueChanges.pipe(startWith(control.value), takeUntilDestroyed(destroyRef)).subscribe((v) => {
if (this._blockBoundUpdate || v === this.selectedIds()) {
this._blockBoundUpdate = false;
return;
}
if (v === null || v === undefined) {
this.clear();
}
else {
this.set(...(v instanceof Array ? v : [v]));
}
});
}
updateBoundedFormControl() {
if (!this._boundFormControl)
return;
this._blockBoundUpdate = true;
this._boundFormControl.setValue(this.selectedIds());
this._boundFormControl.setSelectedItems(this.selected());
}
calculateSelectionState() {
const selectedViewItemsCount = this._currentViewItems
.map(this._itemToId)
.filter((t) => this.selectedIds()[t]).length;
if (this._selectedCount === 0 || selectedViewItemsCount === 0) {
this.indeterminate.set(false);
this.allSelected.set(false);
}
else if (this._selectedCount === this._totalCount) {
this.indeterminate.set(false);
this.allSelected.set(true);
}
else {
this.indeterminate.set(selectedViewItemsCount !== this._currentViewItems.length);
this.allSelected.set(!this.indeterminate());
}
}
}
const ActiveValues = [
{ value: true, label: 'Active' },
{ value: false, label: 'Inactive' },
];
const SuspendedValues = [
{ value: false, label: 'Active' },
{ value: true, label: 'Inactive' },
];
const BooleanValues = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' },
];
class FilterModel {
constructor(init, overrideEmpty) {
this.overrideEmpty = overrideEmpty;
this._filters = signal({});
this._changes$ = new Subject();
this.filters = computed(() => {
const obj = this._filters();
if (!obj)
return this.overrideEmpty;
Object.keys(obj).forEach((key) => obj[key] === undefined && delete obj[key]);
if (Object.keys(obj).length === 0)
return this.overrideEmpty;
return obj;
});
this.filtersArray = computed(() => {
const filters = this.filters();
if (!filters)
return [];
return Object.values(filters);
});
this.hasFilter = computed(() => {
return this.filters() !== undefined;
});
this.changes$ = this._changes$.pipe(debounceTime(50));
this._filters.set(init ?? overrideEmpty ?? {});
}
set(data, values, emit = true) {
if (values === undefined || values.every((t) => t.value === undefined || t.value === null)) {
this.remove(data.prop);
return;
}
this._filters.set({
...this._filters(),
[data.prop]: this.createFilterItem(data, values),
});
if (emit)
this.emitChanges();
}
remove(prop) {
this._filters.set({
...this._filters(),
[prop]: undefined,
});
this.emitChanges();
}
clear() {
this._filters.set({});
this.emitChanges();
}
create() {
const filters = this.filtersArray();
return filters
.filter((t) => !!t.values && t.values.length > 0)
.map((item) => item.values.map((v) => ({
key: v.key ?? item.key,
strictKey: !!item.strictKey || !!v.key,
value: this.mapValueToString(v.value),
type: v.type,
})))
.reduce((pre, cur) => pre.concat(cur), []);
}
// Emit will be called automatically, use this is you disabled emit on any functions
emitChanges() {
this._changes$.next(this._filters());
}
createFilterItem(opts, value) {
const item = {
...this.getKey(opts),
values: undefined,
prop: opts.prop,
label: opts.label,
icon: opts.icon,
};
return this.setValueForFilterItem(item, value, opts.items);
}
setValueForFilterItem(item, values, records) {
const newValues = values
? values.every((t) => t.value === null || t.value === undefined)
? undefined
: values.map((t) => {
const value = t.value;
if (value instanceof Date && t.controlType === 'date') {
if (t.type === 'lower') {
value.setHours(23, 59, 59, 999);
}
else {
value.setHours(0, 0, 0, 0);
}
}
return {
...t,
value,
};
})
: undefined;
item.values = newValues;
item.formatted = newValues ? this.formatFilterItem(item, records) : undefined;
return item;
}
mapValueToString(value) {
if (value instanceof Array) {
return value.map((t) => this.mapValueToString(t));
}
if (value instanceof Date) {
return value.toISOString();
}
if (typeof value === 'boolean') {
return value ? 'true' : 'false';
}
if (typeof value === 'number') {
return value.toString();
}
return value ?? '';
}
mapValueToReadable(filterValue, records) {
const value = filterValue.value;
if (value instanceof Date) {
if (filterValue.controlType === 'datetime') {
return getFormattedDate(value, 'HH:mm ,yyyy/MM/dd');
}
return getFormattedDate(value);
}
if (filterValue.displayText && filterValue.displayText.length) {
return filterValue.displayText instanceof Array ? filterValue.displayText.join(', ') : filterValue.displayText;
}
if (records && records instanceof Array) {
return getFromItemRecord(records, value)?.label ?? '';
}
if (typeof value === 'boolean') {
return value ? $localize `:@@base.values.trueText:Yes` : $localize `:@@base.values.falseText:No`;
}
if (typeof value === 'number') {
return value.toString();
}
return value instanceof Array ? (value.length ? value.join(', ') : '') : value ?? '';
}
getKey(data) {
if (data.filterable === true) {
return { key: data.prop };
}
else if (typeof data.filterable === 'string') {
return data.filterable !== '' ? { key: data.filterable, strictKey: true } : { key: data.prop };
}
return { key: data.prop };
}
formatFilterItem(item, records) {
let prefix = '';
let suffix = '';
const equal = item.values?.find((t) => t.type === 'equal' || t.type === 'contains');
const greater = item.values?.find((t) => t.type === 'greater');
const lower = item.values?.find((t) => t.type === 'lower');
const equalValue = equal?.value !== undefined && equal?.value !== null ? this.mapValueToReadable(equal, records) : undefined;
const greaterValue = greater?.value !== undefined && greater?.value !== null ? this.mapValueToReadable(greater, records) : undefined;
const lowerValue = lower?.value !== undefined && lower?.value !== null ? this.mapValueToReadable(lower, records) : undefined;
if (equalValue !== undefined && equalValue !== null) {
suffix += ` : ${equalValue}`;
}
else if (greaterValue !== undefined && greaterValue !== null && lowerValue !== undefined && lowerValue !== null) {
prefix += `${greaterValue} < `;
suffix += ` < ${lowerValue}`;
}
else if (greaterValue !== undefined && greaterValue !== null) {
suffix += ` > ${greaterValue}`;
}
else if (lowerValue !== undefined && lowerValue !== null) {
suffix += ` < ${lowerValue}`;
}
if (prefix === '' && suffix === '')
return undefined;
return {
full: `${prefix}${item.label}${suffix}`,
text: item.label,
prefix: prefix,
suffix: suffix,
};
}
}
class TableFilterModel extends FilterModel {
constructor(initValue, overrideEmptyValue) {
super(initValue);
this.initValue = initValue;
this.overrideEmptyValue = overrideEmptyValue;
this.columnFilters = {};
this.columnLabels = {};
this.columnsChanged$ = new BehaviorSubject(undefined);
}
set(columnProp, values, emit = true) {
if (typeof columnProp === 'object') {
super.set(columnProp, values, emit);
return;
}
if (!this._columns)
return;
const col = this._columns.find((t) => t.prop === columnProp);
if (!col) {
console.error(`Table filter failed with the given prop: '${columnProp}'`);
return;
}
super.set(col, values, emit);
}
setColumns(columns) {
this._columns = columns;
this.updateFilters();
this.setOverrideEmpty();
this.columnsChanged$.next();
}
getColumnFilters(columnProp) {
return this.columnFilters[columnProp];
}
setOverrideEmpty() {
if (!this.overrideEmptyValue || !this._columns)
return;
this.overrideEmpty = {};
for (const prop in this.overrideEmptyValue) {
const column = this._columns.find((t) => t.prop === prop);
if (!column)
continue;
const value = this.overrideEmptyValue[prop];
const item = this.createFilterItem(column, value);
this.overrideEmpty[item.prop] = item;
}
if (!this.initValue) {
this._filters.set(this.overrideEmpty);
}
}
updateFilters() {
if (!this._columns)
return;
this.columnFilters = {};
this.columnLabels = {};
for (const column of this._columns) {
let filters;
if (column.filterable instanceof Array) {
filters = column.filterable;
}
else if (column.filterable) {
switch (column.type ?? 'text') {
case 'number': {
filters = [
{ inputType: 'number', type: 'equal' },
{ inputType: 'number', type: 'greater' },
{ inputType: 'number', type: 'lower' },
];
break;
}
case 'boolean': {
filters = [{ type: 'equal', controlType: 'select', items: column.items ?? BooleanValues }];
break;
}
case 'plate':
filters = [{ type: 'equal', controlType: 'plate' }];
break;
default: {
filters = [
{
type: 'contains',
controlType: column.items ? 'select' : 'input',
items: column.items ?? undefined,
},
];
}
}
}
if (filters) {
this.columnLabels[column.prop] = column.label;
this.columnFilters[column.prop] = filters;
}
}
}
}
const ICON_COMPONENT_CONFIG = new InjectionToken('IconComponent');
class CacIconComponent {
constructor() {
this.disabled = false;
this.size = '1.5rem';
this.strokeWidth = 1.9;
this.onClick = new EventEmitter();
this.pointerEventsNone = false;
this.thisWidth = '';
this.thisHeight = '';
this.isClickable = signal(false);
componentWithDefaultConfig(this, ICON_COMPONENT_CONFIG);
this.thisWidth = this.size;
this.thisHeight = this.size;
}
ngOnInit() {
this.isClickable.set(this.onClick.observed);
}
ngAfterViewInit() {
// @ts-ignore
const fetchSub = this.matIcon._currentIconFetch;
fetchSub.add(() => {
const svgElement = this.matIcon._elementRef.nativeElement.children.item(0);
if (!svgElement)
return;
svgElement.style.strokeWidth = `${this.strokeWidth}`;
});
}
ngOnChanges(changes) {
if (changes['disabled']) {
this.pointerEventsNone = this.disabled;
}
if (changes['size']) {
this.thisWidth = this.size;
this.thisHeight = this.size;
}
}
onClickEvent(e) {
if (this.disabled)
return;
this.onClick.emit(e);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: CacIconComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.0.7", type: CacIconComponent, isStandalone: true, selector: "cac-icon", inputs: { icon: "icon", disabled: "disabled", size: "size", strokeWidth: "strokeWidth", iconClass: "iconClass", wrapperClass: "wrapperClass" }, outputs: { onClick: "onClick" }, host: { properties: { "class.pointer-events-none": "this.pointerEventsNone", "style.width": "this.thisWidth", "style.height": "this.thisHeight" } }, viewQueries: [{ propertyName: "matIcon", first: true, predicate: ["MatIcon"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div\n role=\"presentation\"\n class=\"ui-icon relative {{ disabled ? 'disabled' : '' }} w-full h-full {{ wrapperClass }}\"\n [class.is-clickable]=\"isClickable()\"\n (click)=\"onClickEvent($event)\"\n >\n @if (!!icon) {\n <mat-icon\n #MatIcon\n class=\"{{ iconClass ?? '' }}\"\n [ngStyle]=\"{ width: size, height: size }\"\n [svgIcon]=\"icon\"\n ></mat-icon>\n }\n <ng-content></ng-content>\n</div>\n", styles: [":host{display:inline-block;color:inherit;position:relative;border-radius:.25rem}:host ::ng-deep .ui-icon{height:inherit;width:inherit;color:inherit;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;display:inline-flex;align-items:center;justify-content:center;border-radius:inherit}:host ::ng-deep .ui-icon.is-clickable{cursor:pointer}:host ::ng-deep .ui-icon.is-clickable:hover{background-color:var(--surface-bright)!important;--tw-brightness: brightness(.9) !important;--tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / .05)) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.is-clickable:hover:active{background-color:var(--surface-container)!important;--tw-brightness: brightness(1) !important;--tw-drop-shadow: drop-shadow(0 0 #0000) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.disabled{cursor:default!important;--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}\n"], dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.0.7", ngImport: i0, type: CacIconComponent, decorators: [{
type: Component,
args: [{ selector: 'cac-icon', standalone: true, imports: [MatIconModule, NgStyle], template: "<div\n role=\"presentation\"\n class=\"ui-icon relative {{ disabled ? 'disabled' : '' }} w-full h-full {{ wrapperClass }}\"\n [class.is-clickable]=\"isClickable()\"\n (click)=\"onClickEvent($event)\"\n >\n @if (!!icon) {\n <mat-icon\n #MatIcon\n class=\"{{ iconClass ?? '' }}\"\n [ngStyle]=\"{ width: size, height: size }\"\n [svgIcon]=\"icon\"\n ></mat-icon>\n }\n <ng-content></ng-content>\n</div>\n", styles: [":host{display:inline-block;color:inherit;position:relative;border-radius:.25rem}:host ::ng-deep .ui-icon{height:inherit;width:inherit;color:inherit;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;display:inline-flex;align-items:center;justify-content:center;border-radius:inherit}:host ::ng-deep .ui-icon.is-clickable{cursor:pointer}:host ::ng-deep .ui-icon.is-clickable:hover{background-color:var(--surface-bright)!important;--tw-brightness: brightness(.9) !important;--tw-drop-shadow: drop-shadow(0 1px 1px rgb(0 0 0 / .05)) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.is-clickable:hover:active{background-color:var(--surface-container)!important;--tw-brightness: brightness(1) !important;--tw-drop-shadow: drop-shadow(0 0 #0000) !important;filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}:host ::ng-deep .ui-icon.disabled{cursor:default!important;--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}\n"] }]
}], ctorParameters: () => [], propDecorators: { matIcon: [{
type: ViewChild,
args: ['MatIcon']
}], icon: [{
type: Input
}], disabled: [{
type: Input
}], size: [{
type: Input
}], strokeWidth: [{
type: Input
}], iconClass: [{
type: Input
}], wrapperClass: [{
type: Input
}], onClick: [{
type: Output
}], pointerEventsNone: [{
type: HostBinding,
args: ['class.pointer-events-none']
}], thisWidth: [{
type: HostBinding,
args: ['style.width']
}], thisHeight: [{
type: HostBinding,
args: ['style.height']
}] } });
const BUTTON_COMPONENT_CONFIG = new InjectionToken('CacButtonComponent');
class CacButtonComponent {
constructor() {
this.iconPosition = 'prefix';
this.appearance = 'filled';
this.tonal = false;
this.elevated = false;
this.theme = 'primary';
this.disabled = false;
this.loadingProp = false;
this.iconSize = '1.5rem';
this.padding = '0.5rem 1rem';
this.fitContent = false;
this.align = 'center';
// TabIndex
this.tab = 0;
this.onClick = new EventEmitter();
this.filledClass = true;
this.strokedClass = false;
this.textClass = false;
this.isElevated = false;
this.isTonal = false;
this.isClicking = false;
this.primaryClass = false;
this.secondaryClass = false;
this.tertiaryClass = false;
this.errorClass = false;
this.disabledClass = false;
this.cursorNotAllowed = false;
this.loading = signal(false);
this.insufficientPermission = signal(false);
componentWithDefaultConfig(this, BUTTON_COMPONENT_CONFIG);
toObservable(this.insufficientPermission)
.pipe(takeUntilDestroyed())
.subscribe(() => {
this.setDisabledClass();
});
}
ngOnInit() {
this.setTheme();
this.checkPermission();
}
ngAfterViewInit() {
setTimeout(() => {
// Angular... doesn't bind tabIndex in template!
this.btnElement.nativeElement.tabIndex = this.tab;
}, 5);
}
ngOnChanges(changes) {
if (changes['loading']) {
this.loading.set(this.loadingProp ?? false);
}
if (changes['appearance']) {
this.strokedClass = this.appearance === 'stroked';
this.filledClass = this.appearance === 'filled';
this.textClass = this.appearance === 'text';
}
if (changes['theme']) {
this.setTheme();
}
if (changes['elevated']) {
this.isElevated = this.elevated;
}
if (changes['tonal']) {
this.isTonal = this.tonal;
}
this.setDisabledClass();
}
onPointerDown() {
this.isClicking = true;
}
onPointerCancel() {
this.isClicking = false;
}
onPointerLeave() {
this.isClicking = false;
}
onPointerUp() {
this.isClicking = false;
}
onClickEvent(e) {
if (this.loadingProp || this.disabled || this.insufficientPermission() || this.loading())
return;
this.onClick.emit(this.createClickEvent(e));
}
createClickEvent(mouseEvent) {
const e = {
event: mouseEvent ?? new Event('click'),
setLoading: (state) => {
this.loading.set(state !== undefined ? state : !this.loading());
},
pipe: () => pipe(startWithTap(() => e.setLoading(true)), tap({
next: () => e.setLoading(false),
error: () => e.setLoading(false),
complete: () => e.setLoading(f