mn-angular-lib 1.0.76 → 1.0.77
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.
- package/fesm2022/mn-angular-lib.mjs +599 -624
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/src/lib/features/mn-grid/mn-grid.component.css +63 -0
- package/src/lib/features/mn-modal/components/mn-modal-shell/mn-modal-shell.component.css +9 -0
- package/src/lib/features/mn-skeleton/mn-skeleton.css +9 -1
- package/types/mn-angular-lib.d.ts +525 -421
|
@@ -3784,6 +3784,62 @@ var ColumnSortType;
|
|
|
3784
3784
|
ColumnSortType["NONE"] = "NONE";
|
|
3785
3785
|
})(ColumnSortType || (ColumnSortType = {}));
|
|
3786
3786
|
|
|
3787
|
+
const mnSkeletonVariants = tv({
|
|
3788
|
+
base: 'bg-base-300 relative overflow-hidden',
|
|
3789
|
+
variants: {
|
|
3790
|
+
shape: {
|
|
3791
|
+
// Each shape carries a sensible default size so a bare <mn-skeleton> stays
|
|
3792
|
+
// visible. The width/height inputs are applied as inline styles, which win
|
|
3793
|
+
// over these utility classes whenever the consumer supplies dimensions.
|
|
3794
|
+
rectangle: 'rounded-md h-4',
|
|
3795
|
+
circle: 'rounded-full h-10 w-10',
|
|
3796
|
+
text: 'rounded h-4',
|
|
3797
|
+
},
|
|
3798
|
+
},
|
|
3799
|
+
defaultVariants: {
|
|
3800
|
+
shape: 'rectangle',
|
|
3801
|
+
},
|
|
3802
|
+
});
|
|
3803
|
+
|
|
3804
|
+
class MnSkeleton {
|
|
3805
|
+
data = {};
|
|
3806
|
+
// Skeletons are decorative loading placeholders; hide them from assistive tech.
|
|
3807
|
+
// The container that swaps skeleton↔content should carry aria-busy / a live region.
|
|
3808
|
+
ariaHidden = 'true';
|
|
3809
|
+
get hostClasses() {
|
|
3810
|
+
return mnSkeletonVariants({ shape: this.data.shape });
|
|
3811
|
+
}
|
|
3812
|
+
get width() {
|
|
3813
|
+
return this.data.width ?? null;
|
|
3814
|
+
}
|
|
3815
|
+
get height() {
|
|
3816
|
+
return this.data.height ?? null;
|
|
3817
|
+
}
|
|
3818
|
+
get isAnimated() {
|
|
3819
|
+
return this.data.animated !== false;
|
|
3820
|
+
}
|
|
3821
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3822
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSkeleton, isStandalone: true, selector: "mn-skeleton", inputs: { data: "data" }, host: { properties: { "attr.aria-hidden": "this.ariaHidden", "class": "this.hostClasses", "style.width": "this.width", "style.height": "this.height" } }, ngImport: i0, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,var(--color-base-100) 65%,transparent) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.mn-skeleton-shimmer-bar{animation:none}}\n"] });
|
|
3823
|
+
}
|
|
3824
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, decorators: [{
|
|
3825
|
+
type: Component,
|
|
3826
|
+
args: [{ selector: 'mn-skeleton', standalone: true, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,color-mix(in srgb,var(--color-base-100) 65%,transparent) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}@media(prefers-reduced-motion:reduce){.mn-skeleton-shimmer-bar{animation:none}}\n"] }]
|
|
3827
|
+
}], propDecorators: { data: [{
|
|
3828
|
+
type: Input
|
|
3829
|
+
}], ariaHidden: [{
|
|
3830
|
+
type: HostBinding,
|
|
3831
|
+
args: ['attr.aria-hidden']
|
|
3832
|
+
}], hostClasses: [{
|
|
3833
|
+
type: HostBinding,
|
|
3834
|
+
args: ['class']
|
|
3835
|
+
}], width: [{
|
|
3836
|
+
type: HostBinding,
|
|
3837
|
+
args: ['style.width']
|
|
3838
|
+
}], height: [{
|
|
3839
|
+
type: HostBinding,
|
|
3840
|
+
args: ['style.height']
|
|
3841
|
+
}] } });
|
|
3842
|
+
|
|
3787
3843
|
/**
|
|
3788
3844
|
* Attribute directive that applies responsive-hiding classes to table cells/headers.
|
|
3789
3845
|
* Hides the element by default and shows it as `table-cell` at the specified breakpoint.
|
|
@@ -3919,39 +3975,50 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
3919
3975
|
type: Input
|
|
3920
3976
|
}] } });
|
|
3921
3977
|
|
|
3922
|
-
|
|
3978
|
+
/**
|
|
3979
|
+
* Shared chrome for MnLib collection components (table, list, grid):
|
|
3980
|
+
* data subscription, client/server search, every pagination mode, load-more,
|
|
3981
|
+
* skeleton-row count, empty-state plumbing, common i18n key resolution and
|
|
3982
|
+
* toolbar change-detection.
|
|
3983
|
+
*
|
|
3984
|
+
* Concrete components extend this (or {@link MnSelectableCollectionBase}) and
|
|
3985
|
+
* implement only their rendering. The class is decorated `@Directive()` so it can
|
|
3986
|
+
* declare `@Input`s and use `inject()` while remaining abstract.
|
|
3987
|
+
*
|
|
3988
|
+
* Init runs in a fixed order (see {@link ngOnInit}); subclasses hook in via the
|
|
3989
|
+
* `protected` template methods rather than overriding `ngOnInit`.
|
|
3990
|
+
*/
|
|
3991
|
+
class MnCollectionBase {
|
|
3923
3992
|
dataSource;
|
|
3924
|
-
sortChange = new EventEmitter();
|
|
3925
|
-
selectionChange = new EventEmitter();
|
|
3926
|
-
rowClick = new EventEmitter();
|
|
3927
3993
|
filteredItems = [];
|
|
3928
3994
|
paginatedItems = [];
|
|
3929
3995
|
searchValue = '';
|
|
3930
3996
|
loadingMoreRows = false;
|
|
3931
|
-
currentSort = null;
|
|
3932
|
-
selectedIds = new Set();
|
|
3933
3997
|
currentPage = 1;
|
|
3934
3998
|
pageSize = 10;
|
|
3935
|
-
/** Per-column filter values keyed by column key. */
|
|
3936
|
-
columnFilters = {};
|
|
3937
3999
|
cdr = inject(ChangeDetectorRef);
|
|
3938
4000
|
lang = inject(MnLanguageService);
|
|
4001
|
+
/** Prefix used in validation error messages, e.g. `MnList`. Overridden by subclasses. */
|
|
4002
|
+
componentName = 'MnCollection';
|
|
3939
4003
|
dataSubscription;
|
|
3940
4004
|
searchSubject = new Subject();
|
|
3941
4005
|
searchSubscription;
|
|
3942
4006
|
langSubscription;
|
|
3943
|
-
/** Tracks the previous toolbar
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
4007
|
+
/** Tracks the previous toolbar template reference for change detection. */
|
|
4008
|
+
previousToolbarTemplate;
|
|
4009
|
+
// ── Template-method hooks ──
|
|
4010
|
+
/** Whether the component delegates search to the consumer (server-side). */
|
|
4011
|
+
get isServerSearched() {
|
|
4012
|
+
return !!this.dataSource.onServerSearch;
|
|
4013
|
+
}
|
|
4014
|
+
get isPaginated() {
|
|
4015
|
+
const mode = this.dataSource.paginationMode;
|
|
4016
|
+
return mode === 'paginated' || mode === 'client-side-pagination';
|
|
4017
|
+
}
|
|
4018
|
+
/** Whether the component delegates pagination to the consumer (server-side). */
|
|
4019
|
+
get isServerPaginated() {
|
|
4020
|
+
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
4021
|
+
return mode === 'paginated' || mode === 'load-more';
|
|
3955
4022
|
}
|
|
3956
4023
|
get showLoadMore() {
|
|
3957
4024
|
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
@@ -3964,109 +4031,7 @@ class MnTable {
|
|
|
3964
4031
|
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
3965
4032
|
return mode === 'load-more' && hasMore;
|
|
3966
4033
|
}
|
|
3967
|
-
|
|
3968
|
-
this.dataSubscription?.unsubscribe();
|
|
3969
|
-
this.searchSubscription?.unsubscribe();
|
|
3970
|
-
this.langSubscription?.unsubscribe();
|
|
3971
|
-
}
|
|
3972
|
-
// ── Search ──
|
|
3973
|
-
get isPaginated() {
|
|
3974
|
-
const mode = this.dataSource.paginationMode;
|
|
3975
|
-
return mode === 'paginated' || mode === 'client-side-pagination';
|
|
3976
|
-
}
|
|
3977
|
-
// ── Column Filters ──
|
|
3978
|
-
/** Whether any column has filtering enabled. */
|
|
3979
|
-
get hasColumnFilters() {
|
|
3980
|
-
return this.dataSource.columns.some(c => c.filterable);
|
|
3981
|
-
}
|
|
3982
|
-
/** Updates a column filter value and re-applies filtering. */
|
|
3983
|
-
onColumnFilter(columnKey, value) {
|
|
3984
|
-
this.columnFilters[columnKey] = value;
|
|
3985
|
-
this.currentPage = 1;
|
|
3986
|
-
this.applyFilterAndSort(false);
|
|
3987
|
-
this.cdr.markForCheck();
|
|
3988
|
-
}
|
|
3989
|
-
// ── Sorting ──
|
|
3990
|
-
sort(column) {
|
|
3991
|
-
if (!column.sortType || column.sortType === ColumnSortType.NONE)
|
|
3992
|
-
return;
|
|
3993
|
-
if (this.currentSort?.columnKey === column.key) {
|
|
3994
|
-
this.currentSort = this.currentSort.direction === 'asc'
|
|
3995
|
-
? { columnKey: column.key, direction: 'desc' }
|
|
3996
|
-
: null;
|
|
3997
|
-
}
|
|
3998
|
-
else {
|
|
3999
|
-
this.currentSort = { columnKey: column.key, direction: 'asc' };
|
|
4000
|
-
}
|
|
4001
|
-
this.sortChange.emit(this.currentSort);
|
|
4002
|
-
this.applyFilterAndSort(false);
|
|
4003
|
-
}
|
|
4004
|
-
getSortIcon(column) {
|
|
4005
|
-
if (!this.currentSort || this.currentSort.columnKey !== column.key)
|
|
4006
|
-
return '';
|
|
4007
|
-
return this.currentSort.direction === 'asc' ? '▲' : '▼';
|
|
4008
|
-
}
|
|
4009
|
-
isSortable(column) {
|
|
4010
|
-
return !!column.sortType && column.sortType !== ColumnSortType.NONE;
|
|
4011
|
-
}
|
|
4012
|
-
// ── Selection ──
|
|
4013
|
-
isSelected(row) {
|
|
4014
|
-
return this.selectedIds.has(this.dataSource.getID(row));
|
|
4015
|
-
}
|
|
4016
|
-
toggleRow(row) {
|
|
4017
|
-
const id = this.dataSource.getID(row);
|
|
4018
|
-
const mode = this.dataSource.selectionMode ?? 'none';
|
|
4019
|
-
if (mode === 'single') {
|
|
4020
|
-
this.selectedIds.clear();
|
|
4021
|
-
this.selectedIds.add(id);
|
|
4022
|
-
}
|
|
4023
|
-
else if (mode === 'multi') {
|
|
4024
|
-
if (this.selectedIds.has(id)) {
|
|
4025
|
-
this.selectedIds.delete(id);
|
|
4026
|
-
}
|
|
4027
|
-
else {
|
|
4028
|
-
this.selectedIds.add(id);
|
|
4029
|
-
}
|
|
4030
|
-
}
|
|
4031
|
-
this.emitSelection();
|
|
4032
|
-
}
|
|
4033
|
-
toggleAll() {
|
|
4034
|
-
if (this.selectedIds.size === this.filteredItems.length) {
|
|
4035
|
-
this.selectedIds.clear();
|
|
4036
|
-
}
|
|
4037
|
-
else {
|
|
4038
|
-
this.filteredItems.forEach(row => this.selectedIds.add(this.dataSource.getID(row)));
|
|
4039
|
-
}
|
|
4040
|
-
this.emitSelection();
|
|
4041
|
-
}
|
|
4042
|
-
get allSelected() {
|
|
4043
|
-
return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
|
|
4044
|
-
}
|
|
4045
|
-
get hasSelection() {
|
|
4046
|
-
return (this.dataSource.selectionMode ?? 'none') !== 'none';
|
|
4047
|
-
}
|
|
4048
|
-
get isMultiSelect() {
|
|
4049
|
-
return this.dataSource.selectionMode === 'multi';
|
|
4050
|
-
}
|
|
4051
|
-
// ── Row interaction ──
|
|
4052
|
-
onRowClick(row) {
|
|
4053
|
-
if (this.hasSelection) {
|
|
4054
|
-
this.toggleRow(row);
|
|
4055
|
-
}
|
|
4056
|
-
this.dataSource.onRowClick?.(row);
|
|
4057
|
-
this.rowClick.emit(row);
|
|
4058
|
-
}
|
|
4059
|
-
// ── Pagination ──
|
|
4060
|
-
/** Whether the table delegates pagination to the consumer (server-side). */
|
|
4061
|
-
get isServerPaginated() {
|
|
4062
|
-
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
4063
|
-
return mode === 'paginated' || mode === 'load-more';
|
|
4064
|
-
}
|
|
4065
|
-
/** Whether the table delegates search to the consumer (server-side). */
|
|
4066
|
-
get isServerSearched() {
|
|
4067
|
-
return !!this.dataSource.onServerSearch;
|
|
4068
|
-
}
|
|
4069
|
-
// ── Paginated Mode ──
|
|
4034
|
+
// ── Lifecycle ──
|
|
4070
4035
|
/** Total number of items, accounting for server-side pagination. */
|
|
4071
4036
|
get totalItemCount() {
|
|
4072
4037
|
if (this.isServerPaginated && this.dataSource.totalItems != null) {
|
|
@@ -4077,83 +4042,104 @@ class MnTable {
|
|
|
4077
4042
|
get totalPages() {
|
|
4078
4043
|
return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
|
|
4079
4044
|
}
|
|
4045
|
+
get resolvedPageSizeOptions() {
|
|
4046
|
+
return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
4047
|
+
}
|
|
4048
|
+
// ── Search ──
|
|
4049
|
+
/** Page-size options formatted for mn-select. */
|
|
4050
|
+
get pageSizeSelectOptions() {
|
|
4051
|
+
return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
|
|
4052
|
+
}
|
|
4053
|
+
get visiblePages() {
|
|
4054
|
+
const total = this.totalPages;
|
|
4055
|
+
const current = this.currentPage;
|
|
4056
|
+
const maxVisible = 3;
|
|
4057
|
+
let start = Math.max(1, current - Math.floor(maxVisible / 2));
|
|
4058
|
+
let end = start + maxVisible - 1;
|
|
4059
|
+
if (end > total) {
|
|
4060
|
+
end = total;
|
|
4061
|
+
start = Math.max(1, end - maxVisible + 1);
|
|
4062
|
+
}
|
|
4063
|
+
const pages = [];
|
|
4064
|
+
for (let i = start; i <= end; i++) {
|
|
4065
|
+
pages.push(i);
|
|
4066
|
+
}
|
|
4067
|
+
return pages;
|
|
4068
|
+
}
|
|
4069
|
+
// ── Pagination ──
|
|
4070
|
+
get skeletonRows() {
|
|
4071
|
+
return Array.from({ length: this.dataSource.skeletonRowCount ?? 5 });
|
|
4072
|
+
}
|
|
4080
4073
|
ngOnInit() {
|
|
4081
4074
|
this.validateDataSource();
|
|
4082
4075
|
this.resolveTranslationKeys();
|
|
4083
|
-
this.currentSort = this.dataSource.defaultSort ?? null;
|
|
4084
4076
|
this.pageSize = this.dataSource.pageSize ?? 10;
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
this.columnFilters[col.key] = '';
|
|
4089
|
-
}
|
|
4090
|
-
}
|
|
4091
|
-
// Pre-select rows from initialSelectedIds if provided
|
|
4092
|
-
if (this.dataSource.initialSelectedIds?.length) {
|
|
4093
|
-
for (const id of this.dataSource.initialSelectedIds) {
|
|
4094
|
-
this.selectedIds.add(id);
|
|
4095
|
-
}
|
|
4096
|
-
this.emitSelection();
|
|
4097
|
-
}
|
|
4098
|
-
this.applyFilterAndSort(false);
|
|
4099
|
-
// Skip the initial BehaviorSubject emission (already handled above)
|
|
4100
|
-
// to avoid triggering markForCheck during the first change detection cycle.
|
|
4077
|
+
this.beforeInitialFilter();
|
|
4078
|
+
this.applyFilter(false);
|
|
4079
|
+
// Skip the initial BehaviorSubject emission (already handled above).
|
|
4101
4080
|
this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
|
|
4102
|
-
this.
|
|
4081
|
+
this.applyFilter(false);
|
|
4103
4082
|
this.cdr.markForCheck();
|
|
4104
4083
|
});
|
|
4105
4084
|
this.searchSubscription = this.searchSubject
|
|
4106
4085
|
.pipe(debounceTime(300))
|
|
4107
4086
|
.subscribe(value => {
|
|
4108
4087
|
this.searchValue = value;
|
|
4109
|
-
this.
|
|
4088
|
+
this.applyFilter(true);
|
|
4110
4089
|
this.cdr.markForCheck();
|
|
4111
4090
|
});
|
|
4112
|
-
// Re-resolve translation keys
|
|
4091
|
+
// Re-resolve translation keys whenever the locale changes.
|
|
4113
4092
|
this.langSubscription = this.lang.locale$.pipe(skip(1)).subscribe(() => {
|
|
4114
4093
|
this.resolveTranslationKeys();
|
|
4115
4094
|
this.cdr.markForCheck();
|
|
4116
4095
|
});
|
|
4117
4096
|
}
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
for (const col of this.dataSource.columns) {
|
|
4124
|
-
if (col.headerKey) {
|
|
4125
|
-
col.header = this.lang.t(col.headerKey);
|
|
4126
|
-
}
|
|
4127
|
-
if (col.filterPlaceholderKey) {
|
|
4128
|
-
col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
|
|
4129
|
-
}
|
|
4130
|
-
}
|
|
4131
|
-
if (this.dataSource.emptyMessageKey) {
|
|
4132
|
-
this.dataSource.emptyMessage = this.lang.t(this.dataSource.emptyMessageKey);
|
|
4133
|
-
}
|
|
4134
|
-
if (this.dataSource.searchPlaceholderKey) {
|
|
4135
|
-
this.dataSource.searchPlaceholder = this.lang.t(this.dataSource.searchPlaceholderKey);
|
|
4136
|
-
}
|
|
4137
|
-
if (this.dataSource.labels) {
|
|
4138
|
-
if (this.dataSource.labels.loadMoreKey) {
|
|
4139
|
-
this.dataSource.labels.loadMore = this.lang.t(this.dataSource.labels.loadMoreKey);
|
|
4140
|
-
}
|
|
4141
|
-
if (this.dataSource.labels.rowsPerPageKey) {
|
|
4142
|
-
this.dataSource.labels.rowsPerPage = this.lang.t(this.dataSource.labels.rowsPerPageKey);
|
|
4143
|
-
}
|
|
4097
|
+
ngDoCheck() {
|
|
4098
|
+
const currentTemplate = this.trackedToolbarTemplate;
|
|
4099
|
+
if (currentTemplate !== this.previousToolbarTemplate) {
|
|
4100
|
+
this.previousToolbarTemplate = currentTemplate;
|
|
4101
|
+
this.cdr.markForCheck();
|
|
4144
4102
|
}
|
|
4145
4103
|
}
|
|
4104
|
+
ngOnDestroy() {
|
|
4105
|
+
this.dataSubscription?.unsubscribe();
|
|
4106
|
+
this.searchSubscription?.unsubscribe();
|
|
4107
|
+
this.langSubscription?.unsubscribe();
|
|
4108
|
+
}
|
|
4146
4109
|
onSearch(searchString) {
|
|
4147
4110
|
this.currentPage = 1;
|
|
4148
4111
|
if (this.isServerSearched) {
|
|
4149
4112
|
this.searchValue = searchString;
|
|
4150
|
-
this.dataSource.onServerSearch(searchString);
|
|
4113
|
+
this.dataSource.onServerSearch?.(searchString);
|
|
4151
4114
|
this.cdr.markForCheck();
|
|
4152
4115
|
}
|
|
4153
4116
|
else {
|
|
4154
4117
|
this.searchSubject.next(searchString);
|
|
4155
4118
|
}
|
|
4156
4119
|
}
|
|
4120
|
+
goToPage(page) {
|
|
4121
|
+
if (page < 1 || page > this.totalPages)
|
|
4122
|
+
return;
|
|
4123
|
+
this.currentPage = page;
|
|
4124
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
4125
|
+
this.applyPagination();
|
|
4126
|
+
}
|
|
4127
|
+
else {
|
|
4128
|
+
this.dataSource.onPageChange?.(page);
|
|
4129
|
+
}
|
|
4130
|
+
this.cdr.markForCheck();
|
|
4131
|
+
}
|
|
4132
|
+
onPageSizeChange(newSize) {
|
|
4133
|
+
this.pageSize = newSize;
|
|
4134
|
+
this.currentPage = 1;
|
|
4135
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
4136
|
+
this.applyPagination();
|
|
4137
|
+
}
|
|
4138
|
+
else {
|
|
4139
|
+
this.dataSource.onPageSizeChange?.(newSize);
|
|
4140
|
+
}
|
|
4141
|
+
this.cdr.markForCheck();
|
|
4142
|
+
}
|
|
4157
4143
|
loadMoreRows() {
|
|
4158
4144
|
// Server-side infinite scroll: delegate to consumer callback.
|
|
4159
4145
|
if (this.dataSource.onLoadMore) {
|
|
@@ -4170,12 +4156,245 @@ class MnTable {
|
|
|
4170
4156
|
.then(rows => this.processLoadedRows(rows))
|
|
4171
4157
|
.catch(() => this.loadingMoreRows = false);
|
|
4172
4158
|
}
|
|
4173
|
-
|
|
4174
|
-
return
|
|
4159
|
+
isTemplateRef(value) {
|
|
4160
|
+
return value instanceof TemplateRef;
|
|
4161
|
+
}
|
|
4162
|
+
trackByID = (_index, item) => {
|
|
4163
|
+
return this.dataSource.getID(item);
|
|
4164
|
+
};
|
|
4165
|
+
// ── Template helpers ──
|
|
4166
|
+
/** Runs after pageSize is set but before the first {@link applyFilter}. */
|
|
4167
|
+
beforeInitialFilter() {
|
|
4168
|
+
// no-op by default
|
|
4169
|
+
}
|
|
4170
|
+
/**
|
|
4171
|
+
* Resolves translation keys to display strings via {@link MnLanguageService}.
|
|
4172
|
+
* Subclasses override to resolve their own keys; call `super` to keep these.
|
|
4173
|
+
*/
|
|
4174
|
+
resolveTranslationKeys() {
|
|
4175
|
+
if (this.dataSource.emptyMessageKey) {
|
|
4176
|
+
this.dataSource.emptyMessage = this.lang.t(this.dataSource.emptyMessageKey);
|
|
4177
|
+
}
|
|
4178
|
+
if (this.dataSource.searchPlaceholderKey) {
|
|
4179
|
+
this.dataSource.searchPlaceholder = this.lang.t(this.dataSource.searchPlaceholderKey);
|
|
4180
|
+
}
|
|
4181
|
+
if (this.dataSource.labels) {
|
|
4182
|
+
if (this.dataSource.labels.loadMoreKey) {
|
|
4183
|
+
this.dataSource.labels.loadMore = this.lang.t(this.dataSource.labels.loadMoreKey);
|
|
4184
|
+
}
|
|
4185
|
+
if (this.dataSource.labels.rowsPerPageKey) {
|
|
4186
|
+
this.dataSource.labels.rowsPerPage = this.lang.t(this.dataSource.labels.rowsPerPageKey);
|
|
4187
|
+
}
|
|
4188
|
+
}
|
|
4189
|
+
}
|
|
4190
|
+
// ── Shared internals ──
|
|
4191
|
+
applyPagination() {
|
|
4192
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
4193
|
+
const start = (this.currentPage - 1) * this.pageSize;
|
|
4194
|
+
this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
|
|
4195
|
+
}
|
|
4196
|
+
else {
|
|
4197
|
+
// Server always provides the correct page/slice — no client-side slicing.
|
|
4198
|
+
this.paginatedItems = this.filteredItems;
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
/** Client-side search filtering shared by list and grid. */
|
|
4202
|
+
applySearchFilter(items) {
|
|
4203
|
+
if (!this.isServerSearched && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue && this.searchValue.length > 0) {
|
|
4204
|
+
const term = this.searchValue.toLowerCase();
|
|
4205
|
+
return items.filter(row => this.dataSource.isInSearch(row, term));
|
|
4206
|
+
}
|
|
4207
|
+
return items;
|
|
4208
|
+
}
|
|
4209
|
+
processLoadedRows(rows) {
|
|
4210
|
+
const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
|
|
4211
|
+
this.dataSource.dataRows.next(merged);
|
|
4212
|
+
this.loadingMoreRows = false;
|
|
4213
|
+
this.applyFilter(false);
|
|
4214
|
+
}
|
|
4215
|
+
validateDataSource() {
|
|
4216
|
+
const mode = this.dataSource.paginationMode;
|
|
4217
|
+
if (mode === 'paginated') {
|
|
4218
|
+
if (!this.dataSource.onPageChange) {
|
|
4219
|
+
throw new Error(`[${this.componentName}] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
|
|
4220
|
+
}
|
|
4221
|
+
if (this.dataSource.totalItems == null) {
|
|
4222
|
+
throw new Error(`[${this.componentName}] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
|
|
4223
|
+
}
|
|
4224
|
+
}
|
|
4225
|
+
if (mode === 'load-more' || mode === 'infinite-scroll') {
|
|
4226
|
+
if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
|
|
4227
|
+
throw new Error(`[${this.componentName}] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
4228
|
+
}
|
|
4229
|
+
}
|
|
4230
|
+
// Validate pageSize is one of pageSizeOptions when pagination is active
|
|
4231
|
+
if (mode && mode !== 'none') {
|
|
4232
|
+
const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
4233
|
+
const size = this.dataSource.pageSize ?? 10;
|
|
4234
|
+
if (!options.includes(size)) {
|
|
4235
|
+
throw new Error(`[${this.componentName}] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
|
|
4236
|
+
}
|
|
4237
|
+
}
|
|
4238
|
+
}
|
|
4239
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
4240
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnCollectionBase, isStandalone: true, inputs: { dataSource: "dataSource" }, ngImport: i0 });
|
|
4241
|
+
}
|
|
4242
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionBase, decorators: [{
|
|
4243
|
+
type: Directive
|
|
4244
|
+
}], propDecorators: { dataSource: [{
|
|
4245
|
+
type: Input
|
|
4246
|
+
}] } });
|
|
4247
|
+
|
|
4248
|
+
/**
|
|
4249
|
+
* Extends {@link MnCollectionBase} with single/multi row selection, shared by
|
|
4250
|
+
* components that support it (table, list). Grid extends the plain base instead.
|
|
4251
|
+
*/
|
|
4252
|
+
class MnSelectableCollectionBase extends MnCollectionBase {
|
|
4253
|
+
selectionChange = new EventEmitter();
|
|
4254
|
+
selectedIds = new Set();
|
|
4255
|
+
get allSelected() {
|
|
4256
|
+
return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
|
|
4257
|
+
}
|
|
4258
|
+
get hasSelection() {
|
|
4259
|
+
return (this.dataSource.selectionMode ?? 'none') !== 'none';
|
|
4260
|
+
}
|
|
4261
|
+
get isMultiSelect() {
|
|
4262
|
+
return this.dataSource.selectionMode === 'multi';
|
|
4263
|
+
}
|
|
4264
|
+
isSelected(item) {
|
|
4265
|
+
return this.selectedIds.has(this.dataSource.getID(item));
|
|
4266
|
+
}
|
|
4267
|
+
toggle(item) {
|
|
4268
|
+
const id = this.dataSource.getID(item);
|
|
4269
|
+
const mode = this.dataSource.selectionMode ?? 'none';
|
|
4270
|
+
if (mode === 'single') {
|
|
4271
|
+
this.selectedIds.clear();
|
|
4272
|
+
this.selectedIds.add(id);
|
|
4273
|
+
}
|
|
4274
|
+
else if (mode === 'multi') {
|
|
4275
|
+
if (this.selectedIds.has(id)) {
|
|
4276
|
+
this.selectedIds.delete(id);
|
|
4277
|
+
}
|
|
4278
|
+
else {
|
|
4279
|
+
this.selectedIds.add(id);
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
this.emitSelection();
|
|
4283
|
+
}
|
|
4284
|
+
toggleAll() {
|
|
4285
|
+
if (this.selectedIds.size === this.filteredItems.length) {
|
|
4286
|
+
this.selectedIds.clear();
|
|
4287
|
+
}
|
|
4288
|
+
else {
|
|
4289
|
+
this.filteredItems.forEach(item => this.selectedIds.add(this.dataSource.getID(item)));
|
|
4290
|
+
}
|
|
4291
|
+
this.emitSelection();
|
|
4292
|
+
}
|
|
4293
|
+
/** Seeds selection from `initialSelectedIds` before the first filter pass. */
|
|
4294
|
+
beforeInitialFilter() {
|
|
4295
|
+
super.beforeInitialFilter();
|
|
4296
|
+
if (this.dataSource.initialSelectedIds?.length) {
|
|
4297
|
+
for (const id of this.dataSource.initialSelectedIds) {
|
|
4298
|
+
this.selectedIds.add(id);
|
|
4299
|
+
}
|
|
4300
|
+
this.emitSelection();
|
|
4301
|
+
}
|
|
4302
|
+
}
|
|
4303
|
+
emitSelection() {
|
|
4304
|
+
const rows = (this.dataSource.dataRows.value ?? []).filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
4305
|
+
this.dataSource.selectedRows?.next(rows);
|
|
4306
|
+
this.selectionChange.emit(rows);
|
|
4307
|
+
}
|
|
4308
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelectableCollectionBase, deps: null, target: i0.ɵɵFactoryTarget.Directive });
|
|
4309
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSelectableCollectionBase, isStandalone: true, outputs: { selectionChange: "selectionChange" }, usesInheritance: true, ngImport: i0 });
|
|
4310
|
+
}
|
|
4311
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSelectableCollectionBase, decorators: [{
|
|
4312
|
+
type: Directive
|
|
4313
|
+
}], propDecorators: { selectionChange: [{
|
|
4314
|
+
type: Output
|
|
4315
|
+
}] } });
|
|
4316
|
+
|
|
4317
|
+
/**
|
|
4318
|
+
* Presentational pagination footer shared by every MnLib collection component
|
|
4319
|
+
* (table, list, grid): the load-more button, the page-size selector and the
|
|
4320
|
+
* page navigator. It holds no state — the host component owns pagination state
|
|
4321
|
+
* (via {@link import('./mn-collection-base.directive').MnCollectionBase}) and
|
|
4322
|
+
* reacts to the outputs.
|
|
4323
|
+
*/
|
|
4324
|
+
class MnCollectionPagination {
|
|
4325
|
+
/** Prefix for the page-size select's id, keeping it unique per host. */
|
|
4326
|
+
idPrefix = 'mn-collection';
|
|
4327
|
+
isPaginated = false;
|
|
4328
|
+
isServerPaginated = false;
|
|
4329
|
+
showLoadMore = false;
|
|
4330
|
+
loadingMoreRows = false;
|
|
4331
|
+
currentPage = 1;
|
|
4332
|
+
pageSize = 10;
|
|
4333
|
+
totalPages = 1;
|
|
4334
|
+
totalItemCount = 0;
|
|
4335
|
+
visiblePages = [];
|
|
4336
|
+
pageSizeSelectOptions = [];
|
|
4337
|
+
labels;
|
|
4338
|
+
loadMore = new EventEmitter();
|
|
4339
|
+
pageChange = new EventEmitter();
|
|
4340
|
+
pageSizeChange = new EventEmitter();
|
|
4341
|
+
get showPagination() {
|
|
4342
|
+
return this.isPaginated && (this.totalPages > 1 || this.isServerPaginated);
|
|
4343
|
+
}
|
|
4344
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4345
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnCollectionPagination, isStandalone: true, selector: "mn-collection-pagination", inputs: { idPrefix: "idPrefix", isPaginated: "isPaginated", isServerPaginated: "isServerPaginated", showLoadMore: "showLoadMore", loadingMoreRows: "loadingMoreRows", currentPage: "currentPage", pageSize: "pageSize", totalPages: "totalPages", totalItemCount: "totalItemCount", visiblePages: "visiblePages", pageSizeSelectOptions: "pageSizeSelectOptions", labels: "labels" }, outputs: { loadMore: "loadMore", pageChange: "pageChange", pageSizeChange: "pageSizeChange" }, ngImport: i0, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n mnButton\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"First page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Previous page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u2039\n </button>\n\n @for (page of visiblePages; track page) {\n <button\n (click)=\"pageChange.emit(page)\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"'Page ' + page\"\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n >{{ page }}\n </button>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Next page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Last page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00BB\n </button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4346
|
+
}
|
|
4347
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnCollectionPagination, decorators: [{
|
|
4348
|
+
type: Component,
|
|
4349
|
+
args: [{ selector: 'mn-collection-pagination', standalone: true, imports: [MnButton, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Load more button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n (click)=\"loadMore.emit()\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n [disabled]=\"loadingMoreRows\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n mnButton\n >\n @if (loadingMoreRows) {\n <span\n class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (showPagination) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"pageSizeChange.emit($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: idPrefix + '-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n (click)=\"pageChange.emit(1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"First page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00AB\n </button>\n\n <button\n (click)=\"pageChange.emit(currentPage - 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n [disabled]=\"currentPage === 1\"\n aria-label=\"Previous page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u2039\n </button>\n\n @for (page of visiblePages; track page) {\n <button\n (click)=\"pageChange.emit(page)\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n [attr.aria-label]=\"'Page ' + page\"\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n mnButton\n >{{ page }}\n </button>\n }\n\n <button\n (click)=\"pageChange.emit(currentPage + 1)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Next page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u203A\n </button>\n\n <button\n (click)=\"pageChange.emit(totalPages)\"\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n [disabled]=\"currentPage === totalPages\"\n aria-label=\"Last page\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n mnButton\n >\u00BB\n </button>\n </div>\n </div>\n}\n" }]
|
|
4350
|
+
}], propDecorators: { idPrefix: [{
|
|
4351
|
+
type: Input
|
|
4352
|
+
}], isPaginated: [{
|
|
4353
|
+
type: Input
|
|
4354
|
+
}], isServerPaginated: [{
|
|
4355
|
+
type: Input
|
|
4356
|
+
}], showLoadMore: [{
|
|
4357
|
+
type: Input
|
|
4358
|
+
}], loadingMoreRows: [{
|
|
4359
|
+
type: Input
|
|
4360
|
+
}], currentPage: [{
|
|
4361
|
+
type: Input
|
|
4362
|
+
}], pageSize: [{
|
|
4363
|
+
type: Input
|
|
4364
|
+
}], totalPages: [{
|
|
4365
|
+
type: Input
|
|
4366
|
+
}], totalItemCount: [{
|
|
4367
|
+
type: Input
|
|
4368
|
+
}], visiblePages: [{
|
|
4369
|
+
type: Input
|
|
4370
|
+
}], pageSizeSelectOptions: [{
|
|
4371
|
+
type: Input
|
|
4372
|
+
}], labels: [{
|
|
4373
|
+
type: Input
|
|
4374
|
+
}], loadMore: [{
|
|
4375
|
+
type: Output
|
|
4376
|
+
}], pageChange: [{
|
|
4377
|
+
type: Output
|
|
4378
|
+
}], pageSizeChange: [{
|
|
4379
|
+
type: Output
|
|
4380
|
+
}] } });
|
|
4381
|
+
|
|
4382
|
+
class MnTable extends MnSelectableCollectionBase {
|
|
4383
|
+
sortChange = new EventEmitter();
|
|
4384
|
+
rowClick = new EventEmitter();
|
|
4385
|
+
currentSort = null;
|
|
4386
|
+
/** Per-column filter values keyed by column key. */
|
|
4387
|
+
columnFilters = {};
|
|
4388
|
+
componentName = 'MnTable';
|
|
4389
|
+
get trackedToolbarTemplate() {
|
|
4390
|
+
return this.dataSource?.toolbarLeftTemplate;
|
|
4175
4391
|
}
|
|
4176
|
-
/**
|
|
4177
|
-
|
|
4178
|
-
|
|
4392
|
+
/** Updates a column filter value and re-applies filtering. */
|
|
4393
|
+
onColumnFilter(columnKey, value) {
|
|
4394
|
+
this.columnFilters[columnKey] = value;
|
|
4395
|
+
this.currentPage = 1;
|
|
4396
|
+
this.applyFilter(false);
|
|
4397
|
+
this.cdr.markForCheck();
|
|
4179
4398
|
}
|
|
4180
4399
|
/** Filter options formatted for mn-select for a given column. */
|
|
4181
4400
|
getFilterSelectOptions(column) {
|
|
@@ -4185,59 +4404,63 @@ class MnTable {
|
|
|
4185
4404
|
...(column.filterOptions ?? []).map(opt => ({ label: opt.label, value: String(opt.value) })),
|
|
4186
4405
|
];
|
|
4187
4406
|
}
|
|
4188
|
-
|
|
4189
|
-
|
|
4407
|
+
// ── Column Filters ──
|
|
4408
|
+
/** Whether any column has filtering enabled. */
|
|
4409
|
+
get hasColumnFilters() {
|
|
4410
|
+
return this.dataSource.columns.some(c => c.filterable);
|
|
4411
|
+
}
|
|
4412
|
+
sort(column) {
|
|
4413
|
+
if (!column.sortType || column.sortType === ColumnSortType.NONE)
|
|
4190
4414
|
return;
|
|
4191
|
-
this.
|
|
4192
|
-
|
|
4193
|
-
|
|
4415
|
+
if (this.currentSort?.columnKey === column.key) {
|
|
4416
|
+
this.currentSort = this.currentSort.direction === 'asc'
|
|
4417
|
+
? { columnKey: column.key, direction: 'desc' }
|
|
4418
|
+
: null;
|
|
4194
4419
|
}
|
|
4195
4420
|
else {
|
|
4196
|
-
this.
|
|
4421
|
+
this.currentSort = { columnKey: column.key, direction: 'asc' };
|
|
4197
4422
|
}
|
|
4198
|
-
this.
|
|
4423
|
+
this.sortChange.emit(this.currentSort);
|
|
4424
|
+
this.applyFilter(false);
|
|
4199
4425
|
}
|
|
4200
|
-
|
|
4201
|
-
this.
|
|
4202
|
-
|
|
4203
|
-
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
4204
|
-
this.applyPagination();
|
|
4205
|
-
}
|
|
4206
|
-
else {
|
|
4207
|
-
this.dataSource.onPageSizeChange?.(newSize);
|
|
4426
|
+
onRowClick(row) {
|
|
4427
|
+
if (this.hasSelection) {
|
|
4428
|
+
this.toggle(row);
|
|
4208
4429
|
}
|
|
4209
|
-
this.
|
|
4430
|
+
this.dataSource.onRowClick?.(row);
|
|
4431
|
+
this.rowClick.emit(row);
|
|
4210
4432
|
}
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4220
|
-
}
|
|
4221
|
-
const pages = [];
|
|
4222
|
-
for (let i = start; i <= end; i++) {
|
|
4223
|
-
pages.push(i);
|
|
4224
|
-
}
|
|
4225
|
-
return pages;
|
|
4433
|
+
// ── Sorting ──
|
|
4434
|
+
/**
|
|
4435
|
+
* Resolves the skeleton placeholder config for a column's cells.
|
|
4436
|
+
* Falls back to a text-shaped bar at 75% width (the previous default); any
|
|
4437
|
+
* fields the column provides override that default.
|
|
4438
|
+
*/
|
|
4439
|
+
getColumnSkeletonData(column) {
|
|
4440
|
+
const skeleton = column.skeleton;
|
|
4441
|
+
const overrides = skeleton && !this.isTemplateRef(skeleton) ? skeleton : {};
|
|
4442
|
+
return { shape: 'text', width: '75%', ...overrides };
|
|
4226
4443
|
}
|
|
4227
|
-
|
|
4228
|
-
if (this.
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4444
|
+
getSortIcon(column) {
|
|
4445
|
+
if (!this.currentSort || this.currentSort.columnKey !== column.key)
|
|
4446
|
+
return '';
|
|
4447
|
+
return this.currentSort.direction === 'asc' ? '▲' : '▼';
|
|
4448
|
+
}
|
|
4449
|
+
isSortable(column) {
|
|
4450
|
+
return !!column.sortType && column.sortType !== ColumnSortType.NONE;
|
|
4451
|
+
}
|
|
4452
|
+
// ── Row interaction ──
|
|
4453
|
+
/** Sets sort/filter state seeded from the data source before the first filter pass. */
|
|
4454
|
+
beforeInitialFilter() {
|
|
4455
|
+
super.beforeInitialFilter();
|
|
4456
|
+
this.currentSort = this.dataSource.defaultSort ?? null;
|
|
4457
|
+
for (const col of this.dataSource.columns) {
|
|
4458
|
+
if (col.filterable) {
|
|
4459
|
+
this.columnFilters[col.key] = '';
|
|
4460
|
+
}
|
|
4235
4461
|
}
|
|
4236
4462
|
}
|
|
4237
4463
|
// ── Template helpers ──
|
|
4238
|
-
isTemplateRef(value) {
|
|
4239
|
-
return value instanceof TemplateRef;
|
|
4240
|
-
}
|
|
4241
4464
|
getCellValue(column, row) {
|
|
4242
4465
|
if (typeof column.cell === 'function')
|
|
4243
4466
|
return column.cell(row);
|
|
@@ -4249,9 +4472,6 @@ class MnTable {
|
|
|
4249
4472
|
return column.cellSm.cell(row);
|
|
4250
4473
|
return '';
|
|
4251
4474
|
}
|
|
4252
|
-
trackByID = (_index, row) => {
|
|
4253
|
-
return this.dataSource.getID(row);
|
|
4254
|
-
};
|
|
4255
4475
|
trackByKey = (_index, column) => {
|
|
4256
4476
|
return column.key;
|
|
4257
4477
|
};
|
|
@@ -4263,18 +4483,25 @@ class MnTable {
|
|
|
4263
4483
|
count++;
|
|
4264
4484
|
return count;
|
|
4265
4485
|
}
|
|
4266
|
-
// ── Skeleton
|
|
4267
|
-
|
|
4268
|
-
|
|
4269
|
-
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4486
|
+
// ── Skeleton ──
|
|
4487
|
+
/**
|
|
4488
|
+
* Resolves table-specific translation keys (column headers/filters) plus the
|
|
4489
|
+
* shared keys handled by the base.
|
|
4490
|
+
*/
|
|
4491
|
+
resolveTranslationKeys() {
|
|
4492
|
+
super.resolveTranslationKeys();
|
|
4493
|
+
for (const col of this.dataSource.columns) {
|
|
4494
|
+
if (col.headerKey) {
|
|
4495
|
+
col.header = this.lang.t(col.headerKey);
|
|
4496
|
+
}
|
|
4497
|
+
if (col.filterPlaceholderKey) {
|
|
4498
|
+
col.filterPlaceholder = this.lang.t(col.filterPlaceholderKey);
|
|
4499
|
+
}
|
|
4277
4500
|
}
|
|
4501
|
+
}
|
|
4502
|
+
// ── Filtering & sorting ──
|
|
4503
|
+
applyFilter(searchForItems) {
|
|
4504
|
+
let items = this.applySearchFilter(this.dataSource.dataRows.value ?? []);
|
|
4278
4505
|
// Per-column filters
|
|
4279
4506
|
for (const col of this.dataSource.columns) {
|
|
4280
4507
|
const filterValue = this.columnFilters[col.key];
|
|
@@ -4331,52 +4558,13 @@ class MnTable {
|
|
|
4331
4558
|
}
|
|
4332
4559
|
});
|
|
4333
4560
|
}
|
|
4334
|
-
|
|
4335
|
-
const merged = [...new Map([...this.dataSource.dataRows.value, ...rows].map(item => [this.dataSource.getID(item), item])).values()];
|
|
4336
|
-
this.dataSource.dataRows.next(merged);
|
|
4337
|
-
this.loadingMoreRows = false;
|
|
4338
|
-
this.applyFilterAndSort(false);
|
|
4339
|
-
}
|
|
4340
|
-
validateDataSource() {
|
|
4341
|
-
const mode = this.dataSource.paginationMode;
|
|
4342
|
-
if (mode === 'paginated') {
|
|
4343
|
-
if (!this.dataSource.onPageChange) {
|
|
4344
|
-
throw new Error(`[MnTable] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
|
|
4345
|
-
}
|
|
4346
|
-
if (this.dataSource.totalItems == null) {
|
|
4347
|
-
throw new Error(`[MnTable] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
|
|
4348
|
-
}
|
|
4349
|
-
}
|
|
4350
|
-
if (mode === 'load-more' || mode === 'infinite-scroll') {
|
|
4351
|
-
if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
|
|
4352
|
-
throw new Error(`[MnTable] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
4353
|
-
}
|
|
4354
|
-
}
|
|
4355
|
-
// Validate pageSize is one of pageSizeOptions when pagination is active
|
|
4356
|
-
if (mode && mode !== 'none') {
|
|
4357
|
-
const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
4358
|
-
const size = this.dataSource.pageSize ?? 10;
|
|
4359
|
-
if (!options.includes(size)) {
|
|
4360
|
-
throw new Error(`[MnTable] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
|
|
4361
|
-
}
|
|
4362
|
-
}
|
|
4363
|
-
}
|
|
4364
|
-
emitSelection() {
|
|
4365
|
-
const rows = (this.dataSource.dataRows.value ?? []).filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
4366
|
-
this.dataSource.selectedRows?.next(rows);
|
|
4367
|
-
this.selectionChange.emit(rows);
|
|
4368
|
-
}
|
|
4369
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4370
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", inputs: { dataSource: "dataSource" }, outputs: { sortChange: "sortChange", selectionChange: "selectionChange", rowClick: "rowClick" }, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\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-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\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 -->\n @if (hasColumnFilters) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 <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)=\"toggleRow(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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\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]=\"column.width ?? null\"\n >\n @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 button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'outline', color: 'primary' }\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-table-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { 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: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4561
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
4562
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnTable, isStandalone: true, selector: "mn-table", outputs: { sortChange: "sortChange", rowClick: "rowClick" }, usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\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-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\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 -->\n @if (hasColumnFilters) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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]=\"column.width ?? null\"\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 {\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 <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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\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]=\"column.width ?? null\"\n >\n @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", 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: MnSkeleton, selector: "mn-skeleton", inputs: ["data"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
4371
4563
|
}
|
|
4372
4564
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
4373
4565
|
type: Component,
|
|
4374
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, FormsModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\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-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\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 -->\n @if (hasColumnFilters) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <tr class=\"animate-pulse\">\n @if (hasSelection) {\n <td class=\"px-2 py-3\"><div class=\"h-4 w-4 rounded bg-base-300\"></div></td>\n }\n @for (column of dataSource.columns; track column.key) {\n <td class=\"px-4 py-3\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n <div class=\"h-4 w-3/4 rounded bg-base-300\"></div>\n </td>\n }\n </tr>\n }\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 <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)=\"toggleRow(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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\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]=\"column.width ?? null\"\n >\n @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 button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'md', variant: 'outline', color: 'primary' }\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-center md:justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"hidden md:flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Rows:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-table-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2 hidden md:inline\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013 {{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: page === currentPage ? 'primary' : 'gray' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n" }]
|
|
4375
|
-
}], propDecorators: {
|
|
4376
|
-
type: Input
|
|
4377
|
-
}], sortChange: [{
|
|
4378
|
-
type: Output
|
|
4379
|
-
}], selectionChange: [{
|
|
4566
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnHiddenBelowDirective, MnShowAboveDirective, MnShowBelowDirective, MnInputField, MnSelect, MnSkeleton, FormsModule, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: custom toolbar template + search -->\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 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 (dataSource.canSearch) {\n <mn-lib-input-field\n class=\"w-full min-[375px]:flex-1 min-[480px]:max-w-64\"\n [props]=\"{\n id: 'mn-table-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\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-[375px]:w-auto min-[375px]:flex-row\">\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarRightTemplate\"></ng-container>\n </div>\n }\n </div>\n</div>\n\n<!-- Table wrapper with horizontal scroll -->\n<div class=\"overflow-x-auto\" role=\"region\" aria-label=\"Data table\">\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 bg-base-200 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 class=\"text-sm px-2 py-1 md:px-4 md:py-2 whitespace-nowrap\"\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]=\"column.width ?? null\"\n [attr.aria-sort]=\"currentSort?.columnKey === column.key ? (currentSort!.direction === 'asc' ? 'ascending' : 'descending') : null\"\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 -->\n @if (hasColumnFilters) {\n <tr class=\"bg-base-100 border-b border-base-300\">\n @if (hasSelection) {\n <th class=\"px-2 py-1 \"></th>\n }\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"px-4 py-2\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <mn-lib-input-field\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n type: 'text',\n label: '',\n placeholder: column.filterPlaceholder ?? '',\n autocomplete: column.filterAutocomplete ?? undefined,\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true,\n hover: true\n }\"\n [ngModel]=\"columnFilters[column.key]\"\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n ></mn-lib-input-field>\n } @else if (column.filterType === 'select') {\n <mn-lib-select\n (ngModelChange)=\"onColumnFilter(column.key, $event)\"\n [ngModel]=\"columnFilters[column.key] ?? ''\"\n [props]=\"{\n id: 'mn-table-filter-' + column.key,\n options: getFilterSelectOptions(column),\n size: 'sm',\n fullWidth: true\n }\"\n (click)=\"$event.stopPropagation()\"\n ></mn-lib-select>\n }\n }\n </th>\n }\n </tr>\n }\n </thead>\n\n <tbody>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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]=\"column.width ?? null\"\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 {\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 <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 class=\"text-xs px-2 py-1 md:px-4 md:py-2\"\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]=\"column.width ?? null\"\n >\n @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" }]
|
|
4567
|
+
}], propDecorators: { sortChange: [{
|
|
4380
4568
|
type: Output
|
|
4381
4569
|
}], rowClick: [{
|
|
4382
4570
|
type: Output
|
|
@@ -5129,7 +5317,7 @@ class MnFormBodyComponent {
|
|
|
5129
5317
|
}
|
|
5130
5318
|
}
|
|
5131
5319
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5132
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", inputs: ["dataSource"], outputs: ["sortChange", "selectionChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
5320
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnFormBodyComponent, isStandalone: true, selector: "mn-form-body", inputs: { config: "config", modalRef: "modalRef", hideFooter: "hideFooter", hideCustomBody: "hideCustomBody" }, outputs: { formStatusChange: "formStatusChange" }, viewQueries: [{ propertyName: "inputFields", predicate: MnInputField, descendants: true }, { propertyName: "textareas", predicate: MnTextarea, descendants: true }], ngImport: i0, template: "@if ((config.component || config.template) && !hideCustomBody) {\n <mn-custom-body-host\n [config]=\"asAny(config)\"\n [modalRef]=\"asAny(modalRef)\"\n class=\"mb-6 block\"\n ></mn-custom-body-host>\n}\n\n<form (ngSubmit)=\"submit()\" [formGroup]=\"form\" class=\"flex flex-col gap-6 h-full\">\n <!-- Shared field rendering template (must be inside form for formControlName) -->\n <ng-template #fieldTemplate let-rowField>\n @switch (rowField.field.kind) {\n @case (FieldKind.TEXT) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'text',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mask: asField(rowField.field).mask,\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.NUMBER) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'number',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.PASSWORD) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'password',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.SELECT) {\n <div class=\"flex flex-col\">\n <mn-lib-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"getSelectProps(rowField.field)\"\n ></mn-lib-select>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-1 pl-2 pt-1\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n </div>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CHECKBOX) {\n <div>\n <mn-lib-checkbox\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-checkbox>\n </div>\n }\n\n @case (FieldKind.DATE) {\n <div>\n <mn-lib-input-field\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n type: 'date',\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n startDate: asField(rowField.field).minDate,\n endDate: asField(rowField.field).maxDate,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-input-field>\n </div>\n }\n\n @case (FieldKind.DATETIME) {\n <div>\n <mn-lib-datetime\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n mode: asField(rowField.field).mode || 'datetime-local',\n min: asField(rowField.field).min,\n max: asField(rowField.field).max,\n step: asField(rowField.field).step,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-datetime>\n </div>\n }\n\n @case (FieldKind.TEXTAREA) {\n <div>\n <mn-lib-textarea\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n placeholder: asField(rowField.field).placeholder,\n rows: asField(rowField.field).rows || 4,\n resize: 'vertical',\n autocomplete: asField(rowField.field).autocomplete,\n readonly: isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-textarea>\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT) {\n <div>\n @if (isFieldLoading(asKey(rowField.field.key))) {\n <div class=\"flex items-center gap-2 py-2 text-sm text-base-content/50\">\n <div class=\"w-4 h-4 border-2 border-base-300 border-t-blue-500 rounded-full animate-spin\"></div>\n {{ labels.loadingOptions }}\n </div>\n }\n @if (!isFieldLoading(asKey(rowField.field.key))) {\n <mn-lib-multi-select\n [formControlName]=\"asKey(rowField.field.key)\"\n [props]=\"asAny({\n id: asKey(rowField.field.key),\n label: asField(rowField.field).label,\n options: getFieldOptions(rowField.field),\n searchable: asField(rowField.field).searchable,\n searchPlaceholder: asField(rowField.field).searchPlaceholder,\n maxSelections: asField(rowField.field).maxSelections,\n disabled: isFieldDisabled(rowField.field) || isFieldReadOnly(rowField.field)\n })\"\n ></mn-lib-multi-select>\n }\n @if (getFieldError(asKey(rowField.field.key))) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.MULTI_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.SINGLE_SELECT_TABLE) {\n <ng-container [ngTemplateOutlet]=\"selectTableTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n }\n\n @case (FieldKind.COLOR) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"color\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"w-10 h-10 rounded-lg border border-base-300 cursor-pointer p-0.5\"\n [value]=\"getColorValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onColorChange(rowField.field, $event)\"\n />\n <span class=\"text-sm text-base-content/60 font-mono\">{{ getColorValue(rowField.field) }}</span>\n </div>\n @if (asField(rowField.field).swatches) {\n <div class=\"flex gap-1.5 mt-1\">\n @for (swatch of asField(rowField.field).swatches; track swatch) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"w-6 h-6 rounded-md border border-base-300 cursor-pointer transition-transform hover:scale-110\"\n [style.background-color]=\"swatch\"\n [class.ring-2]=\"getColorValue(rowField.field) === swatch\"\n [class.ring-blue-500]=\"getColorValue(rowField.field) === swatch\"\n (click)=\"setColorFromSwatch(rowField.field, swatch)\"\n [attr.aria-label]=\"'Select color ' + swatch\"\n ></button>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.RATING) {\n <div class=\"flex flex-col gap-1\">\n <span [id]=\"'rating-label-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n <div [attr.aria-labelledby]=\"'rating-label-' + asKey(rowField.field.key)\" class=\"flex items-center gap-1\"\n role=\"group\">\n @for (star of getRatingRange(rowField.field); track star) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n type=\"button\"\n class=\"text-2xl cursor-pointer transition-colors focus:outline-none\"\n [class.text-yellow-400]=\"star <= getRatingValue(rowField.field)\"\n [class.text-base-300]=\"star > getRatingValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (click)=\"setRating(rowField.field, star)\"\n >\n ★\n </button>\n }\n <span class=\"text-sm text-base-content/50 ml-2\">{{ getRatingValue(rowField.field) }} / {{ asField(rowField.field).max || 5 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.SLIDER) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div class=\"flex items-center gap-3\">\n <input\n type=\"range\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"flex-1 h-2 bg-base-200 rounded-lg appearance-none cursor-pointer accent-blue-500\"\n [attr.min]=\"asField(rowField.field).min ?? 0\"\n [attr.max]=\"asField(rowField.field).max ?? 100\"\n [attr.step]=\"asField(rowField.field).step ?? 1\"\n [value]=\"getSliderValue(rowField.field)\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (input)=\"onSliderChange(rowField.field, $event)\"\n />\n @if (asField(rowField.field).showValue !== false) {\n <span class=\"text-sm text-base-content/60 min-w-[3rem] text-right\">\n {{ getSliderValue(rowField.field) }}{{ asField(rowField.field).unit || '' }}\n </span>\n }\n </div>\n <div class=\"flex justify-between text-xs text-base-content/40 px-1\">\n <span>{{ asField(rowField.field).min ?? 0 }}</span>\n <span>{{ asField(rowField.field).max ?? 100 }}</span>\n </div>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.FILE) {\n <div class=\"flex flex-col gap-1\">\n <label [for]=\"'field-' + asKey(rowField.field.key)\"\n class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </label>\n <div\n class=\"relative border-2 border-dashed border-base-300 rounded-xl p-6 text-center transition-colors hover:border-blue-400 hover:bg-blue-50/30\"\n [class.border-red-300]=\"form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched\"\n [class.opacity-50]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n >\n <input\n type=\"file\"\n [id]=\"'field-' + asKey(rowField.field.key)\"\n class=\"absolute inset-0 w-full h-full opacity-0 cursor-pointer\"\n [attr.accept]=\"asField(rowField.field).accept || null\"\n [attr.multiple]=\"asField(rowField.field).multiple || null\"\n [disabled]=\"isFieldReadOnly(rowField.field) || isFieldDisabled(rowField.field)\"\n (change)=\"onFileChange(rowField.field, $event)\"\n />\n <div class=\"flex flex-col items-center gap-2\">\n <svg class=\"w-8 h-8 text-base-content/40\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\" stroke-width=\"1.5\" stroke=\"currentColor\">\n <path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5\" />\n </svg>\n <p class=\"text-sm text-base-content/50\">{{ labels.fileUploadPrompt }}</p>\n @if (asField(rowField.field).accept) {\n <p class=\"text-xs text-base-content/40\">{{ labels.accepted }} {{ asField(rowField.field).accept }}</p>\n }\n @if (asField(rowField.field).maxSize) {\n <p class=\"text-xs text-base-content/40\">{{ labels.maxSize }} {{ formatFileSize(asField(rowField.field).maxSize) }}</p>\n }\n </div>\n </div>\n <!-- Selected files list -->\n @if (getSelectedFiles(asKey(rowField.field.key)).length > 0) {\n <div class=\"flex flex-col gap-1 mt-2\">\n @for (file of getSelectedFiles(asKey(rowField.field.key)); track file.name; let i = $index) {\n <div class=\"flex items-center justify-between px-3 py-1.5 bg-base-200 rounded-lg text-sm\">\n <span class=\"text-base-content truncate\">{{ file.name }} ({{ formatFileSize(file.size) }})</span>\n <button mnButton [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\" type=\"button\" class=\"text-base-content/40 hover:text-red-500 ml-2 cursor-pointer\" (click)=\"removeFile(asKey(rowField.field.key), i)\">×</button>\n </div>\n }\n </div>\n }\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFileError(rowField.field) }}\n </div>\n }\n </div>\n }\n\n @case (FieldKind.CUSTOM) {\n <div>\n <ng-container\n mnCustomFieldHost\n [component]=\"asField(rowField.field).component\"\n [inputs]=\"asField(rowField.field).inputs\"\n [formControlName]=\"asKey(rowField.field.key)\"\n ></ng-container>\n </div>\n }\n\n @default {\n <div></div>\n }\n }\n\n <!-- Show cross-field error below any field that has one -->\n @if (getFieldError(asKey(rowField.field.key)) && rowField.field.kind !== FieldKind.SELECT && rowField.field.kind !== FieldKind.MULTI_SELECT) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ getFieldError(asKey(rowField.field.key)) }}\n </div>\n }\n </ng-template>\n\n <!-- Shared template for MULTI_SELECT_TABLE and SINGLE_SELECT_TABLE -->\n <ng-template #selectTableTemplate let-rowField>\n <div class=\"flex flex-col gap-1\">\n @if (asField(rowField.field).label) {\n <span class=\"pl-2 pb-1 flex flex-row gap-0.5 text-base font-medium text-base-content\">\n {{ asField(rowField.field).label }}\n @if (hasRequiredValidator(rowField.field)) {\n <span class=\"text-red-500\">*</span>\n }\n </span>\n }\n <mn-table\n [dataSource]=\"tableDataSources[asKey(rowField.field.key)]\"\n [attr.aria-label]=\"asField(rowField.field).label || null\"\n (selectionChange)=\"onTableSelectionChange(rowField.field, $event)\"\n ></mn-table>\n @if (form.get(asKey(rowField.field.key))?.invalid && form.get(asKey(rowField.field.key))?.touched) {\n <div class=\"text-red-500 text-xs pl-2 pt-1\">\n {{ labels.fieldRequired }}\n </div>\n }\n </div>\n </ng-template>\n\n <!-- Field Groups (sections with headers) -->\n @if (fieldGroups.length > 0) {\n <div class=\"flex flex-col gap-6\">\n @for (group of fieldGroups; track group.title) {\n <div class=\"flex flex-col gap-4\" [style.display]=\"isGroupVisible(group) ? '' : 'none'\">\n <div class=\"border-b border-base-300 pb-2\">\n <h3 class=\"text-base font-semibold text-base-content\">{{ group.title }}</h3>\n @if (group.description) {\n <p class=\"text-sm text-base-content/50 mt-0.5\">{{ group.description }}</p>\n }\n </div>\n @for (row of group.rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Standard rows (no groups) -->\n @if (rows.length > 0) {\n <div class=\"flex flex-col gap-4\">\n @for (row of rows; track $index) {\n <div class=\"grid gap-4\" [style.grid-template-columns]=\"getGridColumns(row)\">\n @for (rowField of row.fields; track rowField.field.key) {\n <div class=\"flex flex-col gap-2\" [style.grid-column]=\"getGridSpan(rowField)\" [style.display]=\"isFieldVisible(rowField.field) ? '' : 'none'\">\n <ng-container [ngTemplateOutlet]=\"fieldTemplate\" [ngTemplateOutletContext]=\"{ $implicit: rowField }\"></ng-container>\n </div>\n }\n </div>\n }\n </div>\n }\n\n <!-- Form-level errors (not tied to a specific field) -->\n @if (formErrors['_form']) {\n <div class=\"text-red-500 text-sm px-2 py-1 bg-red-50 rounded-md\">\n {{ formErrors['_form'] }}\n </div>\n }\n\n @if (!hideFooter) {\n <div class=\"flex gap-3 pt-4 pb-6 border-t border-base-300 mt-auto sticky bottom-0 bg-base-100 z-10\">\n <button\n mnButton\n type=\"button\"\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (mousedown)=\"modalRef.dismiss(ModalCloseReason.CANCELLED)\"\n >\n {{ labels.cancel }}\n </button>\n\n <div class=\"flex-1\"></div>\n\n <button\n mnButton\n type=\"submit\"\n [data]=\"{ variant: 'fill', color: 'primary', disabled: form.invalid || isSubmitting }\"\n [disabled]=\"form.invalid || isSubmitting\"\n >\n {{ isSubmitting ? labels.submitting : labels.submit }}\n </button>\n </div>\n }\n</form>\n", styles: [".select-arrow{background-image:url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%236b7280' d='M6 8L1 3h10z'/%3E%3C/svg%3E\");background-repeat:no-repeat;background-position:right .75rem center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnDatetime, selector: "mn-lib-datetime", inputs: ["props"] }, { kind: "component", type: MnMultiSelect, selector: "mn-lib-multi-select", inputs: ["props"] }, { kind: "component", type: MnTextarea, selector: "mn-lib-textarea", inputs: ["props"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "directive", type: MnCustomFieldHostDirective, selector: "[mnCustomFieldHost]", inputs: ["component", "inputs"] }, { kind: "component", type: MnTable, selector: "mn-table", outputs: ["sortChange", "rowClick"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }] });
|
|
5133
5321
|
}
|
|
5134
5322
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnFormBodyComponent, decorators: [{
|
|
5135
5323
|
type: Component,
|
|
@@ -5619,11 +5807,11 @@ class MnWizardBodyComponent {
|
|
|
5619
5807
|
}
|
|
5620
5808
|
}
|
|
5621
5809
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5622
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col
|
|
5810
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnWizardBodyComponent, isStandalone: true, selector: "mn-wizard-body", inputs: { config: "config", modalRef: "modalRef" }, viewQueries: [{ propertyName: "formBodies", predicate: MnFormBodyComponent, descendants: true }, { propertyName: "stepWrappers", predicate: ["stepWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard -->\n <div class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions"], outputs: ["actionClick"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] });
|
|
5623
5811
|
}
|
|
5624
5812
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnWizardBodyComponent, decorators: [{
|
|
5625
5813
|
type: Component,
|
|
5626
|
-
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent], template: "<div class=\"flex flex-col
|
|
5814
|
+
args: [{ selector: 'mn-wizard-body', standalone: true, imports: [CommonModule, ReactiveFormsModule, MnButton, MnFormBodyComponent, MnCustomBodyHostComponent, MnFooterActionsComponent], template: "<div class=\"flex flex-col flex-auto min-h-0\">\n @if (config.component || config.template) {\n <mn-custom-body-host\n [config]=\"$any(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block flex-none px-6 pt-6\"\n ></mn-custom-body-host>\n }\n\n <!-- Steps header \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none px-6 pt-2 pb-4 border-b border-base-300 bg-base-100\">\n <div class=\"relative flex items-start\">\n <!-- Background line spanning from first circle center to last circle center -->\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.right]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-base-300 z-0\"\n ></div>\n\n <!-- Progress fill line -->\n @if (visibleSteps.length > 1) {\n <div\n [style.left]=\"'calc(' + 100 / (2 * visibleSteps.length) + '%)'\"\n [style.width]=\"'calc(' + (currentProgressIndex / (visibleSteps.length - 1)) * (100 - 100 / visibleSteps.length) + '%)'\"\n class=\"absolute top-3.75 h-1 bg-success z-1 transition-all duration-500\"\n ></div>\n }\n\n @for (step of visibleSteps; track step.id; let i = $index; ) {\n <div\n (keyup.enter)=\"isFreeFlow ? goToStep(step) : null\"\n (click)=\"isFreeFlow ? goToStep(step) : null\"\n [attr.tabindex]=\"isFreeFlow && canNavigateToStep(step) ? 0 : null\"\n [class.cursor-pointer]=\"isFreeFlow && canNavigateToStep(step)\"\n class=\"relative flex flex-col items-center gap-1 z-10 w-0 flex-1\"\n >\n <div\n [ngClass]=\"{\n 'bg-primary text-primary-content': step.id === currentStepId,\n 'bg-success text-success-content': visitedStepIds.includes(step.id) && step.id !== currentStepId,\n 'bg-base-200 text-base-content/50': !visitedStepIds.includes(step.id) && step.id !== currentStepId\n }\"\n class=\"w-8 h-8 rounded-full flex items-center justify-center font-semibold transition-all text-sm\"\n >{{ i + 1 }}\n </div>\n <div\n [ngClass]=\"{\n 'text-base-content font-semibold': step.id === currentStepId,\n 'text-base-content/50': step.id !== currentStepId\n }\"\n class=\"text-xs text-center whitespace-nowrap hidden sm:block\"\n >{{ step.title }}\n </div>\n\n </div>\n }\n </div>\n </div>\n\n <!-- Step body \u2014 the only scrolling region in the wizard -->\n <div class=\"flex-auto min-h-0 overflow-y-auto px-6 py-6\">\n <div [style.min-height]=\"measuredMinHeight ? measuredMinHeight + 'px' : null\" class=\"min-h-48\">\n @for (step of config.steps; track step.id) {\n <div #stepWrapper [style.display]=\"step.id === currentStepId ? 'block' : 'none'\">\n <h3 class=\"text-lg font-semibold text-base-content mb-4\">{{ step.title }}</h3>\n <div class=\"text-base-content/80\">\n <!-- Form step -->\n @if (stepFormConfigs[step.id]) {\n <mn-form-body\n [config]=\"stepFormConfigs[step.id]\"\n [modalRef]=\"$any(modalRef)\"\n [hideFooter]=\"true\"\n [hideCustomBody]=\"true\"\n ></mn-form-body>\n }\n\n <!-- Text body -->\n @if (!stepFormConfigs[step.id] && isTextBody(step)) {\n <div>\n {{ step.body }}\n </div>\n }\n\n <!-- Dynamic content container for component/template bodies -->\n <ng-container #dynamicContainer></ng-container>\n </div>\n </div>\n }\n </div>\n </div>\n\n <!-- Wizard-level errors (from onBeforeComplete) \u2014 fixed, above the footer -->\n @if (wizardErrors && (wizardErrors | keyvalue).length > 0) {\n <div class=\"flex flex-col gap-1 mx-6 mb-2 px-2 py-2 bg-red-50 rounded-md\">\n @for (err of wizardErrors | keyvalue; track err.key) {\n <div class=\"text-red-500 text-sm\">\n {{ err.value }}\n </div>\n }\n </div>\n }\n\n <!-- Footer \u2014 fixed (does not scroll with the step body) -->\n <div class=\"flex-none flex gap-3 px-6 pt-4 pb-6 border-t border-base-300 bg-base-100\">\n @if (!currentStep?.hideBack) {\n <button\n mnButton\n [data]=\"{ variant: 'outline', color: 'secondary' }\"\n (click)=\"back()\"\n >\n {{ currentStep?.backLabel || (canGoBack ? labels.back : labels.close) }}\n </button>\n }\n\n <!-- Custom footer actions -->\n @if (config.footerActions) {\n <mn-footer-actions\n [actions]=\"config.footerActions\"\n (actionClick)=\"handleFooterAction($any($event))\"\n ></mn-footer-actions>\n } @else {\n <div class=\"flex-1\"></div>\n }\n\n @if (!isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid }\"\n (click)=\"next()\"\n >\n {{ currentStep?.nextLabel || labels.next }}\n </button>\n }\n\n @if (isLastStep) {\n <button\n mnButton\n [data]=\"{ variant: 'fill', color: 'primary', disabled: !isCurrentStepValid || isCompleting }\"\n [disabled]=\"!isCurrentStepValid || isCompleting\"\n (click)=\"complete()\"\n >\n {{ currentStep?.nextLabel || (isCompleting ? labels.completing : labels.complete) }}\n </button>\n }\n </div>\n</div>\n" }]
|
|
5627
5815
|
}], propDecorators: { config: [{
|
|
5628
5816
|
type: Input
|
|
5629
5817
|
}], modalRef: [{
|
|
@@ -5815,17 +6003,7 @@ class MnModalShellComponent {
|
|
|
5815
6003
|
asCustom(config) {
|
|
5816
6004
|
return config;
|
|
5817
6005
|
}
|
|
5818
|
-
|
|
5819
|
-
const size = this.config.sizeWidth || ModalSize.MD;
|
|
5820
|
-
const closing = this.isClosing ? ' closing' : '';
|
|
5821
|
-
const animType = typeof this.config.animation === 'string'
|
|
5822
|
-
? this.config.animation
|
|
5823
|
-
: this.config.animation?.type || 'slide';
|
|
5824
|
-
const animation = ` anim-${animType}`;
|
|
5825
|
-
const stacked = this.isStacked ? ' is-stacked' : '';
|
|
5826
|
-
const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
|
|
5827
|
-
return `modal-shell modal-${size}${closing}${animation}${stacked}${mobileSheet}`;
|
|
5828
|
-
}
|
|
6006
|
+
static SWIPE_DISMISS_THRESHOLD = 150;
|
|
5829
6007
|
/** Whether this modal renders as a bottom sheet on small screens (default: true). */
|
|
5830
6008
|
get isMobileSheet() {
|
|
5831
6009
|
return this.config.mobileBottomSheet !== false;
|
|
@@ -5856,7 +6034,9 @@ class MnModalShellComponent {
|
|
|
5856
6034
|
onCloseButtonClick() {
|
|
5857
6035
|
this.handleClose(ModalCloseReason.DISMISSED);
|
|
5858
6036
|
}
|
|
5859
|
-
|
|
6037
|
+
/** True once a swipe has crossed the dismiss threshold — slides the sheet off-screen
|
|
6038
|
+
* via the transform transition instead of replaying the slide-up keyframe. */
|
|
6039
|
+
swipeDismissing = false;
|
|
5860
6040
|
// =========================
|
|
5861
6041
|
// Mobile bottom-sheet swipe-to-dismiss (via the grabber handle)
|
|
5862
6042
|
// =========================
|
|
@@ -5864,6 +6044,18 @@ class MnModalShellComponent {
|
|
|
5864
6044
|
sheetDragY = 0;
|
|
5865
6045
|
/** True while the user is actively dragging the grabber (disables snap transition). */
|
|
5866
6046
|
isDraggingSheet = false;
|
|
6047
|
+
get hostClasses() {
|
|
6048
|
+
const size = this.config.sizeWidth || ModalSize.MD;
|
|
6049
|
+
const closing = this.isClosing ? ' closing' : '';
|
|
6050
|
+
const animType = typeof this.config.animation === 'string'
|
|
6051
|
+
? this.config.animation
|
|
6052
|
+
: this.config.animation?.type || 'slide';
|
|
6053
|
+
const animation = ` anim-${animType}`;
|
|
6054
|
+
const stacked = this.isStacked ? ' is-stacked' : '';
|
|
6055
|
+
const mobileSheet = this.isMobileSheet ? ' mobile-sheet' : '';
|
|
6056
|
+
const swiping = this.swipeDismissing ? ' swipe-dismissing' : '';
|
|
6057
|
+
return `modal-shell modal-${size}${closing}${animation}${stacked}${mobileSheet}${swiping}`;
|
|
6058
|
+
}
|
|
5867
6059
|
dragStartY = 0;
|
|
5868
6060
|
/** Whether the sheet can be dismissed at all (drives whether the swipe is armed). */
|
|
5869
6061
|
get canClose() {
|
|
@@ -5896,10 +6088,16 @@ class MnModalShellComponent {
|
|
|
5896
6088
|
this.isDraggingSheet = false;
|
|
5897
6089
|
if (this.sheetDragY > MnModalShellComponent.SWIPE_DISMISS_THRESHOLD) {
|
|
5898
6090
|
const closed = await this.handleClose(ModalCloseReason.DISMISSED);
|
|
5899
|
-
if (
|
|
6091
|
+
if (closed) {
|
|
6092
|
+
// Continue the gesture: glide the sheet the rest of the way down rather than
|
|
6093
|
+
// snapping back to 0 and replaying the slide-up keyframe (which looked un-animated).
|
|
6094
|
+
this.swipeDismissing = true;
|
|
6095
|
+
this.sheetDragY = window.innerHeight;
|
|
6096
|
+
this.cdr.detectChanges();
|
|
6097
|
+
}
|
|
6098
|
+
else {
|
|
5900
6099
|
this.sheetDragY = 0; // guard rejected — spring back
|
|
5901
6100
|
}
|
|
5902
|
-
// closed === true: leave it; the closing animation takes over.
|
|
5903
6101
|
}
|
|
5904
6102
|
else {
|
|
5905
6103
|
this.sheetDragY = 0; // not far enough — spring back
|
|
@@ -6033,7 +6231,7 @@ class MnModalShellComponent {
|
|
|
6033
6231
|
}
|
|
6034
6232
|
}
|
|
6035
6233
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6036
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-
|
|
6234
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnModalShellComponent, isStandalone: true, selector: "mn-modal-shell", inputs: { config: "config", modalRef: "modalRef" }, host: { listeners: { "document:keydown.escape": "onEscapeKey($event)" }, properties: { "class": "this.hostClasses" } }, ngImport: i0, template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .2s ease}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .2s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;max-height:92vh;border-radius:1rem 1rem 0 0;animation:slideUpIn .25s ease-out}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .25s ease-out}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:slideUpOut .2s ease-in forwards}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "component", type: MnWizardBodyComponent, selector: "mn-wizard-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFormBodyComponent, selector: "mn-form-body", inputs: ["config", "modalRef", "hideFooter", "hideCustomBody"], outputs: ["formStatusChange"] }, { kind: "component", type: MnConfirmationBodyComponent, selector: "mn-confirmation-body", inputs: ["config", "modalRef"] }, { kind: "component", type: MnCustomBodyHostComponent, selector: "mn-custom-body-host", inputs: ["config", "modalRef"] }, { kind: "component", type: MnFooterActionsComponent, selector: "mn-footer-actions", inputs: ["actions"], outputs: ["actionClick"] }] });
|
|
6037
6235
|
}
|
|
6038
6236
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalShellComponent, decorators: [{
|
|
6039
6237
|
type: Component,
|
|
@@ -6044,17 +6242,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
6044
6242
|
MnConfirmationBodyComponent,
|
|
6045
6243
|
MnCustomBodyHostComponent,
|
|
6046
6244
|
MnFooterActionsComponent,
|
|
6047
|
-
], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-
|
|
6245
|
+
], template: "@if (showBackdrop) {\n <!-- The backdrop is a visual overlay \u2014 keyboard dismiss is handled at component level via Escape key -->\n <!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events,@angular-eslint/template/interactive-supports-focus -->\n <div class=\"modal-backdrop absolute inset-0 bg-black/50 animate-[fadeIn_0.2s_ease-in-out]\" (click)=\"onBackdropClick()\"></div>\n}\n\n<!-- eslint-disable-next-line @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"modal-container relative bg-base-100 rounded-lg shadow-xl max-h-[90vh] overflow-hidden flex flex-col\"\n [class.sheet-dragging]=\"isDraggingSheet\"\n [ngClass]=\"containerSizeClass\"\n [style.height]=\"containerHeightStyle\"\n [style.transform]=\"sheetDragY ? 'translateY(' + sheetDragY + 'px)' : null\"\n role=\"dialog\"\n aria-modal=\"true\"\n [attr.aria-labelledby]=\"config.title ? 'mn-modal-title' : null\"\n [attr.aria-describedby]=\"config.description ? 'mn-modal-description' : null\"\n tabindex=\"-1\"\n (click)=\"$event.stopPropagation()\"\n>\n @if (isMobileSheet) {\n <!-- Drag handle for swipe-to-dismiss, visible only on mobile bottom sheets -->\n <div\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n class=\"sm:hidden flex justify-center pt-2 pb-1 touch-none cursor-grab\"\n >\n <div class=\"h-1.5 w-10 rounded-full bg-base-300\"></div>\n </div>\n }\n\n <!-- On mobile the whole header doubles as a swipe-to-dismiss surface (drags that\n start on the close button are ignored). The grabber above is the visual cue. -->\n <div [class.border-b]=\"config.kind !== ModalKind.WIZARD\"\n [class.border-base-300]=\"config.kind !== ModalKind.WIZARD\"\n (pointercancel)=\"onSheetPointerUp()\"\n (pointerdown)=\"onSheetPointerDown($event)\"\n (pointermove)=\"onSheetPointerMove($event)\"\n (pointerup)=\"onSheetPointerUp()\"\n [class.cursor-grab]=\"isMobileSheet\"\n [class.sm:cursor-auto]=\"isMobileSheet\"\n [class.sm:touch-auto]=\"isMobileSheet\"\n [class.touch-none]=\"isMobileSheet\"\n class=\"flex items-center justify-between p-6\">\n <div class=\"flex flex-col gap-0.5\">\n @if (config.title) {\n <h2 class=\"m-0 text-xl font-semibold text-base-content\" id=\"mn-modal-title\">{{ config.title }}</h2>\n }\n @if (config.subtitle) {\n <p class=\"m-0 text-sm text-base-content/60 font-normal\">{{ config.subtitle }}</p>\n }\n </div>\n @if (showCloseButton) {\n <button\n class=\"bg-transparent border-none text-2xl cursor-pointer text-base-content/50 p-0 w-8 h-8 flex items-center justify-center rounded transition-colors hover:bg-base-200 hover:text-base-content\"\n (click)=\"onCloseButtonClick()\"\n aria-label=\"Close modal\"\n >\n \u00D7\n </button>\n }\n </div>\n @if (config.description) {\n <p class=\"m-0 px-6 text-sm text-base-content/60 leading-relaxed\" id=\"mn-modal-description\">{{ config.description }}</p>\n }\n\n @if (config.kind === ModalKind.WIZARD) {\n <!-- Wizard manages its own internal scrolling so the steps header and footer\n stay fixed while only the step body scrolls. No padding/scroll here. -->\n <div class=\"flex-auto min-h-0 overflow-hidden flex flex-col\">\n <mn-wizard-body\n [config]=\"asWizard(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"flex-auto min-h-0 flex flex-col\"\n ></mn-wizard-body>\n </div>\n } @else {\n <div class=\"flex-1 overflow-y-auto px-6 pt-6\">\n @if (config.kind === ModalKind.FORM) {\n <mn-form-body\n [config]=\"asForm(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block h-full\"\n ></mn-form-body>\n }\n\n @if (config.kind === ModalKind.CONFIRMATION) {\n <mn-confirmation-body\n [config]=\"asConfirmation(config)\"\n [modalRef]=\"$any(modalRef)\"\n ></mn-confirmation-body>\n }\n\n @if (config.kind === ModalKind.CUSTOM) {\n <mn-custom-body-host\n [config]=\"asCustom(config)\"\n [modalRef]=\"$any(modalRef)\"\n class=\"block pb-6\"\n ></mn-custom-body-host>\n }\n </div>\n }\n\n <!-- Custom Footer Actions (not for wizard modals, they render their own) -->\n @if (hasCustomFooterActions && config.kind !== ModalKind.WIZARD) {\n <div class=\"flex gap-3 p-6 border-t border-base-300\">\n <mn-footer-actions\n [actions]=\"config.footerActions || []\"\n (actionClick)=\"onFooterAction($event)\"\n ></mn-footer-actions>\n </div>\n }\n</div>\n", styles: [":host{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;transition:transform .3s ease-in-out,filter .3s ease-in-out,opacity .3s ease-in-out}:host(.is-stacked){transform:scale(.96) translateY(-1rem);filter:brightness(.9) blur(1px);pointer-events:none;opacity:.8}.modal-container{transition:transform .2s ease}.modal-container.sheet-dragging{transition:none}:host(.swipe-dismissing) .modal-container,:host(.swipe-dismissing).closing .modal-container{animation:none!important;transition:transform .2s ease-in}@keyframes fadeIn{0%{opacity:0}to{opacity:1}}@keyframes slideIn{0%{opacity:0;transform:translateY(-1rem)}to{opacity:1;transform:translateY(0)}}@keyframes zoomIn{0%{opacity:0;transform:scale(.95)}to{opacity:1;transform:scale(1)}}@keyframes fadeOut{0%{opacity:1}to{opacity:0}}@keyframes slideOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(1rem)}}@keyframes zoomOut{0%{opacity:1;transform:scale(1)}to{opacity:0;transform:scale(.95)}}@keyframes slideUpIn{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes slideUpOut{0%{opacity:1;transform:translateY(0)}to{opacity:0;transform:translateY(100%)}}:host(.anim-slide) .modal-container{animation:slideIn .2s ease-in-out}:host(.anim-fade) .modal-container{animation:fadeIn .2s ease-in-out}:host(.anim-zoom) .modal-container{animation:zoomIn .2s ease-in-out}:host(.closing) .modal-backdrop{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-slide .modal-container{animation:slideOut .15s ease-in-out forwards}:host(.closing).anim-fade .modal-container{animation:fadeOut .15s ease-in-out forwards}:host(.closing).anim-zoom .modal-container{animation:zoomOut .15s ease-in-out forwards}@media(max-width:639.98px){:host(.mobile-sheet){align-items:flex-end}:host(.mobile-sheet) .modal-container{width:100%;max-width:100%;max-height:92vh;border-radius:1rem 1rem 0 0;animation:slideUpIn .25s ease-out}:host(.mobile-sheet).anim-slide .modal-container,:host(.mobile-sheet).anim-fade .modal-container,:host(.mobile-sheet).anim-zoom .modal-container{animation:slideUpIn .25s ease-out}:host(.mobile-sheet).closing .modal-container,:host(.mobile-sheet).closing.anim-slide .modal-container,:host(.mobile-sheet).closing.anim-fade .modal-container,:host(.mobile-sheet).closing.anim-zoom .modal-container{animation:slideUpOut .2s ease-in forwards}}\n"] }]
|
|
6048
6246
|
}], propDecorators: { config: [{
|
|
6049
6247
|
type: Input
|
|
6050
6248
|
}], modalRef: [{
|
|
6051
6249
|
type: Input
|
|
6052
|
-
}], hostClasses: [{
|
|
6053
|
-
type: HostBinding,
|
|
6054
|
-
args: ['class']
|
|
6055
6250
|
}], onEscapeKey: [{
|
|
6056
6251
|
type: HostListener,
|
|
6057
6252
|
args: ['document:keydown.escape', ['$event']]
|
|
6253
|
+
}], hostClasses: [{
|
|
6254
|
+
type: HostBinding,
|
|
6255
|
+
args: ['class']
|
|
6058
6256
|
}] } });
|
|
6059
6257
|
|
|
6060
6258
|
class MnModalService {
|
|
@@ -6080,260 +6278,123 @@ class MnModalService {
|
|
|
6080
6278
|
// Attach to application
|
|
6081
6279
|
this.appRef.attachView(componentRef.hostView);
|
|
6082
6280
|
const domElem = componentRef.location.nativeElement;
|
|
6083
|
-
document.body.appendChild(domElem);
|
|
6084
|
-
// Clean up on close
|
|
6085
|
-
modalRef.afterClosed$.subscribe(() => {
|
|
6086
|
-
this.appRef.detachView(componentRef.hostView);
|
|
6087
|
-
domElem.remove();
|
|
6088
|
-
// Update stack
|
|
6089
|
-
const index = this.modalStack.indexOf(modalRef);
|
|
6090
|
-
if (index > -1) {
|
|
6091
|
-
this.modalStack.splice(index, 1);
|
|
6092
|
-
if (this.modalStack.length > 0) {
|
|
6093
|
-
const topModal = this.modalStack[this.modalStack.length - 1];
|
|
6094
|
-
topModal.component.isStacked = false;
|
|
6095
|
-
}
|
|
6096
|
-
}
|
|
6097
|
-
});
|
|
6098
|
-
return modalRef;
|
|
6099
|
-
}
|
|
6100
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6101
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, providedIn: 'root' });
|
|
6102
|
-
}
|
|
6103
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, decorators: [{
|
|
6104
|
-
type: Injectable,
|
|
6105
|
-
args: [{
|
|
6106
|
-
providedIn: 'root',
|
|
6107
|
-
}]
|
|
6108
|
-
}] });
|
|
6109
|
-
|
|
6110
|
-
// Types
|
|
6111
|
-
|
|
6112
|
-
class MnList {
|
|
6113
|
-
dataSource;
|
|
6114
|
-
selectionChange = new EventEmitter();
|
|
6115
|
-
itemClick = new EventEmitter();
|
|
6116
|
-
filteredItems = [];
|
|
6117
|
-
paginatedItems = [];
|
|
6118
|
-
searchValue = '';
|
|
6119
|
-
loadingMoreRows = false;
|
|
6120
|
-
selectedIds = new Set();
|
|
6121
|
-
currentPage = 1;
|
|
6122
|
-
pageSize = 10;
|
|
6123
|
-
cdr = inject(ChangeDetectorRef);
|
|
6124
|
-
dataSubscription;
|
|
6125
|
-
searchSubject = new Subject();
|
|
6126
|
-
searchSubscription;
|
|
6127
|
-
/** Tracks the previous toolbar template reference for change detection. */
|
|
6128
|
-
previousToolbarTemplate;
|
|
6129
|
-
ngDoCheck() {
|
|
6130
|
-
const currentTemplate = this.dataSource?.toolbarTemplate;
|
|
6131
|
-
if (currentTemplate !== this.previousToolbarTemplate) {
|
|
6132
|
-
this.previousToolbarTemplate = currentTemplate;
|
|
6133
|
-
this.cdr.markForCheck();
|
|
6134
|
-
}
|
|
6135
|
-
}
|
|
6136
|
-
get showLoadMore() {
|
|
6137
|
-
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
6138
|
-
// Server-side load-more: check if there are more items to load.
|
|
6139
|
-
if (this.dataSource.onLoadMore) {
|
|
6140
|
-
const totalItems = this.dataSource.totalItems ?? 0;
|
|
6141
|
-
return mode === 'load-more' && this.filteredItems.length < totalItems;
|
|
6142
|
-
}
|
|
6143
|
-
const strategy = this.dataSource.paginationStrategy;
|
|
6144
|
-
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
6145
|
-
return mode === 'load-more' && hasMore;
|
|
6146
|
-
}
|
|
6147
|
-
ngOnDestroy() {
|
|
6148
|
-
this.dataSubscription?.unsubscribe();
|
|
6149
|
-
this.searchSubscription?.unsubscribe();
|
|
6150
|
-
}
|
|
6151
|
-
// ── Search ──
|
|
6152
|
-
get isPaginated() {
|
|
6153
|
-
const mode = this.dataSource.paginationMode;
|
|
6154
|
-
return mode === 'paginated' || mode === 'client-side-pagination';
|
|
6155
|
-
}
|
|
6156
|
-
// ── Selection ──
|
|
6157
|
-
isSelected(item) {
|
|
6158
|
-
return this.selectedIds.has(this.dataSource.getID(item));
|
|
6159
|
-
}
|
|
6160
|
-
toggleItem(item) {
|
|
6161
|
-
const id = this.dataSource.getID(item);
|
|
6162
|
-
const mode = this.dataSource.selectionMode ?? 'none';
|
|
6163
|
-
if (mode === 'single') {
|
|
6164
|
-
this.selectedIds.clear();
|
|
6165
|
-
this.selectedIds.add(id);
|
|
6166
|
-
}
|
|
6167
|
-
else if (mode === 'multi') {
|
|
6168
|
-
if (this.selectedIds.has(id)) {
|
|
6169
|
-
this.selectedIds.delete(id);
|
|
6170
|
-
}
|
|
6171
|
-
else {
|
|
6172
|
-
this.selectedIds.add(id);
|
|
6281
|
+
document.body.appendChild(domElem);
|
|
6282
|
+
// Clean up on close
|
|
6283
|
+
modalRef.afterClosed$.subscribe(() => {
|
|
6284
|
+
this.appRef.detachView(componentRef.hostView);
|
|
6285
|
+
domElem.remove();
|
|
6286
|
+
// Update stack
|
|
6287
|
+
const index = this.modalStack.indexOf(modalRef);
|
|
6288
|
+
if (index > -1) {
|
|
6289
|
+
this.modalStack.splice(index, 1);
|
|
6290
|
+
if (this.modalStack.length > 0) {
|
|
6291
|
+
const topModal = this.modalStack[this.modalStack.length - 1];
|
|
6292
|
+
topModal.component.isStacked = false;
|
|
6293
|
+
}
|
|
6173
6294
|
}
|
|
6174
|
-
}
|
|
6175
|
-
|
|
6295
|
+
});
|
|
6296
|
+
return modalRef;
|
|
6176
6297
|
}
|
|
6177
|
-
|
|
6178
|
-
|
|
6179
|
-
|
|
6180
|
-
|
|
6181
|
-
|
|
6182
|
-
|
|
6298
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
6299
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, providedIn: 'root' });
|
|
6300
|
+
}
|
|
6301
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnModalService, decorators: [{
|
|
6302
|
+
type: Injectable,
|
|
6303
|
+
args: [{
|
|
6304
|
+
providedIn: 'root',
|
|
6305
|
+
}]
|
|
6306
|
+
}] });
|
|
6307
|
+
|
|
6308
|
+
// Types
|
|
6309
|
+
|
|
6310
|
+
/** Default skeleton lines reproducing the previous two-bar placeholder. */
|
|
6311
|
+
const DEFAULT_LIST_SKELETON_LINES = [
|
|
6312
|
+
{ shape: 'text', width: '75%' },
|
|
6313
|
+
{ shape: 'text', width: '50%', height: '0.75rem' },
|
|
6314
|
+
];
|
|
6315
|
+
class MnList extends MnSelectableCollectionBase {
|
|
6316
|
+
itemClick = new EventEmitter();
|
|
6317
|
+
componentName = 'MnList';
|
|
6318
|
+
/** Skeleton lines rendered for each placeholder item, falling back to the default two-bar layout. */
|
|
6319
|
+
get skeletonLines() {
|
|
6320
|
+
const skeleton = this.dataSource.skeleton;
|
|
6321
|
+
if (skeleton && !this.isTemplateRef(skeleton)) {
|
|
6322
|
+
return skeleton.lines;
|
|
6183
6323
|
}
|
|
6184
|
-
|
|
6185
|
-
}
|
|
6186
|
-
get allSelected() {
|
|
6187
|
-
return this.filteredItems.length > 0 && this.selectedIds.size === this.filteredItems.length;
|
|
6188
|
-
}
|
|
6189
|
-
get hasSelection() {
|
|
6190
|
-
return (this.dataSource.selectionMode ?? 'none') !== 'none';
|
|
6191
|
-
}
|
|
6192
|
-
get isMultiSelect() {
|
|
6193
|
-
return this.dataSource.selectionMode === 'multi';
|
|
6324
|
+
return DEFAULT_LIST_SKELETON_LINES;
|
|
6194
6325
|
}
|
|
6195
6326
|
// ── Item interaction ──
|
|
6196
6327
|
onItemClick(item) {
|
|
6197
6328
|
this.dataSource.onItemClick?.(item);
|
|
6198
6329
|
this.itemClick.emit(item);
|
|
6199
6330
|
}
|
|
6200
|
-
// ──
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
6204
|
-
return mode === 'paginated' || mode === 'load-more';
|
|
6205
|
-
}
|
|
6206
|
-
/** Total number of items, accounting for server-side pagination. */
|
|
6207
|
-
get totalItemCount() {
|
|
6208
|
-
if (this.isServerPaginated && this.dataSource.totalItems != null) {
|
|
6209
|
-
return this.dataSource.totalItems;
|
|
6210
|
-
}
|
|
6211
|
-
return this.filteredItems.length;
|
|
6212
|
-
}
|
|
6213
|
-
// ── Paginated Mode ──
|
|
6214
|
-
get totalPages() {
|
|
6215
|
-
return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
|
|
6216
|
-
}
|
|
6217
|
-
ngOnInit() {
|
|
6218
|
-
this.validateDataSource();
|
|
6219
|
-
this.pageSize = this.dataSource.pageSize ?? 10;
|
|
6220
|
-
// Pre-select items from initialSelectedIds if provided
|
|
6221
|
-
if (this.dataSource.initialSelectedIds?.length) {
|
|
6222
|
-
for (const id of this.dataSource.initialSelectedIds) {
|
|
6223
|
-
this.selectedIds.add(id);
|
|
6224
|
-
}
|
|
6225
|
-
this.emitSelection();
|
|
6226
|
-
}
|
|
6227
|
-
this.applyFilter(false);
|
|
6228
|
-
this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
|
|
6229
|
-
this.applyFilter(false);
|
|
6230
|
-
this.cdr.markForCheck();
|
|
6231
|
-
});
|
|
6232
|
-
this.searchSubscription = this.searchSubject
|
|
6233
|
-
.pipe(debounceTime(300))
|
|
6234
|
-
.subscribe(value => {
|
|
6235
|
-
this.searchValue = value;
|
|
6236
|
-
this.applyFilter(true);
|
|
6237
|
-
this.cdr.markForCheck();
|
|
6238
|
-
});
|
|
6239
|
-
}
|
|
6240
|
-
onSearch(searchString) {
|
|
6241
|
-
this.currentPage = 1;
|
|
6242
|
-
if (this.isServerPaginated) {
|
|
6243
|
-
this.searchValue = searchString;
|
|
6244
|
-
this.dataSource.onServerSearch?.(searchString);
|
|
6245
|
-
this.cdr.markForCheck();
|
|
6246
|
-
}
|
|
6247
|
-
else {
|
|
6248
|
-
this.searchSubject.next(searchString);
|
|
6249
|
-
}
|
|
6250
|
-
}
|
|
6251
|
-
loadMoreRows() {
|
|
6252
|
-
// Server-side infinite scroll: delegate to consumer callback.
|
|
6253
|
-
if (this.dataSource.onLoadMore) {
|
|
6254
|
-
this.dataSource.onLoadMore();
|
|
6255
|
-
return;
|
|
6256
|
-
}
|
|
6257
|
-
if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
|
|
6258
|
-
return;
|
|
6259
|
-
this.loadingMoreRows = true;
|
|
6260
|
-
const promise = (this.searchValue.length > 0 && this.dataSource.searchForAdditionalItems)
|
|
6261
|
-
? this.dataSource.searchForAdditionalItems(this.searchValue)
|
|
6262
|
-
: this.dataSource.loadAdditionalRows();
|
|
6263
|
-
promise
|
|
6264
|
-
.then(rows => this.processLoadedRows(rows))
|
|
6265
|
-
.catch(() => this.loadingMoreRows = false);
|
|
6266
|
-
}
|
|
6267
|
-
get resolvedPageSizeOptions() {
|
|
6268
|
-
return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
6269
|
-
}
|
|
6270
|
-
/** Page-size options formatted for mn-select. */
|
|
6271
|
-
get pageSizeSelectOptions() {
|
|
6272
|
-
return this.resolvedPageSizeOptions.map(opt => ({ label: String(opt), value: opt }));
|
|
6331
|
+
// ── Skeleton ──
|
|
6332
|
+
get trackedToolbarTemplate() {
|
|
6333
|
+
return this.dataSource?.toolbarTemplate;
|
|
6273
6334
|
}
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
this.
|
|
6278
|
-
if (
|
|
6279
|
-
this.
|
|
6280
|
-
}
|
|
6281
|
-
else {
|
|
6282
|
-
this.dataSource.onPageChange?.(page);
|
|
6335
|
+
// ── Filtering ──
|
|
6336
|
+
applyFilter(searchForItems) {
|
|
6337
|
+
this.filteredItems = this.applySearchFilter(this.dataSource.dataRows.value);
|
|
6338
|
+
this.applyPagination();
|
|
6339
|
+
if (searchForItems) {
|
|
6340
|
+
this.loadMoreRows();
|
|
6283
6341
|
}
|
|
6284
|
-
this.cdr.markForCheck();
|
|
6285
6342
|
}
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
}
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6343
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
6344
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", outputs: { itemClick: "itemClick" }, usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\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 aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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 {\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 <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: 'Select all', 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: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6345
|
+
}
|
|
6346
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
|
|
6347
|
+
type: Component,
|
|
6348
|
+
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnCheckbox, MnSkeleton, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\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 aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\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 {\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 <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: 'Select all', 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" }]
|
|
6349
|
+
}], propDecorators: { itemClick: [{
|
|
6350
|
+
type: Output
|
|
6351
|
+
}] } });
|
|
6352
|
+
|
|
6353
|
+
/** Default card skeleton: an image block plus two text bars. */
|
|
6354
|
+
const DEFAULT_GRID_SKELETON_LINES = [
|
|
6355
|
+
{ shape: 'rectangle', width: '100%', height: '8rem' },
|
|
6356
|
+
{ shape: 'text', width: '75%' },
|
|
6357
|
+
{ shape: 'text', width: '50%', height: '0.75rem' },
|
|
6358
|
+
];
|
|
6359
|
+
/**
|
|
6360
|
+
* Responsive card-grid component. Shares the collection chrome (search, every
|
|
6361
|
+
* pagination mode, loading skeleton, empty state, toolbar, i18n) with
|
|
6362
|
+
* {@link import('../mn-list').MnList} and {@link import('../mn-table').MnTable}
|
|
6363
|
+
* via {@link MnCollectionBase}, and lays items out as cards instead of rows.
|
|
6364
|
+
* Selection is intentionally not supported.
|
|
6365
|
+
*/
|
|
6366
|
+
class MnGrid extends MnCollectionBase {
|
|
6367
|
+
itemClick = new EventEmitter();
|
|
6368
|
+
componentName = 'MnGrid';
|
|
6369
|
+
/** Whether the grid uses auto-fit (minCardWidth) instead of explicit columns. */
|
|
6370
|
+
get isAutoLayout() {
|
|
6371
|
+
return !!this.dataSource.layout?.minCardWidth;
|
|
6296
6372
|
}
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
const
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
if (end > total) {
|
|
6304
|
-
end = total;
|
|
6305
|
-
start = Math.max(1, end - maxVisible + 1);
|
|
6306
|
-
}
|
|
6307
|
-
const pages = [];
|
|
6308
|
-
for (let i = start; i <= end; i++) {
|
|
6309
|
-
pages.push(i);
|
|
6373
|
+
// ── Layout ──
|
|
6374
|
+
/** Skeleton lines for the default/lines placeholder; null when a custom template is used. */
|
|
6375
|
+
get skeletonLines() {
|
|
6376
|
+
const skeleton = this.dataSource.skeleton;
|
|
6377
|
+
if (skeleton && !this.isTemplateRef(skeleton)) {
|
|
6378
|
+
return skeleton.lines;
|
|
6310
6379
|
}
|
|
6311
|
-
return
|
|
6380
|
+
return DEFAULT_GRID_SKELETON_LINES;
|
|
6312
6381
|
}
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
// ── Skeleton rows for loading ──
|
|
6317
|
-
get skeletonRows() {
|
|
6318
|
-
return Array.from({ length: 5 });
|
|
6382
|
+
// ── Item interaction ──
|
|
6383
|
+
get trackedToolbarTemplate() {
|
|
6384
|
+
return this.dataSource?.toolbarTemplate;
|
|
6319
6385
|
}
|
|
6320
|
-
// ──
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
|
|
6325
|
-
}
|
|
6326
|
-
else {
|
|
6327
|
-
// Server always provides the correct page/slice — no client-side slicing.
|
|
6328
|
-
this.paginatedItems = this.filteredItems;
|
|
6329
|
-
}
|
|
6386
|
+
// ── Skeleton ──
|
|
6387
|
+
onItemClick(item) {
|
|
6388
|
+
this.dataSource.onItemClick?.(item);
|
|
6389
|
+
this.itemClick.emit(item);
|
|
6330
6390
|
}
|
|
6391
|
+
// ── Filtering ──
|
|
6331
6392
|
applyFilter(searchForItems) {
|
|
6332
|
-
let items = this.dataSource.dataRows.value;
|
|
6333
|
-
//
|
|
6334
|
-
|
|
6335
|
-
|
|
6336
|
-
items = items.
|
|
6393
|
+
let items = this.applySearchFilter(this.dataSource.dataRows.value ?? []);
|
|
6394
|
+
// Preview cap: show only the first `maxItems` cards (pager stays hidden).
|
|
6395
|
+
const maxItems = this.dataSource.layout?.maxItems;
|
|
6396
|
+
if (maxItems != null) {
|
|
6397
|
+
items = items.slice(0, maxItems);
|
|
6337
6398
|
}
|
|
6338
6399
|
this.filteredItems = items;
|
|
6339
6400
|
this.applyPagination();
|
|
@@ -6341,52 +6402,13 @@ class MnList {
|
|
|
6341
6402
|
this.loadMoreRows();
|
|
6342
6403
|
}
|
|
6343
6404
|
}
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
this.dataSource.dataRows.next(merged);
|
|
6347
|
-
this.loadingMoreRows = false;
|
|
6348
|
-
this.applyFilter(false);
|
|
6349
|
-
}
|
|
6350
|
-
validateDataSource() {
|
|
6351
|
-
const mode = this.dataSource.paginationMode;
|
|
6352
|
-
if (mode === 'paginated') {
|
|
6353
|
-
if (!this.dataSource.onPageChange) {
|
|
6354
|
-
throw new Error(`[MnList] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
|
|
6355
|
-
}
|
|
6356
|
-
if (this.dataSource.totalItems == null) {
|
|
6357
|
-
throw new Error(`[MnList] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
|
|
6358
|
-
}
|
|
6359
|
-
}
|
|
6360
|
-
if (mode === 'load-more' || mode === 'infinite-scroll') {
|
|
6361
|
-
if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
|
|
6362
|
-
throw new Error(`[MnList] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
6363
|
-
}
|
|
6364
|
-
}
|
|
6365
|
-
// Validate pageSize is one of pageSizeOptions when pagination is active
|
|
6366
|
-
if (mode && mode !== 'none') {
|
|
6367
|
-
const options = this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
6368
|
-
const size = this.dataSource.pageSize ?? 10;
|
|
6369
|
-
if (!options.includes(size)) {
|
|
6370
|
-
throw new Error(`[MnList] pageSize '${size}' is not one of the allowed pageSizeOptions [${options.join(', ')}]. pageSize must be one of pageSizeOptions.`);
|
|
6371
|
-
}
|
|
6372
|
-
}
|
|
6373
|
-
}
|
|
6374
|
-
emitSelection() {
|
|
6375
|
-
const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
6376
|
-
this.dataSource.selectedRows?.next(rows);
|
|
6377
|
-
this.selectionChange.emit(rows);
|
|
6378
|
-
}
|
|
6379
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
6380
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnList, isStandalone: true, selector: "mn-list", inputs: { dataSource: "dataSource" }, outputs: { selectionChange: "selectionChange", itemClick: "itemClick" }, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <div>\n <input\n type=\"text\"\n class=\"input input-sm rounded border border-base-300 bg-base-200 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-brand-500 w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search list\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n</div>\n\n<!-- List wrapper -->\n<div\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 aria-label=\"Data list\"\n>\n <!-- Loading state -->\n @if (dataSource.isDataLoading) {\n @for (_ of skeletonRows; track $index) {\n <div class=\"animate-pulse px-4 py-3\" [class.py-2]=\"dataSource.appearance?.compact\" role=\"listitem\">\n <div class=\"h-4 w-3/4 rounded bg-base-300 mb-1\"></div>\n <div class=\"h-3 w-1/2 rounded bg-base-300\"></div>\n </div>\n @if (!$last && (dataSource.appearance?.dividers !== false)) {\n <div class=\"border-b border-base-300\"></div>\n }\n }\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 <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: 'Select all', 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)=\"toggleItem(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 button -->\n@if (showLoadMore) {\n <div class=\"flex justify-center py-4\">\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'primary' }\"\n class=\"px-4 py-1.5 text-sm rounded border border-brand-500 text-brand-500 hover:bg-brand-100 transition-colors disabled:opacity-50\"\n (click)=\"loadMoreRows()\"\n [disabled]=\"loadingMoreRows\"\n >\n @if (loadingMoreRows) {\n <span class=\"inline-block w-3 h-3 border-2 border-brand-500 border-t-transparent rounded-full animate-spin mr-2\"></span>\n }\n {{ dataSource.labels?.loadMore || 'Load more' }}\n </button>\n </div>\n}\n\n<!-- Pagination controls -->\n@if (isPaginated && (totalPages > 1 || isServerPaginated)) {\n <div class=\"flex items-center justify-between px-2 py-3 text-sm text-base-content\">\n <div class=\"flex items-center gap-2\">\n <span>{{ dataSource.labels?.rowsPerPage || 'Items per page:' }}</span>\n <mn-lib-select\n (ngModelChange)=\"onPageSizeChange($event)\"\n [ngModel]=\"pageSize\"\n [props]=\"{\n id: 'mn-list-page-size',\n options: pageSizeSelectOptions,\n size: 'sm'\n }\"\n ></mn-lib-select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\n \u2013{{ currentPage * pageSize > totalItemCount ? totalItemCount : currentPage * pageSize }}\n of {{ totalItemCount }}</span>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(1)\"\n aria-label=\"First page\"\n >\u00AB</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === 1 }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === 1\"\n (click)=\"goToPage(currentPage - 1)\"\n aria-label=\"Previous page\"\n >\u2039</button>\n\n @for (page of visiblePages; track page) {\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'text', color: 'secondary' }\"\n class=\"px-2.5 py-1 rounded underline underline-offset-2 transition-colors text-xs\"\n [class.border-secondary]=\"page === currentPage\"\n [class.text-accent]=\"page === currentPage\"\n [class.text-white]=\"page !== currentPage\"\n [class.border-base-300]=\"page !== currentPage\"\n [class.hover:bg-base-200]=\"page !== currentPage\"\n (click)=\"goToPage(page)\"\n [attr.aria-label]=\"'Page ' + page\"\n [attr.aria-current]=\"page === currentPage ? 'page' : null\"\n >{{ page }}</button>\n }\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(currentPage + 1)\"\n aria-label=\"Next page\"\n >\u203A</button>\n\n <button\n mnButton\n [data]=\"{ size: 'sm', variant: 'outline', color: 'secondary', disabled: currentPage === totalPages }\"\n class=\"px-2 py-1 rounded border border-base-300 hover:bg-base-200 transition-colors disabled:opacity-40 disabled:cursor-not-allowed aspect-square leading-none\"\n [disabled]=\"currentPage === totalPages\"\n (click)=\"goToPage(totalPages)\"\n aria-label=\"Last page\"\n >\u00BB</button>\n </div>\n </div>\n}\n", styles: [""], dependencies: [{ kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "component", type: MnButton, selector: "button[mnButton], a[mnButton]", inputs: ["data"] }, { kind: "component", type: MnCheckbox, selector: "mn-lib-checkbox", inputs: ["props", "checked"], outputs: ["checkedChange"] }, { kind: "component", type: MnSelect, selector: "mn-lib-select", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6405
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, deps: null, target: i0.ɵɵFactoryTarget.Component });
|
|
6406
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnGrid, isStandalone: true, selector: "mn-grid", outputs: { itemClick: "itemClick" }, usesInheritance: true, ngImport: i0, template: "<!-- Toolbar: search + custom toolbar template -->\n@if (dataSource.canSearch || dataSource.toolbarTemplate) {\n <div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full max-w-xs\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n </div>\n}\n\n<!-- Loading state -->\n@if (dataSource.isDataLoading) {\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-busy=\"true\"\n aria-label=\"Loading\"\n class=\"mn-grid\"\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 {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] rounded-xl border border-dashed border-base-content/15 bg-base-100\">\n <p class=\"text-sm text-base-content/40\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Card grid -->\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-label=\"Card grid\"\n class=\"mn-grid\"\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\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", styles: [":host{display:block}.mn-grid{display:grid;gap:var(--mn-grid-gap, 1rem);grid-template-columns:repeat(var(--mn-grid-cols-base, 1),minmax(0,1fr))}@media(min-width:640px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)),minmax(0,1fr))}}@media(min-width:768px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))),minmax(0,1fr))}}@media(min-width:1024px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)))),minmax(0,1fr))}}@media(min-width:1280px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-xl, var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))))),minmax(0,1fr))}}.mn-grid.mn-grid--auto{grid-template-columns:repeat(auto-fit,minmax(var(--mn-grid-min, 18rem),1fr))}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
6381
6407
|
}
|
|
6382
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type:
|
|
6408
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnGrid, decorators: [{
|
|
6383
6409
|
type: Component,
|
|
6384
|
-
args: [{ selector: 'mn-
|
|
6385
|
-
}], propDecorators: {
|
|
6386
|
-
type: Input
|
|
6387
|
-
}], selectionChange: [{
|
|
6388
|
-
type: Output
|
|
6389
|
-
}], itemClick: [{
|
|
6410
|
+
args: [{ selector: 'mn-grid', standalone: true, imports: [NgTemplateOutlet, FormsModule, MnSkeleton, MnInputField, MnCollectionPagination], changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- Toolbar: search + custom toolbar template -->\n@if (dataSource.canSearch || dataSource.toolbarTemplate) {\n <div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\n (ngModelChange)=\"onSearch($event)\"\n [ngModel]=\"searchValue\"\n [props]=\"{\n id: 'mn-grid-search',\n type: 'search',\n label: '',\n placeholder: dataSource.searchPlaceholder ?? 'Search...',\n size: 'sm',\n borderRadius: 'md',\n fullWidth: true\n }\"\n class=\"w-full max-w-xs\"\n ></mn-lib-input-field>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\n </div>\n}\n\n<!-- Loading state -->\n@if (dataSource.isDataLoading) {\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-busy=\"true\"\n aria-label=\"Loading\"\n class=\"mn-grid\"\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 {\n <!-- Empty state -->\n @if (filteredItems.length === 0) {\n <div class=\"text-center text-xs py-8\">\n @if (dataSource.emptyTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.emptyTemplate\"></ng-container>\n } @else {\n <div\n class=\"flex items-center justify-center min-h-[6rem] rounded-xl border border-dashed border-base-content/15 bg-base-100\">\n <p class=\"text-sm text-base-content/40\">{{ dataSource.emptyMessage }}</p>\n </div>\n }\n </div>\n } @else {\n <!-- Card grid -->\n <div\n [class.mn-grid--auto]=\"isAutoLayout\"\n [style.--mn-grid-cols-base]=\"dataSource.layout?.cols?.base\"\n [style.--mn-grid-cols-lg]=\"dataSource.layout?.cols?.lg\"\n [style.--mn-grid-cols-md]=\"dataSource.layout?.cols?.md\"\n [style.--mn-grid-cols-sm]=\"dataSource.layout?.cols?.sm\"\n [style.--mn-grid-cols-xl]=\"dataSource.layout?.cols?.xl\"\n [style.--mn-grid-gap]=\"dataSource.layout?.gap\"\n [style.--mn-grid-min]=\"dataSource.layout?.minCardWidth\"\n aria-label=\"Card grid\"\n class=\"mn-grid\"\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\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", styles: [":host{display:block}.mn-grid{display:grid;gap:var(--mn-grid-gap, 1rem);grid-template-columns:repeat(var(--mn-grid-cols-base, 1),minmax(0,1fr))}@media(min-width:640px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)),minmax(0,1fr))}}@media(min-width:768px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))),minmax(0,1fr))}}@media(min-width:1024px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1)))),minmax(0,1fr))}}@media(min-width:1280px){.mn-grid{grid-template-columns:repeat(var(--mn-grid-cols-xl, var(--mn-grid-cols-lg, var(--mn-grid-cols-md, var(--mn-grid-cols-sm, var(--mn-grid-cols-base, 1))))),minmax(0,1fr))}}.mn-grid.mn-grid--auto{grid-template-columns:repeat(auto-fit,minmax(var(--mn-grid-min, 18rem),1fr))}\n"] }]
|
|
6411
|
+
}], propDecorators: { itemClick: [{
|
|
6390
6412
|
type: Output
|
|
6391
6413
|
}] } });
|
|
6392
6414
|
|
|
@@ -7865,53 +7887,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
7865
7887
|
}]
|
|
7866
7888
|
}] });
|
|
7867
7889
|
|
|
7868
|
-
const mnSkeletonVariants = tv({
|
|
7869
|
-
base: 'bg-base-300 relative overflow-hidden',
|
|
7870
|
-
variants: {
|
|
7871
|
-
shape: {
|
|
7872
|
-
rectangle: 'rounded-md',
|
|
7873
|
-
circle: 'rounded-full',
|
|
7874
|
-
text: 'rounded h-4',
|
|
7875
|
-
},
|
|
7876
|
-
},
|
|
7877
|
-
defaultVariants: {
|
|
7878
|
-
shape: 'rectangle',
|
|
7879
|
-
},
|
|
7880
|
-
});
|
|
7881
|
-
|
|
7882
|
-
class MnSkeleton {
|
|
7883
|
-
data = {};
|
|
7884
|
-
get hostClasses() {
|
|
7885
|
-
return mnSkeletonVariants({ shape: this.data.shape });
|
|
7886
|
-
}
|
|
7887
|
-
get width() {
|
|
7888
|
-
return this.data.width ?? null;
|
|
7889
|
-
}
|
|
7890
|
-
get height() {
|
|
7891
|
-
return this.data.height ?? null;
|
|
7892
|
-
}
|
|
7893
|
-
get isAnimated() {
|
|
7894
|
-
return this.data.animated !== false;
|
|
7895
|
-
}
|
|
7896
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
7897
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: MnSkeleton, isStandalone: true, selector: "mn-skeleton", inputs: { data: "data" }, host: { properties: { "class": "this.hostClasses", "style.width": "this.width", "style.height": "this.height" } }, ngImport: i0, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.15) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}\n"] });
|
|
7898
|
-
}
|
|
7899
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSkeleton, decorators: [{
|
|
7900
|
-
type: Component,
|
|
7901
|
-
args: [{ selector: 'mn-skeleton', standalone: true, template: "@if (isAnimated) {\n <span class=\"mn-skeleton-shimmer-bar\"></span>\n}\n", styles: [":host{display:block}@keyframes mn-skeleton-shimmer{0%{transform:translate(-150%)}to{transform:translate(150%)}}.mn-skeleton-shimmer-bar{position:absolute;inset:0;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.15) 50%,transparent 100%);animation:mn-skeleton-shimmer 1.5s ease-in-out infinite}\n"] }]
|
|
7902
|
-
}], propDecorators: { data: [{
|
|
7903
|
-
type: Input
|
|
7904
|
-
}], hostClasses: [{
|
|
7905
|
-
type: HostBinding,
|
|
7906
|
-
args: ['class']
|
|
7907
|
-
}], width: [{
|
|
7908
|
-
type: HostBinding,
|
|
7909
|
-
args: ['style.width']
|
|
7910
|
-
}], height: [{
|
|
7911
|
-
type: HostBinding,
|
|
7912
|
-
args: ['style.height']
|
|
7913
|
-
}] } });
|
|
7914
|
-
|
|
7915
7890
|
/**
|
|
7916
7891
|
* Injection token for the base URL used by all CRUD service requests.
|
|
7917
7892
|
*
|
|
@@ -8413,5 +8388,5 @@ function enableMnPreviewMode(configService, langService, allowedOrigins) {
|
|
|
8413
8388
|
* Generated bundle index. Do not edit.
|
|
8414
8389
|
*/
|
|
8415
8390
|
|
|
8416
|
-
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
8391
|
+
export { API_BASE_URL, ActionStyle, BackdropMode, BaseModalBuilder, CALENDAR_CONFIG, CALENDAR_DATE_FORMATTER, CalendarDayComponent, CalendarEventComponent, CalendarEventDefaultComponent, CalendarEventLayoutService, CalendarMonthComponent, CalendarUtility, CalendarView, CalendarViewComponent, CalendarWeekComponent, CloseMode, ColumnSortType, ConfirmationModalBuilder, ConfirmationTone, CrudService, CustomModalBuilder, DEFAULT_CALENDAR_CONFIG, DEFAULT_MN_ALERT_CONFIG, DefaultCalendarDateFormatter, FieldAppearance, FieldKind, FormLayoutMode, FormModalBuilder, KeyboardMode, MN_ALERT_CONFIG, MN_CALENDAR_COMPONENT_NAME, MN_CALENDAR_CONFIG, MN_CHECKBOX_CONFIG, MN_DATETIME_CONFIG, MN_ICON_MAP, MN_INPUT_FIELD_CONFIG, MN_INSTANCE_ID, MN_LIB_DUAL_HORIZONTAL_IMAGE, MN_MULTI_SELECT_CONFIG, MN_SECTION_PATH, MN_SELECT_CONFIG, MN_TEXTAREA_CONFIG, MnAlertOutletComponent, MnAlertService, MnAlertStore, MnBadge, MnButton, MnCheckbox, MnCollectionBase, MnCollectionPagination, MnConfigService, MnConfirmationBodyComponent, MnCustomBodyHostComponent, MnDatetime, MnDualHorizontalImage, MnFormBodyComponent, MnGrid, MnHiddenBelowDirective, MnHttpService, MnIcon, MnIconAttributes, MnInformationCard, MnInputField, MnInstanceDirective, MnLanguageService, MnList, MnModalRef, MnModalService, MnModalShellComponent, MnMultiSelect, MnSectionDirective, MnSelect, MnSelectableCollectionBase, MnShowAboveDirective, MnShowBelowDirective, MnSkeleton, MnTabComponent, MnTable, MnTextarea, MnTranslatePipe, MnWizardBodyComponent, ModalBuilder, ModalCloseReason, ModalIntent, ModalKind, ModalSize, NavigationDirection, OptionState, SelectionMode, StepBuilder, StepState, SubmitMode, UpcomingEventRowComponent, UpcomingEventsComponent, ValidationCode, ValidationStatus, WizardFlowMode, WizardModalBuilder, dateTimeAdapter, defaultTextAdapter, enableMnPreviewMode, isTranslatable, mnAlertVariants, mnBadgeVariants, mnButtonVariants, mnCheckboxVariants, mnCheckboxWrapperVariants, mnDatetimeVariants, mnIconVariants, mnInformationCardVariants, mnInputFieldVariants, mnMultiSelectVariants, mnSelectVariants, mnSkeletonVariants, mnTextareaVariants, numberAdapter, pickAdapter, provideMnAlerts, provideMnCalendarConfig, provideMnComponentConfig, provideMnConfig, provideMnLanguage, resolveCalendarConfig };
|
|
8417
8392
|
//# sourceMappingURL=mn-angular-lib.mjs.map
|