mn-angular-lib 1.0.165 → 1.0.167
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -7609,6 +7609,8 @@ var MnCollectionState;
|
|
|
7609
7609
|
*/
|
|
7610
7610
|
class MnCollectionBase {
|
|
7611
7611
|
dataSource;
|
|
7612
|
+
/** Row count at which the search box auto-enables when `canSearch` is unset. */
|
|
7613
|
+
static DEFAULT_SEARCH_THRESHOLD = 8;
|
|
7612
7614
|
filteredItems = [];
|
|
7613
7615
|
paginatedItems = [];
|
|
7614
7616
|
searchValue = '';
|
|
@@ -7685,6 +7687,26 @@ class MnCollectionBase {
|
|
|
7685
7687
|
get isServerSearched() {
|
|
7686
7688
|
return !!this.dataSource.onServerSearch;
|
|
7687
7689
|
}
|
|
7690
|
+
/**
|
|
7691
|
+
* Whether the search box is shown: the explicit `canSearch` when set, otherwise
|
|
7692
|
+
* auto-enabled once the row count reaches `searchThreshold` and the source can actually
|
|
7693
|
+
* search (a client predicate or a server callback — a box that filters nothing is noise).
|
|
7694
|
+
* A non-empty term keeps the box even when the (server-)filtered result drops below the
|
|
7695
|
+
* threshold, so the user can always clear what they typed.
|
|
7696
|
+
*/
|
|
7697
|
+
get isSearchable() {
|
|
7698
|
+
if (this.dataSource.canSearch !== undefined)
|
|
7699
|
+
return this.dataSource.canSearch;
|
|
7700
|
+
if (!this.dataSource.isInSearch && !this.isServerSearched)
|
|
7701
|
+
return false;
|
|
7702
|
+
if (this.searchValue.length > 0)
|
|
7703
|
+
return true;
|
|
7704
|
+
const threshold = this.dataSource.searchThreshold ?? MnCollectionBase.DEFAULT_SEARCH_THRESHOLD;
|
|
7705
|
+
const rowCount = this.isServerPaginated && this.dataSource.totalItems != null
|
|
7706
|
+
? this.dataSource.totalItems
|
|
7707
|
+
: (this.dataSource.dataRows.value ?? []).length;
|
|
7708
|
+
return rowCount >= threshold;
|
|
7709
|
+
}
|
|
7688
7710
|
get isPaginated() {
|
|
7689
7711
|
const mode = this.dataSource.paginationMode;
|
|
7690
7712
|
return mode === 'paginated' || mode === 'client-side-pagination';
|
|
@@ -7965,7 +7987,7 @@ class MnCollectionBase {
|
|
|
7965
7987
|
}
|
|
7966
7988
|
/** Client-side search filtering shared by list and grid. */
|
|
7967
7989
|
applySearchFilter(items) {
|
|
7968
|
-
if (!this.isServerSearched && this.dataSource.isInSearch && this.
|
|
7990
|
+
if (!this.isServerSearched && this.dataSource.isInSearch && this.isSearchable && this.searchValue && this.searchValue.length > 0) {
|
|
7969
7991
|
const term = this.searchValue.toLowerCase();
|
|
7970
7992
|
return items.filter(row => this.dataSource.isInSearch(row, term));
|
|
7971
7993
|
}
|
|
@@ -8995,6 +9017,19 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
8995
9017
|
hasRowActions(column, row) {
|
|
8996
9018
|
return this.visibleRowActions(column, row).length > 0;
|
|
8997
9019
|
}
|
|
9020
|
+
/**
|
|
9021
|
+
* Whether a row's actions fold into the ⋯ menu below 450px. They do unless the row has
|
|
9022
|
+
* exactly one visible action that renders as a bare icon: that button is narrower than the
|
|
9023
|
+
* ⋯ trigger it would hide behind, so collapsing it only puts a second tap in front of the
|
|
9024
|
+
* one command the row has.
|
|
9025
|
+
*/
|
|
9026
|
+
collapsesRowActions(column, row) {
|
|
9027
|
+
const visible = this.visibleRowActions(column, row);
|
|
9028
|
+
if (visible.length !== 1)
|
|
9029
|
+
return true;
|
|
9030
|
+
const [only] = visible;
|
|
9031
|
+
return this.showActionLabel(column, only, row);
|
|
9032
|
+
}
|
|
8998
9033
|
/**
|
|
8999
9034
|
* Resolves a {@link MnRowValue}: either the fixed value, or the accessor applied to
|
|
9000
9035
|
* the row. Every per-row presentation field goes through here so the fixed and derived
|
|
@@ -9234,11 +9269,11 @@ class MnTable extends MnSelectableCollectionBase {
|
|
|
9234
9269
|
return template.replace('{{label}}', this.selectionLabelFor(row));
|
|
9235
9270
|
}
|
|
9236
9271
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
9237
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "filtersSheet", first: true, predicate: ["filtersSheet"], descendants: true }, { propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"openFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filters: full-width fields decoupled from column widths, presented\n as a bottom sheet so they overlay rather than push the table down. -->\n@if (hasColumnFilters && filtersCollapsed && filtersPanelOpen) {\n <mn-bottom-sheet\n #filtersSheet\n (dismiss)=\"filtersPanelOpen = false\"\n [ariaLabel]=\"filtersButtonLabel\"\n [growWithKeyboard]=\"true\"\n [maxHeightVh]=\"80\"\n >\n <div id=\"mn-table-filters-panel\" class=\"flex flex-col gap-3 px-4 pb-4\">\n <span class=\"text-base font-semibold text-base-content\">{{ filtersButtonLabel }}</span>\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-panel-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n <div class=\"mt-1 flex min-[400px]:justify-end\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[400px]:w-auto\"\n (click)=\"closeFiltersPanel()\"\n >\n <span>{{ filtersCloseLabel }}</span>\n </button>\n </div>\n </div>\n </mn-bottom-sheet>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.actions) {\n <!-- Actions column: inline command buttons that collapse into a \u22EF menu\n once the table is narrower than 450px (container query), for every\n row with actions. A row with no visible actions renders nothing. -->\n @if (hasRowActions(column, row)) {\n <div class=\"inline-flex items-center gap-1\"\n [class.justify-end]=\"(column.align ?? 'left') === 'right'\">\n <span class=\"hidden items-center gap-1 @min-[450px]:inline-flex\">\n <ng-container [ngTemplateOutletContext]=\"{ column: column, row: row }\"\n [ngTemplateOutlet]=\"actionButtons\"></ng-container>\n </span>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span class=\"@min-[450px]:hidden\" (click)=\"$event.stopPropagation()\">\n <mn-lib-dropdown [datasource]=\"{\n id: actionsDropdownId(column, row),\n actions: rowDropdownActions(column, row),\n menuLabel: $any(column.header),\n size: 'sm'\n }\"></mn-lib-dropdown>\n </span>\n </div>\n }\n } @else if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n\n<!-- Inline action buttons for an actions column, shared by the wide-table layout and\n (implicitly) mirrored by the \u22EF menu below 450px. -->\n<ng-template #actionButtons let-column=\"column\" let-row=\"row\">\n @for (action of visibleRowActions(column, row); track $index) {\n <button\n mnButton\n type=\"button\"\n [data]=\"{\n size: 'sm',\n variant: 'text',\n color: rowActionColor(action, row),\n disabled: isRowActionDisabled(action, row)\n }\"\n class=\"cursor-pointer\"\n (click)=\"$event.stopPropagation(); runRowAction(action, row)\"\n [attr.aria-label]=\"rowActionLabel(action, row)\"\n [attr.title]=\"showActionLabel(column, action, row) ? null : rowActionLabel(action, row)\"\n >\n @if (showActionIcon(column, action, row)) {\n <span class=\"inline-flex items-center shrink-0\">\n @let icon = rowActionIcon(action, row);\n @if (isTemplateRef(icon)) {\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n } @else {\n <!-- Data icon: sized here to match the sm button's text, so a caller can\n declare the action in TypeScript without owning a template. -->\n <svg [lucideIcon]=\"$any(icon)\" [size]=\"17\"></svg>\n }\n </span>\n }\n @if (showActionLabel(column, action, row)) {\n <span>{{ rowActionLabel(action, row) }}</span>\n }\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnDropdown, selector: "mn-lib-dropdown", inputs: ["datasource"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9272
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, host: { listeners: { "window:resize": "onWindowResize()" }, classAttribute: "block" }, viewQueries: [{ propertyName: "filtersSheet", first: true, predicate: ["filtersSheet"], descendants: true }, { propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (isSearchable || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (isSearchable) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"openFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filters: full-width fields decoupled from column widths, presented\n as a bottom sheet so they overlay rather than push the table down. -->\n@if (hasColumnFilters && filtersCollapsed && filtersPanelOpen) {\n <mn-bottom-sheet\n #filtersSheet\n (dismiss)=\"filtersPanelOpen = false\"\n [ariaLabel]=\"filtersButtonLabel\"\n [growWithKeyboard]=\"true\"\n [maxHeightVh]=\"80\"\n >\n <div id=\"mn-table-filters-panel\" class=\"flex flex-col gap-3 px-4 pb-4\">\n <span class=\"text-base font-semibold text-base-content\">{{ filtersButtonLabel }}</span>\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-panel-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n <div class=\"mt-1 flex min-[400px]:justify-end\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[400px]:w-auto\"\n (click)=\"closeFiltersPanel()\"\n >\n <span>{{ filtersCloseLabel }}</span>\n </button>\n </div>\n </div>\n </mn-bottom-sheet>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.actions) {\n <!-- Actions column: inline command buttons that collapse into a \u22EF menu\n once the table is narrower than 450px (container query), for every\n row with actions. A row with no visible actions renders nothing.\n A row with a single icon-only action never collapses: one icon button\n is narrower than the \u22EF trigger it would hide behind, so folding it\n only costs a tap. -->\n @if (hasRowActions(column, row)) {\n <div class=\"inline-flex items-center gap-1\"\n [class.justify-end]=\"(column.align ?? 'left') === 'right'\">\n @if (collapsesRowActions(column, row)) {\n <span class=\"hidden items-center gap-1 @min-[450px]:inline-flex\">\n <ng-container [ngTemplateOutletContext]=\"{ column: column, row: row }\"\n [ngTemplateOutlet]=\"actionButtons\"></ng-container>\n </span>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span class=\"@min-[450px]:hidden\" (click)=\"$event.stopPropagation()\">\n <mn-lib-dropdown [datasource]=\"{\n id: actionsDropdownId(column, row),\n actions: rowDropdownActions(column, row),\n menuLabel: $any(column.header),\n size: 'sm'\n }\"></mn-lib-dropdown>\n </span>\n } @else {\n <span class=\"inline-flex items-center gap-1\">\n <ng-container [ngTemplateOutletContext]=\"{ column: column, row: row }\"\n [ngTemplateOutlet]=\"actionButtons\"></ng-container>\n </span>\n }\n </div>\n }\n } @else if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n\n<!-- Inline action buttons for an actions column, shared by the wide-table layout and\n (implicitly) mirrored by the \u22EF menu below 450px. -->\n<ng-template #actionButtons let-column=\"column\" let-row=\"row\">\n @for (action of visibleRowActions(column, row); track $index) {\n <button\n mnButton\n type=\"button\"\n [data]=\"{\n size: 'sm',\n variant: 'text',\n color: rowActionColor(action, row),\n disabled: isRowActionDisabled(action, row)\n }\"\n class=\"cursor-pointer\"\n (click)=\"$event.stopPropagation(); runRowAction(action, row)\"\n [attr.aria-label]=\"rowActionLabel(action, row)\"\n [attr.title]=\"showActionLabel(column, action, row) ? null : rowActionLabel(action, row)\"\n >\n @if (showActionIcon(column, action, row)) {\n <span class=\"inline-flex items-center shrink-0\">\n @let icon = rowActionIcon(action, row);\n @if (isTemplateRef(icon)) {\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n } @else {\n <!-- Data icon: sized here to match the sm button's text, so a caller can\n declare the action in TypeScript without owning a template. -->\n <svg [lucideIcon]=\"$any(icon)\" [size]=\"17\"></svg>\n }\n </span>\n }\n @if (showActionLabel(column, action, row)) {\n <span>{{ rowActionLabel(action, row) }}</span>\n }\n </button>\n }\n</ng-template>\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "directive", type: MnShowAboveDirective, selector: "[mnShowAbove]", inputs: ["mnShowAbove"] }, { kind: "directive", type: MnShowBelowDirective, selector: "[mnShowBelow]", inputs: ["mnShowBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnDropdown, selector: "mn-lib-dropdown", inputs: ["datasource"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnBottomSheet, selector: "mn-bottom-sheet", inputs: ["showBackdrop", "showGrabber", "dismissible", "minHeightPx", "maxHeightVh", "containerClass", "ariaLabel", "ariaLabelledby", "growWithKeyboard", "dismissGuard"], outputs: ["dismiss"] }, { kind: "component", type: LucideFilter, selector: "svg[lucideFunnel], svg[lucideFilter]" }, { kind: "component", type: LucideX, selector: "svg[lucideX]" }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
9238
9273
|
}
|
|
9239
9274
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
9240
9275
|
type: Component,
|
|
9241
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnDropdown, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, MnBottomSheet, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (dataSource.canSearch || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (dataSource.canSearch) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"openFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filters: full-width fields decoupled from column widths, presented\n as a bottom sheet so they overlay rather than push the table down. -->\n@if (hasColumnFilters && filtersCollapsed && filtersPanelOpen) {\n <mn-bottom-sheet\n #filtersSheet\n (dismiss)=\"filtersPanelOpen = false\"\n [ariaLabel]=\"filtersButtonLabel\"\n [growWithKeyboard]=\"true\"\n [maxHeightVh]=\"80\"\n >\n <div id=\"mn-table-filters-panel\" class=\"flex flex-col gap-3 px-4 pb-4\">\n <span class=\"text-base font-semibold text-base-content\">{{ filtersButtonLabel }}</span>\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-panel-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n <div class=\"mt-1 flex min-[400px]:justify-end\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[400px]:w-auto\"\n (click)=\"closeFiltersPanel()\"\n >\n <span>{{ filtersCloseLabel }}</span>\n </button>\n </div>\n </div>\n </mn-bottom-sheet>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.actions) {\n <!-- Actions column: inline command buttons that collapse into a \u22EF menu\n once the table is narrower than 450px (container query), for every\n row with actions. A row with no visible actions renders nothing. -->\n @if (hasRowActions(column, row)) {\n <div class=\"inline-flex items-center gap-1\"\n [class.justify-end]=\"(column.align ?? 'left') === 'right'\">\n <span class=\"hidden items-center gap-1 @min-[450px]:inline-flex\">\n <ng-container [ngTemplateOutletContext]=\"{ column: column, row: row }\"\n [ngTemplateOutlet]=\"actionButtons\"></ng-container>\n </span>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span class=\"@min-[450px]:hidden\" (click)=\"$event.stopPropagation()\">\n <mn-lib-dropdown [datasource]=\"{\n id: actionsDropdownId(column, row),\n actions: rowDropdownActions(column, row),\n menuLabel: $any(column.header),\n size: 'sm'\n }\"></mn-lib-dropdown>\n </span>\n </div>\n }\n } @else if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n\n<!-- Inline action buttons for an actions column, shared by the wide-table layout and\n (implicitly) mirrored by the \u22EF menu below 450px. -->\n<ng-template #actionButtons let-column=\"column\" let-row=\"row\">\n @for (action of visibleRowActions(column, row); track $index) {\n <button\n mnButton\n type=\"button\"\n [data]=\"{\n size: 'sm',\n variant: 'text',\n color: rowActionColor(action, row),\n disabled: isRowActionDisabled(action, row)\n }\"\n class=\"cursor-pointer\"\n (click)=\"$event.stopPropagation(); runRowAction(action, row)\"\n [attr.aria-label]=\"rowActionLabel(action, row)\"\n [attr.title]=\"showActionLabel(column, action, row) ? null : rowActionLabel(action, row)\"\n >\n @if (showActionIcon(column, action, row)) {\n <span class=\"inline-flex items-center shrink-0\">\n @let icon = rowActionIcon(action, row);\n @if (isTemplateRef(icon)) {\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n } @else {\n <!-- Data icon: sized here to match the sm button's text, so a caller can\n declare the action in TypeScript without owning a template. -->\n <svg [lucideIcon]=\"$any(icon)\" [size]=\"17\"></svg>\n }\n </span>\n }\n @if (showActionLabel(column, action, row)) {\n <span>{{ rowActionLabel(action, row) }}</span>\n }\n </button>\n }\n</ng-template>\n" }]
|
|
9276
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnMultiSelect, MnDropdown, MnSkeleton, FormsModule, MnCollectionPagination, MnButton, MnBottomSheet, LucideFilter, LucideX, LucideFunnel, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'block' }, template: "<!-- Everything that reflows lives inside a @container, so the breakpoints below\n measure the table's own width rather than the window's. A table in a modal,\n a sidebar or a narrow grid cell is far narrower than the viewport, and\n viewport breakpoints would hand it a desktop layout it has no room for. -->\n<div class=\"@container\">\n <!-- Selection summary: the whole selection, never paged, filtered or sorted. The\n table below answers \"what could I pick?\", which is why it is searched and paged;\n this answers \"what did I pick?\", which a paginated list cannot without hiding\n most of the answer on some other page. -->\n @if (showSelectionSummary) {\n <div class=\"flex flex-col gap-2 rounded-md border border-base-300 bg-base-200/50 p-3 mb-3\">\n <div class=\"flex items-center justify-between gap-2\">\n <span class=\"text-sm font-medium text-base-content\">{{ selectionSummaryTitle }}</span>\n <button\n (click)=\"clearSelection()\"\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"gap-1 shrink-0\"\n mnButton\n type=\"button\"\n >\n <svg [size]=\"14\" lucideX></svg>\n <span>{{ selectionClearAllLabel }}</span>\n </button>\n </div>\n <!-- Only the first few tags render; the rest collapse behind a \"+N more\" control.\n The heading's count always states the true total, so this hides tags, never\n information. Expanded, the list is height-capped and scrolls, so even a\n selection of hundreds cannot push the table off screen. -->\n <ul\n [class.max-h-28]=\"selectionSummaryExpanded\"\n [class.overflow-y-auto]=\"selectionSummaryExpanded\"\n class=\"flex flex-wrap gap-1.5 list-none m-0 p-0\"\n >\n @for (row of visibleSelectionRows; track dataSource.getID(row)) {\n <li\n class=\"inline-flex items-center gap-1 rounded-full bg-base-100 border border-base-300 pl-2.5 pr-1 py-0.5 text-xs text-base-content max-w-full\">\n <span [attr.title]=\"selectionLabelFor(row)\" class=\"truncate\">{{ selectionLabelFor(row) }}</span>\n <button\n (click)=\"removeSelection(row)\"\n [attr.aria-label]=\"selectionRemoveLabel(row)\"\n class=\"shrink-0 rounded-full p-0.5 hover:bg-base-300 transition-colors cursor-pointer\"\n type=\"button\"\n >\n <svg [size]=\"12\" lucideX></svg>\n </button>\n </li>\n }\n @if (hiddenSelectionCount > 0 || selectionSummaryExpanded) {\n <li>\n <button\n (click)=\"toggleSelectionSummary()\"\n [attr.aria-expanded]=\"selectionSummaryExpanded\"\n [data]=\"{ variant: 'text', color: 'primary', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"text-xs\"\n mnButton\n type=\"button\"\n >\n {{ selectionSummaryToggleLabel }}\n </button>\n </li>\n }\n </ul>\n </div>\n }\n\n <!-- Toolbar: custom toolbar template + search. Only rendered when it has content,\n so an unsearchable table without toolbar templates emits no empty spacer. -->\n@if (isSearchable || dataSource.toolbarLeftTemplate || dataSource.toolbarRightTemplate || (hasColumnFilters && filtersCollapsed)) {\n <div class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full @min-[420px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col @min-[420px]:flex-row @min-[420px]:items-center gap-2 w-full @min-[420px]:flex-1 @min-[560px]:flex-none @min-[560px]:w-auto\">\n @if (isSearchable) {\n <!-- Enter must not submit a surrounding form: the search box belongs to the\n table's chrome, not to whatever form the table happens to sit in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n class=\"w-full @min-[420px]:flex-1 @min-[560px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n ariaLabel: searchPlaceholderLabel,\n placeholder: searchPlaceholderLabel,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n [ngModel]=\"searchValue\"\n (ngModelChange)=\"onSearch($event)\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarRightTemplate) {\n <div class=\"flex flex-col w-full @min-[420px]:w-auto @min-[420px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n <!-- Small-screen filter toggle: replaces the inline per-column filter row below 640px -->\n @if (hasColumnFilters && filtersCollapsed) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'outline', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full @min-[420px]:w-auto gap-1.5\"\n [attr.aria-expanded]=\"filtersPanelOpen\"\n aria-controls=\"mn-table-filters-panel\"\n (click)=\"openFiltersPanel()\"\n >\n <svg lucideFunnel [size]=\"15\"></svg>\n <span>{{ filtersButtonLabel }}</span>\n </button>\n }\n </div>\n</div>\n}\n\n<!-- Small-screen filters: full-width fields decoupled from column widths, presented\n as a bottom sheet so they overlay rather than push the table down. -->\n@if (hasColumnFilters && filtersCollapsed && filtersPanelOpen) {\n <mn-bottom-sheet\n #filtersSheet\n (dismiss)=\"filtersPanelOpen = false\"\n [ariaLabel]=\"filtersButtonLabel\"\n [growWithKeyboard]=\"true\"\n [maxHeightVh]=\"80\"\n >\n <div id=\"mn-table-filters-panel\" class=\"flex flex-col gap-3 px-4 pb-4\">\n <span class=\"text-base font-semibold text-base-content\">{{ filtersButtonLabel }}</span>\n @for (column of dataSource.columns; track column.key) {\n @if (column.filterable) {\n <div class=\"flex flex-col gap-1\">\n <label\n class=\"text-xs font-medium text-base-content/70\"\n [attr.for]=\"'mn-table-filter-panel-' + column.key\"\n >\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n {{ column.header }}\n }\n </label>\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'panel' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n </div>\n }\n }\n @if (hasActiveFilters) {\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'text', color: 'gray', size: 'sm', borderRadius: 'md', hover: true }\"\n class=\"self-start gap-1\"\n (click)=\"clearAllFilters()\"\n >\n <svg lucideX [size]=\"14\"></svg>\n <span>{{ clearFiltersButtonLabel }}</span>\n </button>\n }\n <div class=\"mt-1 flex min-[400px]:justify-end\">\n <button\n type=\"button\"\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', size: 'md', borderRadius: 'md', hover: true }\"\n class=\"w-full min-[400px]:w-auto\"\n (click)=\"closeFiltersPanel()\"\n >\n <span>{{ filtersCloseLabel }}</span>\n </button>\n </div>\n </div>\n </mn-bottom-sheet>\n}\n\n<!-- Table wrapper with horizontal scroll -->\n <div #collectionBody [attr.aria-label]=\"tableRegionLabel\" [style.min-height.px]=\"bodyMinHeight\"\n class=\"overflow-x-auto\"\n role=\"region\">\n <table [class]=\"tableClasses\">\n <thead>\n <tr class=\"bg-base-100\">\n <!-- Selection checkbox column header -->\n @if (hasSelection) {\n <th class=\"w-10 text-center text-sm px-2 py-2\">\n @if (isMultiSelect) {\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-table-select-all', size: 'sm' }\"\n ></mn-lib-checkbox>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n [attr.data-column-key]=\"column.key\"\n [class.truncate]=\"widthsArePinned\"\n [class.cursor-pointer]=\"isSortable(column)\"\n [class.select-none]=\"isSortable(column)\"\n [class.hover:bg-base-200]=\"isSortable(column)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\n class=\"text-sm px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2 whitespace-nowrap\"\n (click)=\"sort(column)\"\n >\n <span class=\"inline-flex items-center gap-1\">\n @if (isTemplateRef(column.header)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.header)\"></ng-container>\n } @else {\n <span>{{ column.header }}</span>\n }\n @if (isSortable(column)) {\n <span class=\"text-[0.65rem] opacity-70 min-w-3 inline-block\">{{ getSortIcon(column) }}</span>\n }\n </span>\n </th>\n }\n\n </tr>\n\n <!-- Per-column filter row (wide screens only; collapses into a panel below 640px).\n `font-normal` on the cells is load-bearing: these are `th` elements, which the\n browser renders bold, and the filter inputs and selects inside them inherit\n that. Filter controls are form fields, not headings, and must not look bold.\n The stacked filter panel renders outside any `th`, so it is unaffected. -->\n @if (hasColumnFilters && !filtersCollapsed) {\n <tr class=\"bg-base-100 border-b border-base-300 font-normal\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 font-normal\"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2 font-normal\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n <!-- Every filter renders as an ordinary control right under its\n header. The rich types used to hide behind a button that opened\n a floating panel, which cost a click to discover, a click to\n apply, and hid whether a column was even filterable. -->\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: column, idScope: 'inline' }\"\n [ngTemplateOutlet]=\"filterField\"\n ></ng-container>\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <tr>\n @if (hasSelection) {\n <td class=\"px-2 py-3\">\n <mn-skeleton [data]=\"{ shape: 'rectangle', width: '1rem', height: '1rem' }\"></mn-skeleton>\n </td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n >\n @if (isTemplateRef(column.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.skeleton)\"></ng-container>\n } @else {\n <mn-skeleton [data]=\"getColumnSkeletonData(column)\"></mn-skeleton>\n }\n </td>\n }\n </tr>\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </td>\n </tr>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <tr class=\"bg-base-100\">\n <td [attr.colspan]=\"totalColumnCount\" class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </td>\n </tr>\n }\n\n <!-- Data rows -->\n @for (row of paginatedItems; track trackByID($index, row); let odd = $odd; let last = $last) {\n <tr\n class=\"bg-base-100 transition-colors duration-150 hover:cursor-pointer\"\n [ngClass]=\"{'bg-primary/10': isSelected(row)}\"\n [class.bg-base-200]=\"!isSelected(row) && odd && dataSource.appearance?.striped\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onRowClick\"\n [class.border-b]=\"!last\"\n [class.border-base-300]=\"!last\"\n [class.border-b-1]=\"last\"\n [class.border-black]=\"last\"\n [class.shadow-3xl]=\"last\"\n (click)=\"onRowClick(row)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <td (click)=\"$event.stopPropagation()\" class=\"w-10 text-center px-2 py-2\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(row)\"\n [checked]=\"isSelected(row)\"\n [props]=\"{ id: 'mn-table-row-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n [attr.title]=\"cellTitle(column, row)\"\n [class.text-left]=\"(column.align ?? 'left') === 'left'\"\n [class.text-center]=\"column.align === 'center'\"\n [class.text-right]=\"column.align === 'right'\"\n [class.truncate]=\"widthsArePinned\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n [style.width]=\"columnWidth(column)\"\n class=\"text-xs px-2 py-1 @min-[640px]:px-4 @min-[640px]:py-2\"\n >\n @if (column.actions) {\n <!-- Actions column: inline command buttons that collapse into a \u22EF menu\n once the table is narrower than 450px (container query), for every\n row with actions. A row with no visible actions renders nothing.\n A row with a single icon-only action never collapses: one icon button\n is narrower than the \u22EF trigger it would hide behind, so folding it\n only costs a tap. -->\n @if (hasRowActions(column, row)) {\n <div class=\"inline-flex items-center gap-1\"\n [class.justify-end]=\"(column.align ?? 'left') === 'right'\">\n @if (collapsesRowActions(column, row)) {\n <span class=\"hidden items-center gap-1 @min-[450px]:inline-flex\">\n <ng-container [ngTemplateOutletContext]=\"{ column: column, row: row }\"\n [ngTemplateOutlet]=\"actionButtons\"></ng-container>\n </span>\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <span class=\"@min-[450px]:hidden\" (click)=\"$event.stopPropagation()\">\n <mn-lib-dropdown [datasource]=\"{\n id: actionsDropdownId(column, row),\n actions: rowDropdownActions(column, row),\n menuLabel: $any(column.header),\n size: 'sm'\n }\"></mn-lib-dropdown>\n </span>\n } @else {\n <span class=\"inline-flex items-center gap-1\">\n <ng-container [ngTemplateOutletContext]=\"{ column: column, row: row }\"\n [ngTemplateOutlet]=\"actionButtons\"></ng-container>\n </span>\n }\n </div>\n }\n } @else if (column.cellSm) {\n <!-- Default cell: hidden below the cellSm breakpoint -->\n <span [mnShowAbove]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n </span>\n <!-- Small cell: shown only below the cellSm breakpoint -->\n <span [mnShowBelow]=\"column.cellSm.below\">\n @if (isTemplateRef(column.cellSm.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cellSm.cell)\"></ng-container>\n } @else {\n {{ getCellSmValue(column, row) }}\n }\n </span>\n } @else {\n @if (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"\n [ngTemplateOutlet]=\"$any(column.cell)\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\n }\n }\n </td>\n }\n\n </tr>\n }\n }\n </tbody>\n </table>\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-table\"\n></mn-collection-pagination>\n</div>\n\n<!-- Single source of truth for every filter control, reused by the inline header\n row and the small-screen panel. `idScope` keeps element ids unique across\n both placements. -->\n<ng-template #filterField let-column let-idScope=\"idScope\">\n @switch (filterTypeOf(column)) {\n @case ('select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @case ('multi-select') {\n <mn-lib-multi-select\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"multiFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getFilterMultiSelectOptions(column),\n placeholder: column.filterPlaceholder ?? '',\n collapsePlaceholder: filterSelectedLabel,\n collapseThreshold: 1,\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-multi-select>\n }\n @case ('boolean') {\n <mn-lib-select\n (ngModelChange)=\"onBooleanFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"booleanFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n options: getBooleanFilterOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n ></mn-lib-select>\n }\n @default {\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onColumnFilter(column, $event)\"\n [disabled]=\"column.filterDisabled ?? false\"\n [ngModel]=\"textFilterValue(column)\"\n [props]=\"{\n id: 'mn-table-filter-' + idScope + '-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n ariaLabel: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n ></mn-lib-input-field>\n }\n }\n</ng-template>\n\n<!-- Inline action buttons for an actions column, shared by the wide-table layout and\n (implicitly) mirrored by the \u22EF menu below 450px. -->\n<ng-template #actionButtons let-column=\"column\" let-row=\"row\">\n @for (action of visibleRowActions(column, row); track $index) {\n <button\n mnButton\n type=\"button\"\n [data]=\"{\n size: 'sm',\n variant: 'text',\n color: rowActionColor(action, row),\n disabled: isRowActionDisabled(action, row)\n }\"\n class=\"cursor-pointer\"\n (click)=\"$event.stopPropagation(); runRowAction(action, row)\"\n [attr.aria-label]=\"rowActionLabel(action, row)\"\n [attr.title]=\"showActionLabel(column, action, row) ? null : rowActionLabel(action, row)\"\n >\n @if (showActionIcon(column, action, row)) {\n <span class=\"inline-flex items-center shrink-0\">\n @let icon = rowActionIcon(action, row);\n @if (isTemplateRef(icon)) {\n <ng-container [ngTemplateOutlet]=\"icon\"></ng-container>\n } @else {\n <!-- Data icon: sized here to match the sm button's text, so a caller can\n declare the action in TypeScript without owning a template. -->\n <svg [lucideIcon]=\"$any(icon)\" [size]=\"17\"></svg>\n }\n </span>\n }\n @if (showActionLabel(column, action, row)) {\n <span>{{ rowActionLabel(action, row) }}</span>\n }\n </button>\n }\n</ng-template>\n" }]
|
|
9242
9277
|
}], ctorParameters: () => [], propDecorators: { sortChange: [{
|
|
9243
9278
|
type: Output
|
|
9244
9279
|
}], rowClick: [{
|
|
@@ -11265,11 +11300,11 @@ class MnList extends MnSelectableCollectionBase {
|
|
|
11265
11300
|
return this.resolveLabel(undefined, 'mnCollection.selectAll', 'Select all');
|
|
11266
11301
|
}
|
|
11267
11302
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
11268
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n
|
|
11303
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n isSearchable ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (isSearchable) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: selectAllLabel, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11269
11304
|
}
|
|
11270
11305
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
|
|
11271
11306
|
type: Component,
|
|
11272
|
-
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n
|
|
11307
|
+
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, FormsModule, MnCheckbox, MnInputField, MnSkeleton, MnCollectionPagination, LucideDynamicIcon], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n isSearchable ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (isSearchable) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-list-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- List wrapper -->\n<div\n #collectionBody\n [style.min-height.px]=\"bodyMinHeight\"\n class=\"w-full\"\n [class.border]=\"dataSource.appearance?.bordered\"\n [class.border-base-300]=\"dataSource.appearance?.bordered\"\n [class.rounded]=\"dataSource.appearance?.bordered\"\n role=\"list\"\n [attr.aria-label]=\"listRegionLabel\"\n>\n <!-- Loading state -->\n @if (isLoadingState) {\n @for (_ of skeletonRows; track $index) {\n <div [class.py-2]=\"dataSource.appearance?.compact\" class=\"px-4 py-3\" role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-1\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n } @else if (isErrorState) {\n <!-- Error state -->\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-error\">\n <p class=\"text-sm\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\" role=\"listitem\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div class=\"flex flex-col items-center gap-2 text-base-content/50\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n }\n\n <!-- Select all (multi-select) -->\n @if (isMultiSelect && filteredItems.length > 0) {\n <div class=\"flex items-center gap-2 px-4 py-2 bg-base-200 text-sm\">\n <mn-lib-checkbox\n (checkedChange)=\"toggleAll()\"\n [checked]=\"allSelected\"\n [props]=\"{ id: 'mn-list-select-all', label: selectAllLabel, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n @if (dataSource.appearance?.dividers !== false) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n\n <!-- Data items -->\n @for (item of paginatedItems; track trackByID($index, item); let odd = $odd; let last = $last) {\n <div\n class=\"flex items-center gap-2 bg-base-100 transition-colors duration-150\"\n [ngClass]=\"{'bg-primary/10': isSelected(item)}\"\n [class.bg-base-200]=\"!isSelected(item) && odd && dataSource.appearance?.dividers !== false\"\n [class.hover:bg-base-200]=\"dataSource.appearance?.hover !== false\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n [class.px-4]=\"true\"\n [class.py-3]=\"!dataSource.appearance?.compact\"\n [class.py-2]=\"dataSource.appearance?.compact\"\n role=\"listitem\"\n (keyup.enter)=\"onItemClick(item)\"\n (click)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div (click)=\"$event.stopPropagation()\" class=\"shrink-0\">\n <mn-lib-checkbox\n (checkedChange)=\"toggle(item)\"\n [checked]=\"isSelected(item)\"\n [props]=\"{ id: 'mn-list-item-' + $index, size: 'sm' }\"\n ></mn-lib-checkbox>\n </div>\n }\n\n <!-- Item content via template -->\n <div class=\"flex-1 min-w-0\">\n <ng-container\n [ngTemplateOutlet]=\"dataSource.itemTemplate\"\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n ></ng-container>\n </div>\n </div>\n @if (!last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\n }\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-list\"\n></mn-collection-pagination>\n" }]
|
|
11273
11308
|
}], propDecorators: { itemClick: [{
|
|
11274
11309
|
type: Output
|
|
11275
11310
|
}], collectionBody: [{
|
|
@@ -11424,11 +11459,11 @@ class MnGrid extends MnCollectionBase {
|
|
|
11424
11459
|
return this.resolveLabel(undefined, 'mnCollection.loading', 'Loading');
|
|
11425
11460
|
}
|
|
11426
11461
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
11427
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n
|
|
11462
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, host: { classAttribute: "block" }, viewQueries: [{ propertyName: "collectionBody", first: true, predicate: ["collectionBody"], descendants: true }], usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n isSearchable ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (isSearchable) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n [attr.aria-label]=\"loadingLabel\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n [attr.aria-label]=\"gridRegionLabel\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCollectionPagination, selector: "mn-collection-pagination", inputs: ["idPrefix", "isPaginated", "isServerPaginated", "showLoadMore", "loadingMoreRows", "currentPage", "pageSize", "totalPages", "totalItemCount", "visiblePages", "pageSizeSelectOptions", "labels"], outputs: ["loadMore", "pageChange", "pageSizeChange"] }, { kind: "component", type: LucideDynamicIcon, selector: "svg[lucideIcon]", inputs: ["lucideIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11428
11463
|
}
|
|
11429
11464
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, decorators: [{
|
|
11430
11465
|
type: Component,
|
|
11431
|
-
args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination, LucideDynamicIcon], host: { class: 'block' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n
|
|
11466
|
+
args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination, LucideDynamicIcon], host: { class: 'block' }, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: left template, then search + right template. Below 375px each group\n takes its own full-width row; from 375px up the left group sits at the start\n and the search group at the end. Structure and classes mirror mn-table so all\n collections behave identically at every width. -->\n@if (\n isSearchable ||\n dataSource.toolbarLeftTemplate ||\n dataSource.toolbarRightTemplate ||\n dataSource.toolbarTemplate\n) {\n <div class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center justify-between gap-2 mb-3\">\n <div class=\"flex items-center gap-2 w-full min-[375px]:w-auto\">\n @if (dataSource.toolbarLeftTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarLeftTemplate\"></ng-container>\n </div>\n }\n </div>\n <div\n class=\"flex flex-col min-[375px]:flex-row min-[375px]:items-center gap-2 w-full min-[375px]:flex-1 min-[480px]:flex-none min-[480px]:w-auto\">\n @if (isSearchable) {\n <!-- Enter must not submit a surrounding form: the search box is the\n collection's own chrome, not a field of whatever form it sits in. -->\n <mn-lib-input-field\n (keydown.enter)=\"$event.preventDefault()\"\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n ariaLabel: dataSource.searchPlaceholder ?? 'Search...',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n ></mn-lib-input-field>\n }\n <!-- `toolbarTemplate` is the deprecated name for this slot; honouring it here\n keeps existing callers rendering exactly where they used to. -->\n @if (dataSource.toolbarRightTemplate ?? dataSource.toolbarTemplate; as rightTemplate) {\n <div class=\"flex flex-col w-full min-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"rightTemplate\"></ng-container>\n </div>\n }\n </div>\n </div>\n}\n\n<!-- Body: skeleton/data swap region. Wrapper holds its height during a server reload. -->\n<div #collectionBody [style.min-height.px]=\"bodyMinHeight\">\n<!-- Loading state -->\n @if (isLoadingState) {\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n aria-busy=\"true\"\n [attr.aria-label]=\"loadingLabel\"\n role=\"list\"\n >\n @for (_ of skeletonRows; track $index) {\n <div role=\"listitem\">\n @if (isTemplateRef(dataSource.skeleton)) {\n <ng-container [ngTemplateOutlet]=\"$any(dataSource.skeleton)\"></ng-container>\n } @else {\n <div class=\"flex flex-col gap-2 p-4 border border-base-300 rounded-lg\">\n @for (line of skeletonLines; track $index) {\n <mn-skeleton [data]=\"line\"></mn-skeleton>\n }\n </div>\n }\n </div>\n }\n </div>\n } @else if (isErrorState) {\n <!-- Error state -->\n @if (dataSource.errorTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.errorTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] py-8 rounded-xl border border-dashed border-error/30 bg-base-100\">\n <p class=\"text-sm text-error\">{{ dataSource.errorMessage }}</p>\n </div>\n }\n} @else {\n <!-- Empty state: a caller-provided template/component (rendered unwrapped, full\n control over its own layout) or, when none is given, the default text. -->\n @if (filteredItems.length === 0) {\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex flex-col items-center justify-center gap-2 min-h-[6rem] py-8 rounded-xl border border-dashed border-base-content/15 bg-base-100 text-base-content/40\">\n @if (dataSource.emptyIcon !== null) {\n <svg [lucideIcon]=\"dataSource.emptyIcon ?? defaultEmptyIcon\" [size]=\"32\" [strokeWidth]=\"1.5\"></svg>\n }\n <p class=\"text-sm\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n } @else {\n <!-- Card grid -->\n <div\n [class]=\"gridClasses\"\n [style.gap]=\"gridGap\"\n [style.grid-template-columns]=\"autoTemplateColumns\"\n [attr.aria-label]=\"gridRegionLabel\"\n role=\"list\"\n >\n @for (item of paginatedItems; track trackByID($index, item)) {\n <div\n (click)=\"onItemClick(item)\"\n (keyup.enter)=\"onItemClick(item)\"\n [attr.tabindex]=\"dataSource.onItemClick ? 0 : null\"\n [class.cursor-pointer]=\"!!dataSource.onItemClick\"\n class=\"transition-colors duration-150\"\n role=\"listitem\"\n >\n <ng-container\n [ngTemplateOutletContext]=\"{ $implicit: item, data: item }\"\n [ngTemplateOutlet]=\"dataSource.cardTemplate\"\n ></ng-container>\n </div>\n }\n </div>\n }\n}\n</div>\n\n<!-- Load more + pagination -->\n<mn-collection-pagination\n (loadMore)=\"loadMoreRows()\"\n (pageChange)=\"goToPage($event)\"\n (pageSizeChange)=\"onPageSizeChange($event)\"\n [currentPage]=\"currentPage\"\n [isPaginated]=\"isPaginated\"\n [isServerPaginated]=\"isServerPaginated\"\n [labels]=\"dataSource.labels\"\n [loadingMoreRows]=\"loadingMoreRows\"\n [pageSizeSelectOptions]=\"pageSizeSelectOptions\"\n [pageSize]=\"pageSize\"\n [showLoadMore]=\"showLoadMore\"\n [totalItemCount]=\"totalItemCount\"\n [totalPages]=\"totalPages\"\n [visiblePages]=\"visiblePages\"\n idPrefix=\"mn-grid\"\n></mn-collection-pagination>\n" }]
|
|
11432
11467
|
}], propDecorators: { itemClick: [{
|
|
11433
11468
|
type: Output
|
|
11434
11469
|
}], collectionBody: [{
|