ngxsmk-datatable
Version:
A powerful, feature-rich Angular datatable component with virtual scrolling, built for Angular 17+
1,029 lines (1,022 loc) • 439 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, SecurityContext, Pipe, EventEmitter, Output, Input, Component, ContentChild, ViewChild, Directive } from '@angular/core';
import * as i6 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i1$1 from '@angular/forms';
import { FormsModule } from '@angular/forms';
import { BehaviorSubject, Subject, fromEvent, interval, map as map$1, distinctUntilChanged as distinctUntilChanged$1, combineLatest, throwError, Observable } from 'rxjs';
import { debounceTime, map, distinctUntilChanged, takeUntil, retry, catchError } from 'rxjs/operators';
import * as i1 from '@angular/platform-browser';
import { HttpParams } from '@angular/common/http';
class VirtualScrollService {
constructor() {
this.stateSubject = new BehaviorSubject({
startIndex: 0,
endIndex: 0,
visibleItems: [],
totalHeight: 0,
scrollTop: 0
});
this.state$ = this.stateSubject.asObservable();
}
calculateVisibleItems(items, containerHeight, itemHeight, scrollTop, bufferSize = 5) {
const visibleItemCount = Math.ceil(containerHeight / itemHeight);
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
const endIndex = Math.min(items.length - 1, startIndex + visibleItemCount + bufferSize);
const visibleItems = items.slice(startIndex, endIndex + 1);
const totalHeight = items.length * itemHeight;
const state = {
startIndex,
endIndex,
visibleItems,
totalHeight,
scrollTop
};
this.stateSubject.next(state);
return state;
}
scrollToItem(itemIndex, itemHeight, containerHeight) {
const maxScrollTop = Math.max(0, (itemIndex + 1) * itemHeight - containerHeight);
return Math.min(itemIndex * itemHeight, maxScrollTop);
}
getItemOffset(itemIndex, itemHeight) {
return itemIndex * itemHeight;
}
isItemVisible(itemIndex, startIndex, endIndex) {
return itemIndex >= startIndex && itemIndex <= endIndex;
}
getVisibleRange(scrollTop, containerHeight, itemHeight, totalItems, bufferSize = 5) {
const visibleItemCount = Math.ceil(containerHeight / itemHeight);
const start = Math.max(0, Math.floor(scrollTop / itemHeight) - bufferSize);
const end = Math.min(totalItems - 1, start + visibleItemCount + bufferSize);
return { start, end };
}
calculateScrollPosition(targetIndex, itemHeight, containerHeight) {
const itemOffset = targetIndex * itemHeight;
const maxScrollTop = Math.max(0, (targetIndex + 1) * itemHeight - containerHeight);
return Math.min(itemOffset, maxScrollTop);
}
getSpacerHeight(startIndex, endIndex, itemHeight, totalItems) {
const topHeight = startIndex * itemHeight;
const bottomHeight = Math.max(0, (totalItems - endIndex - 1) * itemHeight);
return { top: topHeight, bottom: bottomHeight };
}
// ============================================
// Horizontal Virtual Scrolling Methods
// ============================================
/**
* Calculate visible columns based on horizontal scroll position
*/
calculateVisibleColumns(columns, columnWidths, containerWidth, scrollLeft, bufferSize = 2) {
if (!columns || columns.length === 0) {
return {
startColumnIndex: 0,
endColumnIndex: 0,
visibleColumns: [],
totalWidth: 0,
scrollLeft: 0,
leftOffset: 0
};
}
// Calculate cumulative widths
const cumulativeWidths = [];
let totalWidth = 0;
for (let i = 0; i < columns.length; i++) {
const col = columns[i];
const width = columnWidths[col.id] || col.width || 150;
totalWidth += width;
cumulativeWidths.push(totalWidth);
}
// Find start column index
let startColumnIndex = 0;
for (let i = 0; i < cumulativeWidths.length; i++) {
if (cumulativeWidths[i] > scrollLeft) {
startColumnIndex = Math.max(0, i - bufferSize);
break;
}
}
// Find end column index
const visibleWidth = scrollLeft + containerWidth;
let endColumnIndex = columns.length - 1;
for (let i = startColumnIndex; i < cumulativeWidths.length; i++) {
if (cumulativeWidths[i] > visibleWidth) {
endColumnIndex = Math.min(columns.length - 1, i + bufferSize);
break;
}
}
// Calculate left offset for visible columns
const leftOffset = startColumnIndex > 0 ? cumulativeWidths[startColumnIndex - 1] : 0;
// Get visible columns
const visibleColumns = columns.slice(startColumnIndex, endColumnIndex + 1);
return {
startColumnIndex,
endColumnIndex,
visibleColumns,
totalWidth,
scrollLeft,
leftOffset
};
}
/**
* Scroll to a specific column
*/
scrollToColumn(columnIndex, columnWidths, columns, containerWidth) {
if (columnIndex < 0 || columnIndex >= columns.length) {
return 0;
}
let targetScrollLeft = 0;
for (let i = 0; i < columnIndex; i++) {
const col = columns[i];
const width = columnWidths[col.id] || col.width || 150;
targetScrollLeft += width;
}
return targetScrollLeft;
}
/**
* Get spacer widths for horizontal virtual scrolling
*/
getHorizontalSpacerWidth(startColumnIndex, endColumnIndex, columnWidths, columns) {
let leftWidth = 0;
for (let i = 0; i < startColumnIndex; i++) {
const col = columns[i];
const width = columnWidths[col.id] || col.width || 150;
leftWidth += width;
}
let rightWidth = 0;
for (let i = endColumnIndex + 1; i < columns.length; i++) {
const col = columns[i];
const width = columnWidths[col.id] || col.width || 150;
rightWidth += width;
}
return { left: leftWidth, right: rightWidth };
}
/**
* Check if a column is currently visible in the viewport
*/
isColumnVisible(columnIndex, startColumnIndex, endColumnIndex) {
return columnIndex >= startColumnIndex && columnIndex <= endColumnIndex;
}
/**
* Get total width of all columns
*/
getTotalColumnsWidth(columns, columnWidths) {
let totalWidth = 0;
for (const col of columns) {
const width = columnWidths[col.id] || col.width || 150;
totalWidth += width;
}
return totalWidth;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VirtualScrollService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VirtualScrollService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: VirtualScrollService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class SafeHtmlPipe {
constructor(sanitizer) {
this.sanitizer = sanitizer;
}
transform(value) {
if (value == null) {
return null;
}
const stringValue = String(value);
return this.sanitizer.sanitize(SecurityContext.HTML, stringValue);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SafeHtmlPipe, deps: [{ token: i1.DomSanitizer }], target: i0.ɵɵFactoryTarget.Pipe }); }
static { this.ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: SafeHtmlPipe, isStandalone: true, name: "safeHtml" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SafeHtmlPipe, decorators: [{
type: Pipe,
args: [{
name: 'safeHtml',
standalone: true
}]
}], ctorParameters: () => [{ type: i1.DomSanitizer }] });
class NgxsmkPagerComponent {
constructor(cdr) {
this.cdr = cdr;
this.config = null;
this.currentPage = 1;
this.pageSize = 10;
this.totalItems = 0;
this.showRefreshButton = false; // PR #2184
this.pageChange = new EventEmitter();
this.refresh = new EventEmitter(); // PR #2184
// Computed properties
this.totalPages = 0;
this.startIndex = 0;
this.endIndex = 0;
this.hasNextPage = false;
this.hasPreviousPage = false;
this.pageNumbers = [];
}
ngOnInit() {
this.calculatePagination();
}
ngOnChanges(changes) {
if (changes['currentPage'] || changes['pageSize'] || changes['totalItems']) {
this.calculatePagination();
}
}
calculatePagination() {
this.totalPages = Math.ceil(this.totalItems / this.pageSize);
this.startIndex = (this.currentPage - 1) * this.pageSize + 1; // +1 for 1-based display
this.endIndex = Math.min(this.startIndex + this.pageSize - 1, this.totalItems);
this.hasNextPage = this.currentPage < this.totalPages;
this.hasPreviousPage = this.currentPage > 1;
this.generatePageNumbers();
this.cdr.markForCheck();
}
generatePageNumbers() {
const maxSize = this.config?.maxSize || 5;
const totalPages = this.totalPages;
const currentPage = this.currentPage;
if (totalPages <= maxSize) {
this.pageNumbers = Array.from({ length: totalPages }, (_, i) => i + 1);
return;
}
const halfMaxSize = Math.floor(maxSize / 2);
let startPage = Math.max(1, currentPage - halfMaxSize);
let endPage = Math.min(totalPages, startPage + maxSize - 1);
if (endPage - startPage + 1 < maxSize) {
startPage = Math.max(1, endPage - maxSize + 1);
}
this.pageNumbers = Array.from({ length: endPage - startPage + 1 }, (_, i) => startPage + i);
}
onPageChange(page) {
if (page >= 1 && page <= this.totalPages && page !== this.currentPage) {
const previousPage = this.currentPage;
this.pageChange.emit({
page,
pageSize: this.pageSize,
length: this.totalItems,
pageIndex: page - 1,
previousPageIndex: previousPage - 1
});
}
}
onPageSizeChange(newPageSize) {
const pageSizeNum = Number(newPageSize);
if (pageSizeNum !== this.pageSize) {
const previousPage = this.currentPage;
this.pageChange.emit({
page: 1,
pageSize: pageSizeNum,
length: this.totalItems,
pageIndex: 0,
previousPageIndex: previousPage - 1
});
}
}
goToFirstPage() {
this.onPageChange(1);
}
goToLastPage() {
this.onPageChange(this.totalPages);
}
// PR #2184: Refresh button handler
onRefresh() {
this.refresh.emit();
}
goToPreviousPage() {
if (this.hasPreviousPage) {
this.onPageChange(this.currentPage - 1);
}
}
goToNextPage() {
if (this.hasNextPage) {
this.onPageChange(this.currentPage + 1);
}
}
getPageSizeOptions() {
return this.config?.pageSizeOptions || [10, 25, 50, 100];
}
getRangeLabel() {
if (this.totalItems === 0) {
return '0 of 0';
}
return `${this.startIndex}-${this.endIndex} of ${this.totalItems}`;
}
getTotalItemsLabel() {
return `${this.totalItems} items`;
}
isPageNumberVisible(page) {
return this.pageNumbers.includes(page);
}
shouldShowEllipsisBefore() {
return this.pageNumbers.length > 0 && this.pageNumbers[0] > 1;
}
shouldShowEllipsisAfter() {
return this.pageNumbers.length > 0 && this.pageNumbers[this.pageNumbers.length - 1] < this.totalPages;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxsmkPagerComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "17.3.12", type: NgxsmkPagerComponent, isStandalone: true, selector: "ngxsmk-pager", inputs: { config: "config", currentPage: "currentPage", pageSize: "pageSize", totalItems: "totalItems", showRefreshButton: "showRefreshButton" }, outputs: { pageChange: "pageChange", refresh: "refresh" }, usesOnChanges: true, ngImport: i0, template: "@if (config) {\r\n <div class=\"ngxsmk-pager\">\r\n \r\n <!-- Page size selector -->\r\n @if (config.showPageSizeOptions) {\r\n <div class=\"ngxsmk-pager__page-size\">\r\n <label class=\"ngxsmk-pager__page-size-label\">Items per page:</label>\r\n <select \r\n class=\"ngxsmk-pager__page-size-select\"\r\n [value]=\"pageSize\"\r\n (change)=\"onPageSizeChange($any($event.target).value)\"\r\n >\r\n @for (size of getPageSizeOptions(); track size) {\r\n <option [value]=\"size\">\r\n {{ size }}\r\n </option>\r\n }\r\n </select>\r\n </div>\r\n }\r\n\r\n <!-- Range information -->\r\n @if (config.showRangeLabels) {\r\n <div class=\"ngxsmk-pager__range\">\r\n <span class=\"ngxsmk-pager__range-text\">{{ getRangeLabel() }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Total items -->\r\n @if (config.showTotalItems) {\r\n <div class=\"ngxsmk-pager__total\">\r\n <span class=\"ngxsmk-pager__total-text\">{{ getTotalItemsLabel() }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Navigation controls -->\r\n <div class=\"ngxsmk-pager__navigation\">\r\n \r\n <!-- First page button -->\r\n @if (config.showFirstLastButtons) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--first\"\r\n [disabled]=\"!hasPreviousPage\"\r\n (click)=\"goToFirstPage()\"\r\n title=\"First page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u23EE</i>\r\n </button>\r\n }\r\n\r\n <!-- Previous page button -->\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--previous\"\r\n [disabled]=\"!hasPreviousPage\"\r\n (click)=\"goToPreviousPage()\"\r\n title=\"Previous page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u25C0</i>\r\n </button>\r\n\r\n <!-- Page numbers -->\r\n <div class=\"ngxsmk-pager__page-numbers\">\r\n \r\n <!-- Ellipsis before -->\r\n @if (shouldShowEllipsisBefore()) {\r\n <span class=\"ngxsmk-pager__ellipsis\">\r\n ...\r\n </span>\r\n }\r\n\r\n <!-- Page number buttons -->\r\n @for (page of pageNumbers; track page) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--page\"\r\n [class.ngxsmk-pager__button--active]=\"page === currentPage\"\r\n (click)=\"onPageChange(page)\"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n\r\n <!-- Ellipsis after -->\r\n @if (shouldShowEllipsisAfter()) {\r\n <span class=\"ngxsmk-pager__ellipsis\">\r\n ...\r\n </span>\r\n }\r\n\r\n </div>\r\n\r\n <!-- Next page button -->\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--next\"\r\n [disabled]=\"!hasNextPage\"\r\n (click)=\"goToNextPage()\"\r\n title=\"Next page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u25B6</i>\r\n </button>\r\n\r\n <!-- Last page button -->\r\n @if (config.showFirstLastButtons) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--last\"\r\n [disabled]=\"!hasNextPage\"\r\n (click)=\"goToLastPage()\"\r\n title=\"Last page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u23ED</i>\r\n </button>\r\n }\r\n\r\n </div>\r\n\r\n <!-- Refresh button (PR #2184) -->\r\n @if (showRefreshButton) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--refresh\"\r\n (click)=\"onRefresh()\"\r\n title=\"Refresh\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u21BB</i>\r\n </button>\r\n }\r\n\r\n </div>\r\n}\r\n", styles: [".ngxsmk-pager{display:flex;align-items:center;justify-content:space-between;padding-inline:12px;background:transparent;font-size:14px;color:#374151;min-height:50px;gap:16px;min-width:100%;width:max-content;box-sizing:border-box}.ngxsmk-pager__page-size{display:flex;align-items:center;gap:12px}.ngxsmk-pager__page-size-label{font-weight:500;color:#6b7280;white-space:nowrap}.ngxsmk-pager__page-size-select{padding:6px 10px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:14px;cursor:pointer;transition:all .2s ease;color:#374151;min-width:70px}.ngxsmk-pager__page-size-select:hover{border-color:#9ca3af;background:#f9fafb}.ngxsmk-pager__page-size-select:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.ngxsmk-pager__range-text,.ngxsmk-pager__total-text{color:#6b7280;font-weight:500;white-space:nowrap}.ngxsmk-pager__navigation{display:flex;align-items:center;gap:8px}.ngxsmk-pager__page-numbers{display:flex;align-items:center;gap:4px}.ngxsmk-pager__ellipsis{padding:8px 4px;color:#9ca3af;font-weight:600}.ngxsmk-pager__button{display:flex;align-items:center;justify-content:center;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#374151;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;-webkit-user-select:none;user-select:none;min-width:36px;height:36px;padding:0 10px}.ngxsmk-pager__button:hover:not(:disabled){background:#f9fafb;border-color:#9ca3af;color:#374151;box-shadow:0 1px 2px #0000000d}.ngxsmk-pager__button:active:not(:disabled){background:#f3f4f6;transform:translateY(1px)}.ngxsmk-pager__button:disabled{opacity:.4;cursor:not-allowed;background:#f9fafb;border-color:#e5e7eb;color:#9ca3af;box-shadow:none}.ngxsmk-pager__button--active{background:#3b82f6;border-color:#3b82f6;color:#fff;box-shadow:0 1px 2px #0000000d;font-weight:600}.ngxsmk-pager__button--active:hover{background:#2563eb;border-color:#2563eb;box-shadow:0 1px 3px #0000001a;color:#fff}.ngxsmk-pager__button--active:active{background:#1d4ed8;border-color:#1d4ed8}.ngxsmk-pager__button--first,.ngxsmk-pager__button--last,.ngxsmk-pager__button--previous,.ngxsmk-pager__button--next{min-width:40px}.ngxsmk-pager__button--page{min-width:36px}.ngxsmk-pager__icon{font-size:12px;font-weight:700}@media (max-width: 768px){.ngxsmk-pager{flex-direction:column;gap:12px;padding:12px}.ngxsmk-pager__navigation{order:1}.ngxsmk-pager__page-size,.ngxsmk-pager__range,.ngxsmk-pager__total{order:2}.ngxsmk-pager__page-numbers{flex-wrap:wrap;justify-content:center}.ngxsmk-pager__button{min-width:32px;height:32px;font-size:13px;padding:0 8px}.ngxsmk-pager__page-size,.ngxsmk-pager__range,.ngxsmk-pager__total{font-size:13px}}@media (max-width: 480px){.ngxsmk-pager{padding:8px;gap:8px}.ngxsmk-pager__button{min-width:28px;height:28px;font-size:12px;padding:0 6px}.ngxsmk-pager__button--first,.ngxsmk-pager__button--last,.ngxsmk-pager__button--previous,.ngxsmk-pager__button--next{min-width:32px}.ngxsmk-pager__page-size,.ngxsmk-pager__range,.ngxsmk-pager__total{font-size:12px}.ngxsmk-pager__ellipsis{padding:6px 2px}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: NgxsmkPagerComponent, decorators: [{
type: Component,
args: [{ selector: 'ngxsmk-pager', standalone: true, imports: [CommonModule, FormsModule], template: "@if (config) {\r\n <div class=\"ngxsmk-pager\">\r\n \r\n <!-- Page size selector -->\r\n @if (config.showPageSizeOptions) {\r\n <div class=\"ngxsmk-pager__page-size\">\r\n <label class=\"ngxsmk-pager__page-size-label\">Items per page:</label>\r\n <select \r\n class=\"ngxsmk-pager__page-size-select\"\r\n [value]=\"pageSize\"\r\n (change)=\"onPageSizeChange($any($event.target).value)\"\r\n >\r\n @for (size of getPageSizeOptions(); track size) {\r\n <option [value]=\"size\">\r\n {{ size }}\r\n </option>\r\n }\r\n </select>\r\n </div>\r\n }\r\n\r\n <!-- Range information -->\r\n @if (config.showRangeLabels) {\r\n <div class=\"ngxsmk-pager__range\">\r\n <span class=\"ngxsmk-pager__range-text\">{{ getRangeLabel() }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Total items -->\r\n @if (config.showTotalItems) {\r\n <div class=\"ngxsmk-pager__total\">\r\n <span class=\"ngxsmk-pager__total-text\">{{ getTotalItemsLabel() }}</span>\r\n </div>\r\n }\r\n\r\n <!-- Navigation controls -->\r\n <div class=\"ngxsmk-pager__navigation\">\r\n \r\n <!-- First page button -->\r\n @if (config.showFirstLastButtons) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--first\"\r\n [disabled]=\"!hasPreviousPage\"\r\n (click)=\"goToFirstPage()\"\r\n title=\"First page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u23EE</i>\r\n </button>\r\n }\r\n\r\n <!-- Previous page button -->\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--previous\"\r\n [disabled]=\"!hasPreviousPage\"\r\n (click)=\"goToPreviousPage()\"\r\n title=\"Previous page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u25C0</i>\r\n </button>\r\n\r\n <!-- Page numbers -->\r\n <div class=\"ngxsmk-pager__page-numbers\">\r\n \r\n <!-- Ellipsis before -->\r\n @if (shouldShowEllipsisBefore()) {\r\n <span class=\"ngxsmk-pager__ellipsis\">\r\n ...\r\n </span>\r\n }\r\n\r\n <!-- Page number buttons -->\r\n @for (page of pageNumbers; track page) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--page\"\r\n [class.ngxsmk-pager__button--active]=\"page === currentPage\"\r\n (click)=\"onPageChange(page)\"\r\n >\r\n {{ page }}\r\n </button>\r\n }\r\n\r\n <!-- Ellipsis after -->\r\n @if (shouldShowEllipsisAfter()) {\r\n <span class=\"ngxsmk-pager__ellipsis\">\r\n ...\r\n </span>\r\n }\r\n\r\n </div>\r\n\r\n <!-- Next page button -->\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--next\"\r\n [disabled]=\"!hasNextPage\"\r\n (click)=\"goToNextPage()\"\r\n title=\"Next page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u25B6</i>\r\n </button>\r\n\r\n <!-- Last page button -->\r\n @if (config.showFirstLastButtons) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--last\"\r\n [disabled]=\"!hasNextPage\"\r\n (click)=\"goToLastPage()\"\r\n title=\"Last page\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u23ED</i>\r\n </button>\r\n }\r\n\r\n </div>\r\n\r\n <!-- Refresh button (PR #2184) -->\r\n @if (showRefreshButton) {\r\n <button \r\n class=\"ngxsmk-pager__button ngxsmk-pager__button--refresh\"\r\n (click)=\"onRefresh()\"\r\n title=\"Refresh\"\r\n >\r\n <i class=\"ngxsmk-pager__icon\">\u21BB</i>\r\n </button>\r\n }\r\n\r\n </div>\r\n}\r\n", styles: [".ngxsmk-pager{display:flex;align-items:center;justify-content:space-between;padding-inline:12px;background:transparent;font-size:14px;color:#374151;min-height:50px;gap:16px;min-width:100%;width:max-content;box-sizing:border-box}.ngxsmk-pager__page-size{display:flex;align-items:center;gap:12px}.ngxsmk-pager__page-size-label{font-weight:500;color:#6b7280;white-space:nowrap}.ngxsmk-pager__page-size-select{padding:6px 10px;border:1px solid #d1d5db;border-radius:6px;background:#fff;font-size:14px;cursor:pointer;transition:all .2s ease;color:#374151;min-width:70px}.ngxsmk-pager__page-size-select:hover{border-color:#9ca3af;background:#f9fafb}.ngxsmk-pager__page-size-select:focus{outline:none;border-color:#3b82f6;box-shadow:0 0 0 3px #3b82f61a}.ngxsmk-pager__range-text,.ngxsmk-pager__total-text{color:#6b7280;font-weight:500;white-space:nowrap}.ngxsmk-pager__navigation{display:flex;align-items:center;gap:8px}.ngxsmk-pager__page-numbers{display:flex;align-items:center;gap:4px}.ngxsmk-pager__ellipsis{padding:8px 4px;color:#9ca3af;font-weight:600}.ngxsmk-pager__button{display:flex;align-items:center;justify-content:center;border:1px solid #d1d5db;border-radius:6px;background:#fff;color:#374151;font-size:14px;font-weight:500;cursor:pointer;transition:all .2s ease;-webkit-user-select:none;user-select:none;min-width:36px;height:36px;padding:0 10px}.ngxsmk-pager__button:hover:not(:disabled){background:#f9fafb;border-color:#9ca3af;color:#374151;box-shadow:0 1px 2px #0000000d}.ngxsmk-pager__button:active:not(:disabled){background:#f3f4f6;transform:translateY(1px)}.ngxsmk-pager__button:disabled{opacity:.4;cursor:not-allowed;background:#f9fafb;border-color:#e5e7eb;color:#9ca3af;box-shadow:none}.ngxsmk-pager__button--active{background:#3b82f6;border-color:#3b82f6;color:#fff;box-shadow:0 1px 2px #0000000d;font-weight:600}.ngxsmk-pager__button--active:hover{background:#2563eb;border-color:#2563eb;box-shadow:0 1px 3px #0000001a;color:#fff}.ngxsmk-pager__button--active:active{background:#1d4ed8;border-color:#1d4ed8}.ngxsmk-pager__button--first,.ngxsmk-pager__button--last,.ngxsmk-pager__button--previous,.ngxsmk-pager__button--next{min-width:40px}.ngxsmk-pager__button--page{min-width:36px}.ngxsmk-pager__icon{font-size:12px;font-weight:700}@media (max-width: 768px){.ngxsmk-pager{flex-direction:column;gap:12px;padding:12px}.ngxsmk-pager__navigation{order:1}.ngxsmk-pager__page-size,.ngxsmk-pager__range,.ngxsmk-pager__total{order:2}.ngxsmk-pager__page-numbers{flex-wrap:wrap;justify-content:center}.ngxsmk-pager__button{min-width:32px;height:32px;font-size:13px;padding:0 8px}.ngxsmk-pager__page-size,.ngxsmk-pager__range,.ngxsmk-pager__total{font-size:13px}}@media (max-width: 480px){.ngxsmk-pager{padding:8px;gap:8px}.ngxsmk-pager__button{min-width:28px;height:28px;font-size:12px;padding:0 6px}.ngxsmk-pager__button--first,.ngxsmk-pager__button--last,.ngxsmk-pager__button--previous,.ngxsmk-pager__button--next{min-width:32px}.ngxsmk-pager__page-size,.ngxsmk-pager__range,.ngxsmk-pager__total{font-size:12px}.ngxsmk-pager__ellipsis{padding:6px 2px}}\n"] }]
}], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { config: [{
type: Input
}], currentPage: [{
type: Input
}], pageSize: [{
type: Input
}], totalItems: [{
type: Input
}], showRefreshButton: [{
type: Input
}], pageChange: [{
type: Output
}], refresh: [{
type: Output
}] } });
class ColumnResizeService {
constructor() {
this.resizeStateSubject = new BehaviorSubject(null);
this.columnWidthsSubject = new BehaviorSubject({});
this.resizeState$ = this.resizeStateSubject.asObservable();
this.columnWidths$ = this.columnWidthsSubject.asObservable();
this.columnWidths = {};
this.isResizing = false;
this.resizeStartX = 0;
this.resizeStartWidth = 0;
this.resizeColumnId = '';
}
startResize(columnId, startX, currentWidth) {
this.isResizing = true;
this.resizeStartX = startX;
this.resizeStartWidth = currentWidth;
this.resizeColumnId = columnId;
}
updateResize(currentX, minWidth = 50, maxWidth = 1000) {
if (!this.isResizing)
return 0;
const deltaX = currentX - this.resizeStartX;
const newWidth = Math.max(minWidth, Math.min(maxWidth, this.resizeStartWidth + deltaX));
this.columnWidths[this.resizeColumnId] = newWidth;
this.columnWidthsSubject.next({ ...this.columnWidths });
return newWidth;
}
endResize() {
if (!this.isResizing)
return null;
const oldWidth = this.resizeStartWidth;
const newWidth = this.columnWidths[this.resizeColumnId];
const resizeState = {
columnId: this.resizeColumnId,
newWidth,
oldWidth,
minWidth: 50,
maxWidth: 1000
};
this.resizeStateSubject.next(resizeState);
this.isResizing = false;
this.resizeColumnId = '';
return resizeState;
}
cancelResize() {
if (this.isResizing) {
this.columnWidths[this.resizeColumnId] = this.resizeStartWidth;
this.columnWidthsSubject.next({ ...this.columnWidths });
}
this.isResizing = false;
this.resizeColumnId = '';
}
setColumnWidth(columnId, width) {
this.columnWidths[columnId] = width;
this.columnWidthsSubject.next({ ...this.columnWidths });
}
getColumnWidth(columnId) {
return this.columnWidths[columnId] || 100;
}
setColumnWidths(widths) {
this.columnWidths = { ...widths };
this.columnWidthsSubject.next(this.columnWidths);
}
resetColumnWidths() {
this.columnWidths = {};
this.columnWidthsSubject.next({});
}
isResizingColumn() {
return this.isResizing;
}
getResizingColumnId() {
return this.resizeColumnId;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ColumnResizeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ColumnResizeService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ColumnResizeService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
class SelectionService {
constructor() {
this.selectionSubject = new BehaviorSubject(null);
this.selectedRowsSubject = new BehaviorSubject([]);
this.selection$ = this.selectionSubject.asObservable();
this.selectedRows$ = this.selectedRowsSubject.asObservable();
}
createSelectionModel(rowIdentity = (row) => row.id || row, multiSelect = true) {
const selected = new Set();
const deselected = new Set();
const getRowId = (row) => {
const id = rowIdentity(row);
return typeof id === 'string' ? id : JSON.stringify(id);
};
const selectionModel = {
selected,
deselected,
isSelected: (row) => {
const rowId = getRowId(row);
return selected.has(rowId);
},
isDeselected: (row) => {
const rowId = getRowId(row);
return deselected.has(rowId);
},
select: (row) => {
const rowId = getRowId(row);
selected.add(rowId);
deselected.delete(rowId);
this.updateSelectedRows();
},
deselect: (row) => {
const rowId = getRowId(row);
selected.delete(rowId);
deselected.add(rowId);
this.updateSelectedRows();
},
clear: () => {
selected.clear();
deselected.clear();
this.updateSelectedRows();
},
selectAll: (rows) => {
selected.clear();
deselected.clear();
rows.forEach(row => {
const rowId = getRowId(row);
selected.add(rowId);
});
this.updateSelectedRows();
},
deselectAll: () => {
selected.clear();
deselected.clear();
this.updateSelectedRows();
},
toggle: (row) => {
const rowId = getRowId(row);
if (selected.has(rowId)) {
selected.delete(rowId);
deselected.add(rowId);
}
else {
selected.add(rowId);
deselected.delete(rowId);
}
this.updateSelectedRows();
},
isAllSelected: (rows) => {
if (rows.length === 0)
return false;
return rows.every(row => selected.has(getRowId(row)));
},
isIndeterminate: (rows) => {
if (rows.length === 0)
return false;
const selectedCount = rows.filter(row => selected.has(getRowId(row))).length;
return selectedCount > 0 && selectedCount < rows.length;
},
getSelectedRows: () => {
return Array.from(selected);
},
getSelectedCount: () => {
return selected.size;
}
};
this.selectionSubject.next(selectionModel);
return selectionModel;
}
updateSelectedRows() {
const selectionModel = this.selectionSubject.value;
if (selectionModel) {
const selectedRows = selectionModel.getSelectedRows();
this.selectedRowsSubject.next(selectedRows);
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelectionService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelectionService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: SelectionService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}] });
/**
* Service for managing drag-and-drop operations in the datatable
*/
class DragDropService {
constructor() {
this.dragState$ = new BehaviorSubject({
isDragging: false,
type: null,
item: null,
sourceIndex: -1,
targetIndex: -1,
startX: 0,
startY: 0,
currentX: 0,
currentY: 0,
dragElement: null
});
this.columnReorder$ = new Subject();
this.rowReorder$ = new Subject();
this.dragStart$ = new Subject();
this.dragOver$ = new Subject();
this.drop$ = new Subject();
this.config = {
enableColumnReorder: true,
enableRowReorder: false,
dragHandleClass: 'drag-handle',
showDragPreview: true,
dragThreshold: 5,
animationDuration: 200
};
}
/**
* Get current drag state as observable
*/
get state$() {
return this.dragState$.asObservable();
}
/**
* Get column reorder events
*/
get onColumnReorder$() {
return this.columnReorder$.asObservable();
}
/**
* Get row reorder events
*/
get onRowReorder$() {
return this.rowReorder$.asObservable();
}
/**
* Get drag start events
*/
get onDragStart$() {
return this.dragStart$.asObservable();
}
/**
* Get drag over events
*/
get onDragOver$() {
return this.dragOver$.asObservable();
}
/**
* Get drop events
*/
get onDrop$() {
return this.drop$.asObservable();
}
/**
* Get current drag state snapshot
*/
getState() {
return this.dragState$.value;
}
/**
* Set configuration
*/
setConfig(config) {
this.config = { ...this.config, ...config };
}
/**
* Get configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Start dragging a column
*/
startDragColumn(column, index, event) {
if (!this.config.enableColumnReorder)
return;
const rect = event.target.getBoundingClientRect();
const dragStartEvent = {
type: 'column',
item: column,
index,
event,
startX: event.clientX,
startY: event.clientY
};
this.dragState$.next({
isDragging: true,
type: 'column',
item: column,
sourceIndex: index,
targetIndex: index,
startX: event.clientX,
startY: event.clientY,
currentX: event.clientX,
currentY: event.clientY,
dragElement: event.target
});
this.dragStart$.next(dragStartEvent);
// Set drag data
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', index.toString());
// Create drag image if enabled
if (this.config.showDragPreview) {
const dragImage = this.createDragImage(event.target);
if (dragImage) {
event.dataTransfer.setDragImage(dragImage, 0, 0);
}
}
}
/**
* Start dragging a row
*/
startDragRow(row, index, event) {
if (!this.config.enableRowReorder)
return;
const dragStartEvent = {
type: 'row',
item: row,
index,
event,
startX: event.clientX,
startY: event.clientY
};
this.dragState$.next({
isDragging: true,
type: 'row',
item: row,
sourceIndex: index,
targetIndex: index,
startX: event.clientX,
startY: event.clientY,
currentX: event.clientX,
currentY: event.clientY,
dragElement: event.target
});
this.dragStart$.next(dragStartEvent);
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData('text/plain', index.toString());
}
/**
* Handle drag over
*/
dragOver(targetItem, targetIndex, event) {
event.preventDefault();
const state = this.getState();
if (!state.isDragging)
return;
const dragOverEvent = {
type: state.type,
sourceItem: state.item,
sourceIndex: state.sourceIndex,
targetItem,
targetIndex,
event,
currentX: event.clientX,
currentY: event.clientY
};
this.dragState$.next({
...state,
targetIndex,
currentX: event.clientX,
currentY: event.clientY
});
this.dragOver$.next(dragOverEvent);
event.dataTransfer.dropEffect = 'move';
}
/**
* Handle drop
*/
drop(items, event) {
event.preventDefault();
const state = this.getState();
if (!state.isDragging)
return;
const moved = state.sourceIndex !== state.targetIndex;
const dropEvent = {
type: state.type,
sourceItem: state.item,
sourceIndex: state.sourceIndex,
targetIndex: state.targetIndex,
event,
moved
};
this.drop$.next(dropEvent);
if (moved) {
// Reorder the array
const reorderedItems = this.reorderArray(items, state.sourceIndex, state.targetIndex);
// Emit specific event based on type
if (state.type === 'column') {
this.columnReorder$.next({
column: state.item,
oldIndex: state.sourceIndex,
newIndex: state.targetIndex
});
}
else if (state.type === 'row') {
this.rowReorder$.next({
row: state.item,
previousIndex: state.sourceIndex,
newIndex: state.targetIndex,
rows: reorderedItems
});
}
}
this.endDrag();
}
/**
* End drag operation
*/
endDrag() {
this.dragState$.next({
isDragging: false,
type: null,
item: null,
sourceIndex: -1,
targetIndex: -1,
startX: 0,
startY: 0,
currentX: 0,
currentY: 0,
dragElement: null
});
}
/**
* Reorder an array by moving an item from one index to another
*/
reorderArray(array, fromIndex, toIndex) {
const result = [...array];
const [removed] = result.splice(fromIndex, 1);
result.splice(toIndex, 0, removed);
return result;
}
/**
* Create drag image element
*/
createDragImage(element) {
try {
const clone = element.cloneNode(true);
clone.style.position = 'absolute';
clone.style.top = '-9999px';
clone.style.opacity = '0.8';
clone.style.pointerEvents = 'none';
document.body.appendChild(clone);
// Clean up after a short delay
setTimeout(() => {
if (document.body.contains(clone)) {
document.body.removeChild(clone);
}
}, 0);
return clone;
}
catch (error) {
console.error('Failed to create drag image:', error);
return null;
}
}
/**
* Calculate drop position based on mouse position
*/
calculateDropPosition(targetElement, mouseX, mouseY, orientation = 'horizontal') {
const rect = targetElement.getBoundingClientRect();
if (orientation === 'horizontal') {
const midpoint = rect.left + rect.width / 2;
return mouseX < midpoint ? 'before' : 'after';
}
else {
const midpoint = rect.top + rect.height / 2;
return mouseY < midpoint ? 'before' : 'after';
}
}
/**
* Check if dragging is currently active
*/
isDragging() {
return this.dragState$.value.isDragging;
}
/**
* Get the type of current drag operation
*/
getDragType() {
return this.dragState$.value.type;
}
/**
* Get source index of current drag
*/
getSourceIndex() {
return this.dragState$.value.sourceIndex;
}
/**
* Get target index of current drag
*/
getTargetIndex() {
return this.dragState$.value.targetIndex;
}
/**
* Clean up service
*/
ngOnDestroy() {
this.dragState$.complete();
this.columnReorder$.complete();
this.rowReorder$.complete();
this.dragStart$.complete();
this.dragOver$.complete();
this.drop$.complete();
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DragDropService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DragDropService }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: DragDropService, decorators: [{
type: Injectable
}] });
/**
* Service for managing responsive behavior
*/
class ResponsiveService {
constructor() {
this.breakpoints = {
xs: 480,
sm: 768,
md: 1024,
lg: 1280,
xl: 1920
};
this.state$ = new BehaviorSubject(this.calculateState(window.innerWidth, window.innerHeight));
/**
* Observable of responsive state
*/
this.responsiveState$ = this.state$.asObservable();
// Listen to window resize
if (typeof window !== 'undefined') {
fromEvent(window, 'resize')
.pipe(debounceTime(200), map(() => ({
width: window.innerWidth,
height: window.innerHeight
})), distinctUntilChanged((prev, curr) => prev.width === curr.width && prev.height === curr.height))
.subscribe(({ width, height }) => {
this.state$.next(this.calculateState(width, height));
});
}
}
/**
* Get current state
*/
getState() {
return this.state$.value;
}
/**
* Set custom breakpoints
*/
setBreakpoints(breakpoints) {
this.breakpoints = { ...this.breakpoints, ...breakpoints };
const state = this.calculateState(window.innerWidth, window.innerHeight);
this.state$.next(state);
}
/**
* Calculate responsive state
*/
calculateState(width, height) {
const deviceType = this.getDeviceType(width);
const isMobile = deviceType === 'mobile';
const isTablet = deviceType === 'tablet';
const isDesktop = deviceType === 'desktop';
const orientation = height > width ? 'portrait' : 'landscape';
const displayMode = this.getDisplayMode(deviceType);
return {
width,
height,
deviceType,
displayMode,
isMobile,
isTablet,
isDesktop,
orientation
};
}
/**
* Determine device type from width
*/
getDeviceType(width) {
if (width < this.breakpoints.sm) {
return 'mobile';
}
else if (width < this.breakpoints.md) {
return 'tablet';
}
else {
return 'desktop';
}
}
/**
* Get display mode for device type
*/
getDisplayMode(deviceType) {
switch (deviceType) {
case 'mobile':
return 'card';
case 'tablet':
return 'table';
case 'desktop':
return 'table';
}
}
/**
* Check if current viewport matches breakpoint
*/
matches(breakpoint) {
const state = this.getState();
return state.width >= this.breakpoints[breakpoint];
}
/**
* Check if viewport is between breakpoints
*/
matchesBetween(min, max) {
const state = this.getState();
return (state.width >= this.breakpoints[min] &&
state.width < this.breakpoints[max]);
}
/**
* Get visible columns based on device type
*/
getVisibleColumns(allColumns, config) {
const state = this.getState();
if (!config.enabled) {
return allColumns;
}
let hiddenIds = [];
if (state.isMobile && config.hiddenColumnsOnMobile) {
hiddenIds = config.hiddenColumnsOnMobile;
}
else if (state.isTablet && config.hiddenColumnsOnTablet) {
hiddenIds = config.hiddenColumnsOnTablet;
}
return allColumns.filter(col => !hiddenIds.includes(col.id));
}
/**
* Should use card view
*/
shouldUseCardView(config) {
const state = this.getState();
if (!config?.enabled) {
return false;
}
const mobileMode = config.displayModes?.mobile || 'card';
return state.isMobile && mobileMode === 'card';
}
/**
* Should use list view
*/
shouldUseListView(config) {
const state = this.getState();
if (!config?.enabled) {
return false;
}
const mobileMode = config.displayModes?.mobile || 'card';
return state.isMobile && mobileMode === 'list';
}
/**
* Get column priority for responsive hiding
*/
getColumnPriority(column) {
return column.responsivePriority || 999;
}
/**
* Sort columns by responsive priority
*/
sortColumnsByPriority(columns) {
return [...columns].sort((a, b) => {
const priorityA = this.getColumnPriority(a);
const priorityB = this.getColumnPriority(b);
return priorityA - priorityB;
});
}
/**
* Calculate optimal column count for viewport
*/
getOptimalColumnCount(viewportWidth, minColumnWidth = 150) {
return Math.max(1, Math.floor(viewportWidth / minColumnWidth));
}
/**
* Check if touch device
*/
isTouchDevice() {
return ('ontouchstart