ng-table-pg
Version:
A powerful and flexible responsive table component for Angular applications with drag-drop, filtering, pagination, and advanced responsive features
1,060 lines (1,005 loc) • 153 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, Component, Input, Pipe, NgModule, ViewEncapsulation, EventEmitter, ViewChild, Output, HostListener } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i3 from '@angular/forms';
import { FormsModule } from '@angular/forms';
import * as i1$1 from '@ngx-translate/core';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import * as i4 from '@angular/cdk/drag-drop';
import { moveItemInArray, DragDropModule } from '@angular/cdk/drag-drop';
import { Subject } from 'rxjs';
import { saveAs } from 'file-saver';
import * as XLSX from 'xlsx';
import { HttpClient } from '@angular/common/http';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
class TableService {
constructor() { }
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}], ctorParameters: () => [] });
class TableRowComponent {
text;
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableRowComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.5", type: TableRowComponent, isStandalone: true, selector: "table-row", inputs: { text: "text" }, ngImport: i0, template: `
<span class="whitespace-nowrap">{{ text }}</span>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableRowComponent, decorators: [{
type: Component,
args: [{
selector: 'table-row',
standalone: true,
imports: [CommonModule],
template: `
<span class="whitespace-nowrap">{{ text }}</span>
`,
}]
}], propDecorators: { text: [{
type: Input
}] } });
class Web3UtilsPipe {
transform(value, type, decimals = 18) {
if (value == null) {
return 'NO DATA';
}
try {
switch (type) {
case 'wallet':
return this.formatWalletAddress(String(value));
case 'wei':
return this.fromWei(String(value), decimals);
case 'token':
return this.formatTokenAmount(String(value), decimals);
default:
return value;
}
}
catch (error) {
return type === 'wei' ? '0.0000' : String(value);
}
}
formatWalletAddress(address) {
return address.length >= 10
? `${address.substring(0, 6)}...${address.substring(address.length - 4)}`
: address;
}
fromWei(wei, decimals) {
const value = Number(wei) / Math.pow(10, decimals);
return isNaN(value) ? '0.0000' : value.toFixed(4);
}
formatTokenAmount(amount, decimals) {
const value = Number(amount);
if (isNaN(value))
return '0.00';
if (value >= 1000000) {
return (value / 1000000).toFixed(2) + 'M';
}
else if (value >= 1000) {
return (value / 1000).toFixed(2) + 'K';
}
return value.toFixed(2); // Changed to always use 2 decimals for consistency
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: Web3UtilsPipe, deps: [], target: i0.ɵɵFactoryTarget.Pipe });
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "19.1.5", ngImport: i0, type: Web3UtilsPipe, isStandalone: true, name: "web3Utils" });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: Web3UtilsPipe, decorators: [{
type: Pipe,
args: [{
name: 'web3Utils',
standalone: true
}]
}] });
class PipesModule {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: PipesModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.1.5", ngImport: i0, type: PipesModule, imports: [CommonModule, Web3UtilsPipe], exports: [Web3UtilsPipe] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: PipesModule, imports: [CommonModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: PipesModule, decorators: [{
type: NgModule,
args: [{
imports: [CommonModule, Web3UtilsPipe],
exports: [Web3UtilsPipe],
}]
}] });
class TableRowContentComponent {
item;
column;
itemValue;
itemTransformed;
isArrayItemValue = false;
ngOnChanges(changes) {
if (changes['itemValue']) {
this.isArrayItemValue = Array.isArray(this.itemValue);
this.itemTransformed = this.column.transform
? this.column.transform(this.itemValue, this.item)
: this.itemValue;
}
}
getStatus(step) {
const [current, total] = step.split('/').map(Number);
if (current === total)
return 'completed';
if (current === 1)
return 'pending';
return 'in-progress';
}
getStatusClass(status) {
const normalizedStatus = typeof status === 'string' ? status.toLowerCase() : status || 'inactive';
switch (normalizedStatus) {
case 'active':
case 'activa':
case 'completed':
case true:
return 'bg-green-100 text-green-800';
case 'inactive':
case 'inactiva':
case 'refused':
case 'rejected':
case 'reject':
case false:
return 'bg-red-100 text-red-800';
case 'pending_to_send':
case 'approved':
return 'bg-blue-100 text-blue-800';
case 'partial':
case 'in progress':
return 'bg-yellow-100 text-yellow-800';
case 'pending':
return 'bg-gray-100 text-gray-800';
default:
return '';
}
}
getStatusClasses(status) {
return {
'px-2 py-1 rounded-full text-xs font-semibold text-nowrap': true,
[this.getStatusClass(status)]: true,
};
}
formatJson(value) {
try {
const parsed = typeof value === 'string' ? JSON.parse(value) : value;
const formatIndentation = (str) => {
const lines = str.split('\n');
let indent = 0;
return lines
.map((line) => {
// Reducir indentación para líneas que cierran bloques
if (line.includes('}') || line.includes(']')) {
indent -= 2;
}
const currentIndent = ' '.repeat(Math.max(0, indent));
const formattedLine = currentIndent + line.trim();
// Aumentar indentación para la siguiente línea si esta abre un bloque
if (line.includes('{') || line.includes('[')) {
indent += 2;
}
return formattedLine;
})
.join('\n');
};
return formatIndentation(JSON.stringify(parsed, null, 2))
.trim()
.replace(/": "/g, '": "') // Espaciado consistente después de los dos puntos
.replace(/,$/gm, ','); // Asegurar que las comas queden en la misma línea
}
catch (e) {
return String(value);
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableRowContentComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.5", type: TableRowContentComponent, isStandalone: true, selector: "table-row-content", inputs: { item: "item", column: "column", itemValue: "itemValue" }, usesOnChanges: true, ngImport: i0, template: `
<ng-container *ngIf="isArrayItemValue; else singleItemTemplate">
<ng-container *ngFor="let item of itemTransformed">
<span class="mr-2 last:mr-0">
<ng-container
*ngTemplateOutlet="contentTemplate; context: { $implicit: item }"
></ng-container>
</span>
</ng-container>
</ng-container>
<ng-template #singleItemTemplate>
<ng-container
*ngTemplateOutlet="
contentTemplate;
context: { $implicit: itemTransformed }
"
></ng-container>
</ng-template>
<ng-template #contentTemplate let-value>
<ng-container [ngSwitch]="column.type || column.key">
<!-- Caso 'step' -->
<ng-container *ngSwitchCase="'step'">
<span
[ngClass]="{
'px-2 py-1 rounded-full text-xs font-semibold': true,
'bg-green-100 text-green-800':
getStatus(item.step) === 'completed',
'bg-yellow-100 text-yellow-800':
getStatus(item.step) === 'in-progress',
'bg-blue-100 text-blue-800': getStatus(item.step) === 'pending'
}"
>
<table-row [text]="value"></table-row>
</span>
</ng-container>
<!-- Caso 'status' -->
<ng-container *ngSwitchCase="'status'">
<span [ngClass]="getStatusClasses(item[column.key])">
<span *ngIf="item[column.key] === undefined">Inactive</span>
<ng-container [ngSwitch]="item.status || item[column.key]">
<span *ngSwitchCase="true">Active</span>
<span *ngSwitchCase="false">Inactive</span>
<span *ngSwitchCase="'partial'">Partial Active</span>
<span *ngSwitchCase="'reject'">Rejected</span>
<span *ngSwitchCase="'pending_to_send'">Pending to send</span>
<span *ngSwitchCase="'pending'">Pending</span>
<span *ngSwitchCase="'approved'">Approved</span>
<table-row
*ngSwitchDefault
[text]="value | titlecase"
></table-row>
</ng-container>
</span>
</ng-container>
<!-- Caso 'HTML' -->
<ng-container *ngSwitchCase="'html'">
<span [innerHTML]="value"></span>
</ng-container>
<!-- Caso 'status:expired' -->
<ng-container *ngSwitchCase="'status:expired'">
<span
[ngClass]="{
'px-2 py-1 rounded-full text-xs font-semibold text-nowrap': true,
'bg-green-100 text-green-800': item[column.key] === true,
'bg-red-100 text-red-800': item[column.key] === false,
}"
>
<ng-container [ngSwitch]="item[column.key]">
<span *ngSwitchCase="true">Active</span>
<span *ngSwitchCase="false">Expired</span>
<table-row
*ngSwitchDefault
[text]="value | titlecase"
></table-row>
</ng-container>
</span>
</ng-container>
<!-- Caso 'state' -->
<ng-container *ngSwitchCase="'state'">
<span
[ngClass]="{
'px-2 py-1 rounded-full text-xs font-semibold text-nowrap': true,
'bg-blue-100 text-blue-800': item.state === 'pre-approved',
'bg-green-100 text-green-800': item.state === 'approved',
'bg-yellow-100 text-yellow-800': item.state === 'pending',
'bg-red-100 text-red-800': item.state === 'rejected'
}"
>
<table-row [text]="value | titlecase"></table-row>
</span>
</ng-container>
<!-- Caso 'date' -->
<ng-container *ngSwitchCase="'date'">
<ng-container *ngIf="value">
<table-row [text]="value | date : 'yyyy-MM-dd'"></table-row>
</ng-container>
</ng-container>
<!-- Caso 'boolean' -->
<ng-container *ngSwitchCase="'boolean'">
{{ value ? 'Sí' : 'No' }}
</ng-container>
<!-- Caso 'date-time' -->
<ng-container *ngSwitchCase="'date-time'">
<ng-container *ngIf="value">
<table-row [text]="value | date : 'dd/MM/yyyy HH:mm'"></table-row>
</ng-container>
</ng-container>
<!-- Caso 'timestamp' -->
<ng-container *ngSwitchCase="'timestamp'">
<ng-container *ngIf="value">
<table-row
[text]="value.toDate() | date : 'dd/MM/yyyy HH:mm'"
></table-row>
</ng-container>
</ng-container>
<!-- Caso 'json' -->
<ng-container *ngSwitchCase="'json'">
<ng-container *ngIf="value">
<pre
class="bg-slate-50 rounded-lg p-2 text-sm font-mono text-slate-700 overflow-x-auto max-h-[150px] overflow-y-auto shadow-sm border border-slate-200"
><code>{{ formatJson(value) }}</code></pre>
</ng-container>
</ng-container>
<!-- Caso 'currency' -->
<ng-container *ngSwitchCase="'currency'">
<table-row
[text]="value | currency : 'USD' : 'symbol' : '1.4-4'"
></table-row>
</ng-container>
<!-- Casos adicionales -->
<ng-container *ngSwitchCase="'uppercase'">
<table-row [text]="value | uppercase"></table-row>
</ng-container>
<ng-container *ngSwitchCase="'titlecase'">
<table-row [text]="value | titlecase"></table-row>
</ng-container>
<ng-container *ngSwitchCase="'wallet'">
<table-row [text]="value | web3Utils : 'wallet'"></table-row>
</ng-container>
<ng-container *ngSwitchCase="'badget'">
<span
class="px-2 py-1 text-nowrap rounded-full text-xs font-semibold bg-gray-100 text-gray-800"
>
<table-row [text]="value"></table-row>
</span>
</ng-container>
<ng-container *ngSwitchCase="'badget:info'">
<span
class="px-2 py-1 rounded-full text-nowrap text-xs font-semibold bg-blue-100 text-blue-800"
>
<table-row [text]="value"></table-row>
</span>
</ng-container>
<ng-container *ngSwitchDefault>
<table-row [text]="value"></table-row>
</ng-container>
</ng-container>
</ng-template>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "pipe", type: i1.UpperCasePipe, name: "uppercase" }, { kind: "pipe", type: i1.TitleCasePipe, name: "titlecase" }, { kind: "pipe", type: i1.CurrencyPipe, name: "currency" }, { kind: "pipe", type: i1.DatePipe, name: "date" }, { kind: "component", type: TableRowComponent, selector: "table-row", inputs: ["text"] }, { kind: "ngmodule", type: PipesModule }, { kind: "pipe", type: Web3UtilsPipe, name: "web3Utils" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: TableRowContentComponent, decorators: [{
type: Component,
args: [{
selector: 'table-row-content',
standalone: true,
imports: [CommonModule, TableRowComponent, PipesModule],
template: `
<ng-container *ngIf="isArrayItemValue; else singleItemTemplate">
<ng-container *ngFor="let item of itemTransformed">
<span class="mr-2 last:mr-0">
<ng-container
*ngTemplateOutlet="contentTemplate; context: { $implicit: item }"
></ng-container>
</span>
</ng-container>
</ng-container>
<ng-template #singleItemTemplate>
<ng-container
*ngTemplateOutlet="
contentTemplate;
context: { $implicit: itemTransformed }
"
></ng-container>
</ng-template>
<ng-template #contentTemplate let-value>
<ng-container [ngSwitch]="column.type || column.key">
<!-- Caso 'step' -->
<ng-container *ngSwitchCase="'step'">
<span
[ngClass]="{
'px-2 py-1 rounded-full text-xs font-semibold': true,
'bg-green-100 text-green-800':
getStatus(item.step) === 'completed',
'bg-yellow-100 text-yellow-800':
getStatus(item.step) === 'in-progress',
'bg-blue-100 text-blue-800': getStatus(item.step) === 'pending'
}"
>
<table-row [text]="value"></table-row>
</span>
</ng-container>
<!-- Caso 'status' -->
<ng-container *ngSwitchCase="'status'">
<span [ngClass]="getStatusClasses(item[column.key])">
<span *ngIf="item[column.key] === undefined">Inactive</span>
<ng-container [ngSwitch]="item.status || item[column.key]">
<span *ngSwitchCase="true">Active</span>
<span *ngSwitchCase="false">Inactive</span>
<span *ngSwitchCase="'partial'">Partial Active</span>
<span *ngSwitchCase="'reject'">Rejected</span>
<span *ngSwitchCase="'pending_to_send'">Pending to send</span>
<span *ngSwitchCase="'pending'">Pending</span>
<span *ngSwitchCase="'approved'">Approved</span>
<table-row
*ngSwitchDefault
[text]="value | titlecase"
></table-row>
</ng-container>
</span>
</ng-container>
<!-- Caso 'HTML' -->
<ng-container *ngSwitchCase="'html'">
<span [innerHTML]="value"></span>
</ng-container>
<!-- Caso 'status:expired' -->
<ng-container *ngSwitchCase="'status:expired'">
<span
[ngClass]="{
'px-2 py-1 rounded-full text-xs font-semibold text-nowrap': true,
'bg-green-100 text-green-800': item[column.key] === true,
'bg-red-100 text-red-800': item[column.key] === false,
}"
>
<ng-container [ngSwitch]="item[column.key]">
<span *ngSwitchCase="true">Active</span>
<span *ngSwitchCase="false">Expired</span>
<table-row
*ngSwitchDefault
[text]="value | titlecase"
></table-row>
</ng-container>
</span>
</ng-container>
<!-- Caso 'state' -->
<ng-container *ngSwitchCase="'state'">
<span
[ngClass]="{
'px-2 py-1 rounded-full text-xs font-semibold text-nowrap': true,
'bg-blue-100 text-blue-800': item.state === 'pre-approved',
'bg-green-100 text-green-800': item.state === 'approved',
'bg-yellow-100 text-yellow-800': item.state === 'pending',
'bg-red-100 text-red-800': item.state === 'rejected'
}"
>
<table-row [text]="value | titlecase"></table-row>
</span>
</ng-container>
<!-- Caso 'date' -->
<ng-container *ngSwitchCase="'date'">
<ng-container *ngIf="value">
<table-row [text]="value | date : 'yyyy-MM-dd'"></table-row>
</ng-container>
</ng-container>
<!-- Caso 'boolean' -->
<ng-container *ngSwitchCase="'boolean'">
{{ value ? 'Sí' : 'No' }}
</ng-container>
<!-- Caso 'date-time' -->
<ng-container *ngSwitchCase="'date-time'">
<ng-container *ngIf="value">
<table-row [text]="value | date : 'dd/MM/yyyy HH:mm'"></table-row>
</ng-container>
</ng-container>
<!-- Caso 'timestamp' -->
<ng-container *ngSwitchCase="'timestamp'">
<ng-container *ngIf="value">
<table-row
[text]="value.toDate() | date : 'dd/MM/yyyy HH:mm'"
></table-row>
</ng-container>
</ng-container>
<!-- Caso 'json' -->
<ng-container *ngSwitchCase="'json'">
<ng-container *ngIf="value">
<pre
class="bg-slate-50 rounded-lg p-2 text-sm font-mono text-slate-700 overflow-x-auto max-h-[150px] overflow-y-auto shadow-sm border border-slate-200"
><code>{{ formatJson(value) }}</code></pre>
</ng-container>
</ng-container>
<!-- Caso 'currency' -->
<ng-container *ngSwitchCase="'currency'">
<table-row
[text]="value | currency : 'USD' : 'symbol' : '1.4-4'"
></table-row>
</ng-container>
<!-- Casos adicionales -->
<ng-container *ngSwitchCase="'uppercase'">
<table-row [text]="value | uppercase"></table-row>
</ng-container>
<ng-container *ngSwitchCase="'titlecase'">
<table-row [text]="value | titlecase"></table-row>
</ng-container>
<ng-container *ngSwitchCase="'wallet'">
<table-row [text]="value | web3Utils : 'wallet'"></table-row>
</ng-container>
<ng-container *ngSwitchCase="'badget'">
<span
class="px-2 py-1 text-nowrap rounded-full text-xs font-semibold bg-gray-100 text-gray-800"
>
<table-row [text]="value"></table-row>
</span>
</ng-container>
<ng-container *ngSwitchCase="'badget:info'">
<span
class="px-2 py-1 rounded-full text-nowrap text-xs font-semibold bg-blue-100 text-blue-800"
>
<table-row [text]="value"></table-row>
</span>
</ng-container>
<ng-container *ngSwitchDefault>
<table-row [text]="value"></table-row>
</ng-container>
</ng-container>
</ng-template>
`,
}]
}], propDecorators: { item: [{
type: Input
}], column: [{
type: Input
}], itemValue: [{
type: Input
}] } });
class SkeletonTableComponent {
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SkeletonTableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.1.5", type: SkeletonTableComponent, isStandalone: true, selector: "ng-table-pg-skeleton", ngImport: i0, template: `
<div class="space-y-6">
<!-- Search and buttons -->
<div
class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
>
<div class="skeleton h-10 w-full sm:w-2/3"></div>
<div class="flex flex-col sm:flex-row gap-2">
<div class="skeleton h-10 w-full sm:w-24"></div>
<div class="skeleton h-10 w-full sm:w-36"></div>
</div>
</div>
<!-- Table Container with overflow -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-full">
<!-- Desktop Headers (hidden on mobile) -->
<thead class="bg-gray-50 hidden md:table-header-group">
<tr>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-8"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-16"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-12"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-20"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-24"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-20"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-16"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-12"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-8"></div>
</th>
</tr>
</thead>
<!-- Mobile Headers (visible only on mobile) -->
<thead class="bg-gray-50 md:hidden">
<tr>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-16"></div>
</th>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-20"></div>
</th>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-12"></div>
</th>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-8"></div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<!-- Desktop Rows (hidden on mobile) -->
@for (row of [1,2,3,4,5,6,7,8,9,10]; track row) {
<tr class="border-t border-gray-200 hidden md:table-row">
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-8"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-20"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-6 w-8 rounded-full"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-32"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-28"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-24"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-16"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-6 w-16 rounded-full"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-6 w-6"></div>
</td>
</tr>
}
<!-- Mobile Rows (visible only on mobile) -->
@for (row of [1,2,3,4,5,6,7,8,9,10]; track row) {
<tr class="border-t border-gray-200 md:hidden">
<td class="px-3 py-4">
<div class="skeleton h-4 w-16"></div>
</td>
<td class="px-3 py-4">
<div class="skeleton h-4 w-20"></div>
</td>
<td class="px-3 py-4">
<div class="skeleton h-6 w-16 rounded-full"></div>
</td>
<td class="px-3 py-4">
<div class="skeleton h-6 w-6"></div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"
>
<div class="flex items-center gap-2">
<div class="skeleton h-4 w-12"></div>
<div class="skeleton h-8 w-12"></div>
<div class="skeleton h-4 w-16"></div>
</div>
<div class="flex items-center justify-center sm:justify-end gap-2">
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-4 w-4"></div>
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-8 w-8"></div>
</div>
</div>
</div>
`, isInline: true, styles: ["ng-table-pg-skeleton .skeleton{background-color:#e5e7eb!important;animation:ng-skeleton-pulse 2s cubic-bezier(.4,0,.6,1) infinite!important;border-radius:.25rem!important}@keyframes ng-skeleton-pulse{0%,to{opacity:1}50%{opacity:.5}}ng-table-pg-skeleton .flex{display:flex!important}ng-table-pg-skeleton .flex-col{flex-direction:column!important}ng-table-pg-skeleton .gap-2{gap:.5rem!important}ng-table-pg-skeleton .gap-4{gap:1rem!important}ng-table-pg-skeleton .justify-center{justify-content:center!important}ng-table-pg-skeleton .justify-between{justify-content:space-between!important}ng-table-pg-skeleton .items-center{align-items:center!important}ng-table-pg-skeleton .overflow-x-auto{overflow-x:auto!important}ng-table-pg-skeleton .min-w-full{min-width:100%!important}ng-table-pg-skeleton .w-full{width:100%!important}ng-table-pg-skeleton .hidden{display:none!important}@media (min-width: 640px){ng-table-pg-skeleton .sm\\:flex-row{flex-direction:row!important}ng-table-pg-skeleton .sm\\:justify-between{justify-content:space-between!important}ng-table-pg-skeleton .sm\\:items-center{align-items:center!important}ng-table-pg-skeleton .sm\\:justify-end{justify-content:flex-end!important}ng-table-pg-skeleton .sm\\:w-2\\/3{width:66.666667%!important}ng-table-pg-skeleton .sm\\:w-24{width:6rem!important}ng-table-pg-skeleton .sm\\:w-36{width:9rem!important}ng-table-pg-skeleton .sm\\:px-6{padding-left:1.5rem!important;padding-right:1.5rem!important}}@media (min-width: 768px){ng-table-pg-skeleton .md\\:hidden{display:none!important}ng-table-pg-skeleton .md\\:table-header-group{display:table-header-group!important}ng-table-pg-skeleton .md\\:table-row{display:table-row!important}}ng-table-pg-skeleton .space-y-6>*+*{margin-top:1.5rem!important}ng-table-pg-skeleton .px-3{padding-left:.75rem!important;padding-right:.75rem!important}ng-table-pg-skeleton .py-3{padding-top:.75rem!important;padding-bottom:.75rem!important}ng-table-pg-skeleton .py-4{padding-top:1rem!important;padding-bottom:1rem!important}ng-table-pg-skeleton .text-left{text-align:left!important}ng-table-pg-skeleton .bg-white{background-color:#fff!important}ng-table-pg-skeleton .bg-gray-50{background-color:#f9fafb!important}ng-table-pg-skeleton .rounded-lg{border-radius:.5rem!important}ng-table-pg-skeleton .rounded-full{border-radius:9999px!important}ng-table-pg-skeleton .shadow{box-shadow:0 1px 3px #0000001a,0 1px 2px -1px #0000001a!important}ng-table-pg-skeleton .overflow-hidden{overflow:hidden!important}ng-table-pg-skeleton .border-t{border-top-width:1px!important}ng-table-pg-skeleton .border-gray-200{border-color:#e5e7eb!important}ng-table-pg-skeleton .divide-y>*+*{border-top-width:1px!important}ng-table-pg-skeleton .divide-gray-200>*+*{border-color:#e5e7eb!important}ng-table-pg-skeleton .h-4{height:1rem!important}ng-table-pg-skeleton .h-6{height:1.5rem!important}ng-table-pg-skeleton .h-8{height:2rem!important}ng-table-pg-skeleton .h-10{height:2.5rem!important}ng-table-pg-skeleton .w-4{width:1rem!important}ng-table-pg-skeleton .w-6{width:1.5rem!important}ng-table-pg-skeleton .w-8{width:2rem!important}ng-table-pg-skeleton .w-12{width:3rem!important}ng-table-pg-skeleton .w-16{width:4rem!important}ng-table-pg-skeleton .w-20{width:5rem!important}ng-table-pg-skeleton .w-24{width:6rem!important}ng-table-pg-skeleton .w-28{width:7rem!important}ng-table-pg-skeleton .w-32{width:8rem!important}\n"], encapsulation: i0.ViewEncapsulation.None });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: SkeletonTableComponent, decorators: [{
type: Component,
args: [{ selector: 'ng-table-pg-skeleton', standalone: true, encapsulation: ViewEncapsulation.None, template: `
<div class="space-y-6">
<!-- Search and buttons -->
<div
class="flex flex-col sm:flex-row sm:justify-between sm:items-center gap-4"
>
<div class="skeleton h-10 w-full sm:w-2/3"></div>
<div class="flex flex-col sm:flex-row gap-2">
<div class="skeleton h-10 w-full sm:w-24"></div>
<div class="skeleton h-10 w-full sm:w-36"></div>
</div>
</div>
<!-- Table Container with overflow -->
<div class="bg-white rounded-lg shadow overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full min-w-full">
<!-- Desktop Headers (hidden on mobile) -->
<thead class="bg-gray-50 hidden md:table-header-group">
<tr>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-8"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-16"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-12"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-20"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-24"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-20"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-16"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-12"></div>
</th>
<th class="px-3 sm:px-6 py-3 text-left">
<div class="skeleton h-4 w-8"></div>
</th>
</tr>
</thead>
<!-- Mobile Headers (visible only on mobile) -->
<thead class="bg-gray-50 md:hidden">
<tr>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-16"></div>
</th>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-20"></div>
</th>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-12"></div>
</th>
<th class="px-3 py-3 text-left">
<div class="skeleton h-4 w-8"></div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200">
<!-- Desktop Rows (hidden on mobile) -->
@for (row of [1,2,3,4,5,6,7,8,9,10]; track row) {
<tr class="border-t border-gray-200 hidden md:table-row">
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-8"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-20"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-6 w-8 rounded-full"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-32"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-28"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-24"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-4 w-16"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-6 w-16 rounded-full"></div>
</td>
<td class="px-3 sm:px-6 py-4">
<div class="skeleton h-6 w-6"></div>
</td>
</tr>
}
<!-- Mobile Rows (visible only on mobile) -->
@for (row of [1,2,3,4,5,6,7,8,9,10]; track row) {
<tr class="border-t border-gray-200 md:hidden">
<td class="px-3 py-4">
<div class="skeleton h-4 w-16"></div>
</td>
<td class="px-3 py-4">
<div class="skeleton h-4 w-20"></div>
</td>
<td class="px-3 py-4">
<div class="skeleton h-6 w-16 rounded-full"></div>
</td>
<td class="px-3 py-4">
<div class="skeleton h-6 w-6"></div>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- Pagination -->
<div
class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4"
>
<div class="flex items-center gap-2">
<div class="skeleton h-4 w-12"></div>
<div class="skeleton h-8 w-12"></div>
<div class="skeleton h-4 w-16"></div>
</div>
<div class="flex items-center justify-center sm:justify-end gap-2">
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-4 w-4"></div>
<div class="skeleton h-8 w-8"></div>
<div class="skeleton h-8 w-8"></div>
</div>
</div>
</div>
`, styles: ["ng-table-pg-skeleton .skeleton{background-color:#e5e7eb!important;animation:ng-skeleton-pulse 2s cubic-bezier(.4,0,.6,1) infinite!important;border-radius:.25rem!important}@keyframes ng-skeleton-pulse{0%,to{opacity:1}50%{opacity:.5}}ng-table-pg-skeleton .flex{display:flex!important}ng-table-pg-skeleton .flex-col{flex-direction:column!important}ng-table-pg-skeleton .gap-2{gap:.5rem!important}ng-table-pg-skeleton .gap-4{gap:1rem!important}ng-table-pg-skeleton .justify-center{justify-content:center!important}ng-table-pg-skeleton .justify-between{justify-content:space-between!important}ng-table-pg-skeleton .items-center{align-items:center!important}ng-table-pg-skeleton .overflow-x-auto{overflow-x:auto!important}ng-table-pg-skeleton .min-w-full{min-width:100%!important}ng-table-pg-skeleton .w-full{width:100%!important}ng-table-pg-skeleton .hidden{display:none!important}@media (min-width: 640px){ng-table-pg-skeleton .sm\\:flex-row{flex-direction:row!important}ng-table-pg-skeleton .sm\\:justify-between{justify-content:space-between!important}ng-table-pg-skeleton .sm\\:items-center{align-items:center!important}ng-table-pg-skeleton .sm\\:justify-end{justify-content:flex-end!important}ng-table-pg-skeleton .sm\\:w-2\\/3{width:66.666667%!important}ng-table-pg-skeleton .sm\\:w-24{width:6rem!important}ng-table-pg-skeleton .sm\\:w-36{width:9rem!important}ng-table-pg-skeleton .sm\\:px-6{padding-left:1.5rem!important;padding-right:1.5rem!important}}@media (min-width: 768px){ng-table-pg-skeleton .md\\:hidden{display:none!important}ng-table-pg-skeleton .md\\:table-header-group{display:table-header-group!important}ng-table-pg-skeleton .md\\:table-row{display:table-row!important}}ng-table-pg-skeleton .space-y-6>*+*{margin-top:1.5rem!important}ng-table-pg-skeleton .px-3{padding-left:.75rem!important;padding-right:.75rem!important}ng-table-pg-skeleton .py-3{padding-top:.75rem!important;padding-bottom:.75rem!important}ng-table-pg-skeleton .py-4{padding-top:1rem!important;padding-bottom:1rem!important}ng-table-pg-skeleton .text-left{text-align:left!important}ng-table-pg-skeleton .bg-white{background-color:#fff!important}ng-table-pg-skeleton .bg-gray-50{background-color:#f9fafb!important}ng-table-pg-skeleton .rounded-lg{border-radius:.5rem!important}ng-table-pg-skeleton .rounded-full{border-radius:9999px!important}ng-table-pg-skeleton .shadow{box-shadow:0 1px 3px #0000001a,0 1px 2px -1px #0000001a!important}ng-table-pg-skeleton .overflow-hidden{overflow:hidden!important}ng-table-pg-skeleton .border-t{border-top-width:1px!important}ng-table-pg-skeleton .border-gray-200{border-color:#e5e7eb!important}ng-table-pg-skeleton .divide-y>*+*{border-top-width:1px!important}ng-table-pg-skeleton .divide-gray-200>*+*{border-color:#e5e7eb!important}ng-table-pg-skeleton .h-4{height:1rem!important}ng-table-pg-skeleton .h-6{height:1.5rem!important}ng-table-pg-skeleton .h-8{height:2rem!important}ng-table-pg-skeleton .h-10{height:2.5rem!important}ng-table-pg-skeleton .w-4{width:1rem!important}ng-table-pg-skeleton .w-6{width:1.5rem!important}ng-table-pg-skeleton .w-8{width:2rem!important}ng-table-pg-skeleton .w-12{width:3rem!important}ng-table-pg-skeleton .w-16{width:4rem!important}ng-table-pg-skeleton .w-20{width:5rem!important}ng-table-pg-skeleton .w-24{width:6rem!important}ng-table-pg-skeleton .w-28{width:7rem!important}ng-table-pg-skeleton .w-32{width:8rem!important}\n"] }]
}] });
class DropdownButtonComponent {
elementRef;
cdr;
dropdownButton;
dropdownContent;
buttonClass = 'text-gray-400 hover:text-gray-600';
isOpen = false;
opened = new EventEmitter();
closed = new EventEmitter();
dropdownStyle = {
top: `-9999px`,
left: `-9999px`,
};
destroy$ = new Subject();
constructor(elementRef, cdr) {
this.elementRef = elementRef;
this.cdr = cdr;
}
ngAfterViewInit() {
this.updateDropdownPosition(); // Initial position
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
clickOutside(event) {
if (!this.elementRef.nativeElement.contains(event.target))
this.closeDropdown();
}
onScroll() {
if (this.isOpen) {
this.updateDropdownPosition();
}
}
toggleDropdown(event) {
event.stopPropagation();
this.isOpen ? this.closeDropdown() : this.openDropdown();
}
openDropdown() {
this.opened.emit();
this.isOpen = true;
setTimeout(() => this.updateDropdownPosition(), 10);
}
closeDropdown() {
this.dropdownStyle = {
top: `-9999px`,
left: `-9999px`,
};
this.isOpen = false;
this.closed.emit();
}
updateDropdownPosition() {
if (!this.dropdownButton || !this.dropdownContent)
return;
const buttonRect = this.dropdownButton.nativeElement.getBoundingClientRect();
const dropdownRect = this.dropdownContent.nativeElement.getBoundingClientRect();
const scrollableContainer = this.findScrollableContainer(this.dropdownButton.nativeElement);
let top = buttonRect.bottom + 10;
let left = buttonRect.right - dropdownRect.width;
if (scrollableContainer) {
const containerRect = scrollableContainer.getBoundingClientRect();
if (top + dropdownRect.height > window.innerHeight) {
top = buttonRect.top - dropdownRect.height - 10;
if (top < 10)
top = 80;
}
if (top + dropdownRect.height <= window.innerHeight &&
top + dropdownRect.height > containerRect.bottom) {
top = buttonRect.top - dropdownRect.height - 10;
if (top < containerRect.top)
top = containerRect.top;
}
if (left < containerRect.left) {
left = containerRect.left;
}
}
else {
if (top + dropdownRect.height > window.innerHeight) {
top = buttonRect.top - dropdownRect.height - 10;
if (top < 10)
top = 80;
}
if (left < 0) {
left = 0;
}
if (left + dropdownRect.width > window.innerWidth) {
left = window.innerWidth - dropdownRect.width;
}
}
this.dropdownStyle = {
top: `${top}px`,
left: `${left}px`,
overflowY: 'auto',
overflowX: 'hidden',
};
this.cdr.detectChanges();
}
findScrollableContainer(element) {
while (element && element !== document.body) {
const style = window.getComputedStyle(element);
const overflow = style.getPropertyValue('overflow') +
style.getPropertyValue('overflow-y') +
style.getPropertyValue('overflow-x');
if (/(auto|scroll)/.test(overflow) &&
(element.scrollHeight > element.clientHeight ||
element.scrollWidth > element.clientWidth)) {
return element;
}
element = element.parentElement;
}
return null;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: DropdownButtonComponent, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "19.1.5", type: DropdownButtonComponent, isStandalone: true, selector: "app-dropdown-button", inputs: { buttonClass: "buttonClass", isOpen: "isOpen" }, outputs: { opened: "opened", closed: "closed" }, host: { listeners: { "document:click": "clickOutside($event)", "window:scroll": "onScroll()" } }, viewQueries: [{ propertyName: "dropdownButton", first: true, predicate: ["dropdownButton"], descendants: true }, { propertyName: "dropdownContent", first: true, predicate: ["dropdownContent"], descendants: true }], ngImport: i0, template: `
<div class="relative inline-block">
<button
#dropdownButton
(click)="toggleDropdown($event)"
[class]="buttonClass"
>
<ng-content select="[buttonContent]"></ng-content>
</button>
<div
*ngIf="isOpen"
#dropdownContent
[ngStyle]="dropdownStyle"
[ngClass]="{ invisible: dropdownStyle.top == '-9999px' }"
class="fixed rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50"
(click)="$event.stopPropagation()"
>
<div class="py-1">
<ng-content select="[dropdownContent]"></ng-content>
</div>
</div>
</div>
`, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.1.5", ngImport: i0, type: DropdownButtonComponent, decorators: [{
type: Component,
args: [{
selector: 'app-dropdown-button',
standalone: true,
imports: [CommonModule],
template: `
<div class="relative inline-block">
<button
#dropdownButton
(click)="toggleDropdown($event)"
[class]="buttonClass"
>
<ng-content select="[buttonContent]"></ng-content>
</button>
<div
*ngIf="isOpen"
#dropdownContent
[ngStyle]="dropdownStyle"
[ngClass]="{ invisible: dropdownStyle.top == '-9999px' }"
class="fixed rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-50"
(click)="$event.stopPropagation()"
>
<div class="py-1">
<ng-content select="[dropdownContent]"></ng-content>
</div>
</div>
</div>
`,
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.ChangeDetectorRef }], propDecorators: { dropdownButton: [{
type: ViewChild,
args: ['dropdownButton']
}], dropdownContent: [{
type: ViewChild,
args: ['dropdownContent']
}], buttonClass: [{
type: Input
}], isOpen: [{
type: Input
}], opened: [{
type: Output
}], closed: [{
type: Output
}], clickOutside: [{
type: HostListener,
args: ['document:click', ['$event']]
}], onScroll: [{
type: HostListener,
args: ['window:scroll']
}] } });
class TableComponent {
translate;
data = [];