UNPKG

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
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