mn-angular-lib 1.0.2 → 1.0.5
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 +460 -341
- package/fesm2022/mn-angular-lib.mjs.map +1 -1
- package/package.json +1 -1
- package/types/mn-angular-lib.d.ts +75 -16
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Injectable, Optional, Inject, HostBinding, Input, Component, inject, ChangeDetectionStrategy, signal,
|
|
2
|
+
import { InjectionToken, Injectable, Optional, Inject, HostBinding, Input, Component, inject, ChangeDetectionStrategy, signal, APP_INITIALIZER, DestroyRef, SkipSelf, Attribute, Directive, Pipe, ElementRef, Self, HostListener, ViewChild, forwardRef, Renderer2, EventEmitter, ChangeDetectorRef, TemplateRef, Output, ViewContainerRef, ViewChildren, ApplicationRef, EnvironmentInjector, createComponent } from '@angular/core';
|
|
3
3
|
export { TemplateRef, Type } from '@angular/core';
|
|
4
4
|
import { BehaviorSubject, firstValueFrom, skip, Subject, debounceTime, of, takeUntil, map, catchError } from 'rxjs';
|
|
5
5
|
import * as i1 from '@angular/common';
|
|
@@ -220,7 +220,7 @@ const mnAlertVariants = tv({
|
|
|
220
220
|
});
|
|
221
221
|
|
|
222
222
|
const mnButtonVariants = tv({
|
|
223
|
-
base: 'hover:cursor-pointer transition-
|
|
223
|
+
base: 'hover:cursor-pointer transition-all duration-300 ease-in-out',
|
|
224
224
|
variants: {
|
|
225
225
|
size: {
|
|
226
226
|
sm: 'px-2 py-1 text-sm',
|
|
@@ -231,6 +231,7 @@ const mnButtonVariants = tv({
|
|
|
231
231
|
fill: '',
|
|
232
232
|
outline: 'bg-transparent border',
|
|
233
233
|
text: 'bg-transparent',
|
|
234
|
+
textUnderline: 'bg-transparent underline underline-offset-2',
|
|
234
235
|
},
|
|
235
236
|
// Intentionally empty; resolved via compoundVariants
|
|
236
237
|
color: {
|
|
@@ -258,19 +259,19 @@ const mnButtonVariants = tv({
|
|
|
258
259
|
},
|
|
259
260
|
compoundVariants: [
|
|
260
261
|
// Fill
|
|
261
|
-
{ variant: 'fill', color: 'primary', class: 'bg-primary text-primary-content hover:
|
|
262
|
-
{ variant: 'fill', color: 'secondary', class: 'bg-neutral text-neutral-content hover:
|
|
263
|
-
{ variant: 'fill', color: 'danger', class: 'bg-error text-error-content hover:
|
|
264
|
-
{ variant: 'fill', color: 'warning', class: 'bg-warning text-warning-content hover:
|
|
265
|
-
{ variant: 'fill', color: 'success', class: 'bg-success text-success-content hover:
|
|
266
|
-
{ variant: 'fill', color: 'accent', class: 'bg-accent text-accent-content hover:
|
|
262
|
+
{ variant: 'fill', color: 'primary', class: 'bg-primary text-primary-content hover:brightness-60' },
|
|
263
|
+
{ variant: 'fill', color: 'secondary', class: 'bg-neutral text-neutral-content hover:brightness-60' },
|
|
264
|
+
{ variant: 'fill', color: 'danger', class: 'bg-error text-error-content hover:brightness-60' },
|
|
265
|
+
{ variant: 'fill', color: 'warning', class: 'bg-warning text-warning-content hover:brightness-60' },
|
|
266
|
+
{ variant: 'fill', color: 'success', class: 'bg-success text-success-content hover:brightness-60' },
|
|
267
|
+
{ variant: 'fill', color: 'accent', class: 'bg-accent text-accent-content hover:brightness-60' },
|
|
267
268
|
// Outline
|
|
268
|
-
{ variant: 'outline', color: 'primary', class: 'border-primary text-primary hover:bg-primary/
|
|
269
|
-
{ variant: 'outline', color: 'secondary', class: 'border-neutral text-neutral hover:bg-neutral/
|
|
270
|
-
{ variant: 'outline', color: 'danger', class: 'border-error text-error hover:bg-error/
|
|
271
|
-
{ variant: 'outline', color: 'warning', class: 'border-warning text-warning hover:bg-warning/
|
|
272
|
-
{ variant: 'outline', color: 'success', class: 'border-success text-success hover:bg-success/
|
|
273
|
-
{ variant: 'outline', color: 'accent', class: 'border-accent text-accent hover:bg-accent/
|
|
269
|
+
{ variant: 'outline', color: 'primary', class: 'border-primary text-primary hover:bg-primary/40' },
|
|
270
|
+
{ variant: 'outline', color: 'secondary', class: 'border-neutral text-neutral hover:bg-neutral/40' },
|
|
271
|
+
{ variant: 'outline', color: 'danger', class: 'border-error text-error hover:bg-error/40' },
|
|
272
|
+
{ variant: 'outline', color: 'warning', class: 'border-warning text-warning hover:bg-warning/40' },
|
|
273
|
+
{ variant: 'outline', color: 'success', class: 'border-success text-success hover:bg-success/40' },
|
|
274
|
+
{ variant: 'outline', color: 'accent', class: 'border-accent text-accent hover:bg-accent/40' },
|
|
274
275
|
// Text
|
|
275
276
|
{ variant: 'text', color: 'primary', class: 'text-primary hover:bg-primary/10' },
|
|
276
277
|
{ variant: 'text', color: 'secondary', class: 'text-neutral hover:bg-neutral/10' },
|
|
@@ -278,6 +279,13 @@ const mnButtonVariants = tv({
|
|
|
278
279
|
{ variant: 'text', color: 'warning', class: 'text-warning hover:bg-warning/10' },
|
|
279
280
|
{ variant: 'text', color: 'success', class: 'text-success hover:bg-success/10' },
|
|
280
281
|
{ variant: 'text', color: 'accent', class: 'text-accent hover:bg-accent/10' },
|
|
282
|
+
// Text Underline
|
|
283
|
+
{ variant: 'textUnderline', color: 'primary', class: 'text-primary underline underline-offset-2 hover:bg-primary/10' },
|
|
284
|
+
{ variant: 'textUnderline', color: 'secondary', class: 'text-neutral underline underline-offset-2 hover:bg-neutral/10' },
|
|
285
|
+
{ variant: 'textUnderline', color: 'danger', class: 'text-error underline underline-offset-2 hover:bg-error/10' },
|
|
286
|
+
{ variant: 'textUnderline', color: 'warning', class: 'text-warning underline underline-offset-2 hover:bg-warning/10' },
|
|
287
|
+
{ variant: 'textUnderline', color: 'success', class: 'text-success underline underline-offset-2 hover:bg-success/10' },
|
|
288
|
+
{ variant: 'textUnderline', color: 'accent', class: 'text-accent underline underline-offset-2 hover:bg-accent/10' },
|
|
281
289
|
],
|
|
282
290
|
defaultVariants: {
|
|
283
291
|
size: 'md',
|
|
@@ -554,7 +562,7 @@ function pickAdapter(type) {
|
|
|
554
562
|
}
|
|
555
563
|
|
|
556
564
|
const mnInputFieldVariants = tv({
|
|
557
|
-
base: 'bg-base-100 border-1 border-base-300 placeholder-base-content/50 text-base-content text-sm',
|
|
565
|
+
base: 'bg-base-100 border-1 border-base-300 placeholder-base-content/50 text-base-content text-sm outline-none focus:ring-1 focus:ring-primary',
|
|
558
566
|
variants: {
|
|
559
567
|
shadow: {
|
|
560
568
|
true: 'shadow-lg',
|
|
@@ -606,6 +614,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
606
614
|
args: [{ required: true }]
|
|
607
615
|
}] } });
|
|
608
616
|
|
|
617
|
+
/**
|
|
618
|
+
* Types for mn-lib configuration.
|
|
619
|
+
*/
|
|
620
|
+
|
|
609
621
|
class MnLanguageService {
|
|
610
622
|
http;
|
|
611
623
|
_translations = {};
|
|
@@ -954,6 +966,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
954
966
|
args: [{ providedIn: 'root' }]
|
|
955
967
|
}], ctorParameters: () => [{ type: i1$1.HttpClient }] });
|
|
956
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Provides an APP_INITIALIZER that loads the mn-lib configuration from the given URL
|
|
971
|
+
* during application bootstrap. The consuming application is responsible for providing
|
|
972
|
+
* HttpClient (e.g., via HttpClientModule or provideHttpClient()).
|
|
973
|
+
*/
|
|
974
|
+
function provideMnConfig(url, debugMode = false) {
|
|
975
|
+
return [
|
|
976
|
+
{
|
|
977
|
+
provide: APP_INITIALIZER,
|
|
978
|
+
multi: true,
|
|
979
|
+
useFactory: (svc) => () => svc.load(url, debugMode),
|
|
980
|
+
deps: [MnConfigService],
|
|
981
|
+
},
|
|
982
|
+
];
|
|
983
|
+
}
|
|
984
|
+
|
|
957
985
|
/**
|
|
958
986
|
* Represents the current section path based on nested mn-section directives.
|
|
959
987
|
*/
|
|
@@ -969,6 +997,185 @@ const MN_INSTANCE_ID = new InjectionToken('MN_INSTANCE_ID', {
|
|
|
969
997
|
factory: () => null,
|
|
970
998
|
});
|
|
971
999
|
|
|
1000
|
+
/**
|
|
1001
|
+
* Helper to provide a resolved, typed component config via DI.
|
|
1002
|
+
*
|
|
1003
|
+
* Usage in a component/module providers:
|
|
1004
|
+
* const MY_CFG = new InjectionToken<MyCfg>('MY_CFG');
|
|
1005
|
+
* providers: [ provideMnComponentConfig(MY_CFG, 'my-component') ]
|
|
1006
|
+
* Then in the component:
|
|
1007
|
+
* readonly cfg = inject(MY_CFG)
|
|
1008
|
+
*
|
|
1009
|
+
* The returned config object is **reactive**: when the active locale changes,
|
|
1010
|
+
* all translatable values are re-resolved in place so that templates using
|
|
1011
|
+
* `cfg.someLabel` automatically reflect the new language on the next change-detection cycle.
|
|
1012
|
+
*/
|
|
1013
|
+
function provideMnComponentConfig(token, componentName, initial) {
|
|
1014
|
+
return {
|
|
1015
|
+
provide: token,
|
|
1016
|
+
deps: [
|
|
1017
|
+
MnConfigService,
|
|
1018
|
+
MnLanguageService,
|
|
1019
|
+
DestroyRef,
|
|
1020
|
+
[new Optional(), MN_SECTION_PATH],
|
|
1021
|
+
[new Optional(), MN_INSTANCE_ID],
|
|
1022
|
+
],
|
|
1023
|
+
useFactory: (svc, lang, destroyRef, sectionPath, instanceId) => {
|
|
1024
|
+
const resolveConfig = () => {
|
|
1025
|
+
const resolved = svc.resolve(componentName, sectionPath ?? [], instanceId ?? undefined);
|
|
1026
|
+
return Object.assign({}, initial ?? {}, resolved);
|
|
1027
|
+
};
|
|
1028
|
+
// Create the initial config object that will be shared by reference.
|
|
1029
|
+
const cfg = resolveConfig();
|
|
1030
|
+
// Re-resolve translatable values whenever the locale changes.
|
|
1031
|
+
// skip(1) because the current locale was already used for the initial resolve.
|
|
1032
|
+
const sub = lang.locale$.subscribe(() => {
|
|
1033
|
+
const updated = resolveConfig();
|
|
1034
|
+
// Mutate the existing object in place so all template bindings pick up the new values.
|
|
1035
|
+
for (const key of Object.keys(updated)) {
|
|
1036
|
+
cfg[key] = updated[key];
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1040
|
+
return cfg;
|
|
1041
|
+
},
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
class MnSectionDirective {
|
|
1046
|
+
/** Section name contributed by this DOM node to the section path */
|
|
1047
|
+
mnSection;
|
|
1048
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1049
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSectionDirective, isStandalone: true, selector: "[mn-section]", inputs: { mnSection: ["mn-section", "mnSection"] }, providers: [
|
|
1050
|
+
{
|
|
1051
|
+
provide: MN_SECTION_PATH,
|
|
1052
|
+
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
1053
|
+
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
1054
|
+
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
1055
|
+
useFactory: (parentPath, attr) => {
|
|
1056
|
+
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
1057
|
+
const name = (attr ?? '').trim();
|
|
1058
|
+
return name ? [...parent, name] : [...parent];
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
], ngImport: i0 });
|
|
1062
|
+
}
|
|
1063
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
|
|
1064
|
+
type: Directive,
|
|
1065
|
+
args: [{
|
|
1066
|
+
selector: '[mn-section]',
|
|
1067
|
+
standalone: true,
|
|
1068
|
+
providers: [
|
|
1069
|
+
{
|
|
1070
|
+
provide: MN_SECTION_PATH,
|
|
1071
|
+
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
1072
|
+
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
1073
|
+
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
1074
|
+
useFactory: (parentPath, attr) => {
|
|
1075
|
+
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
1076
|
+
const name = (attr ?? '').trim();
|
|
1077
|
+
return name ? [...parent, name] : [...parent];
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
}]
|
|
1082
|
+
}], propDecorators: { mnSection: [{
|
|
1083
|
+
type: Input,
|
|
1084
|
+
args: ['mn-section']
|
|
1085
|
+
}] } });
|
|
1086
|
+
|
|
1087
|
+
class MnInstanceDirective {
|
|
1088
|
+
/** Instance id for targeting per-component instance overrides */
|
|
1089
|
+
mnInstance;
|
|
1090
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1091
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnInstanceDirective, isStandalone: true, selector: "[mn-instance]", inputs: { mnInstance: ["mn-instance", "mnInstance"] }, providers: [
|
|
1092
|
+
{
|
|
1093
|
+
provide: MN_INSTANCE_ID,
|
|
1094
|
+
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
1095
|
+
deps: [new Attribute('mn-instance')],
|
|
1096
|
+
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
1097
|
+
},
|
|
1098
|
+
], ngImport: i0 });
|
|
1099
|
+
}
|
|
1100
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
|
|
1101
|
+
type: Directive,
|
|
1102
|
+
args: [{
|
|
1103
|
+
selector: '[mn-instance]',
|
|
1104
|
+
standalone: true,
|
|
1105
|
+
providers: [
|
|
1106
|
+
{
|
|
1107
|
+
provide: MN_INSTANCE_ID,
|
|
1108
|
+
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
1109
|
+
deps: [new Attribute('mn-instance')],
|
|
1110
|
+
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
1111
|
+
},
|
|
1112
|
+
],
|
|
1113
|
+
}]
|
|
1114
|
+
}], propDecorators: { mnInstance: [{
|
|
1115
|
+
type: Input,
|
|
1116
|
+
args: ['mn-instance']
|
|
1117
|
+
}] } });
|
|
1118
|
+
|
|
1119
|
+
/**
|
|
1120
|
+
* Provides an APP_INITIALIZER that configures the MnLanguageService and
|
|
1121
|
+
* preloads the requested locales during application bootstrap.
|
|
1122
|
+
*
|
|
1123
|
+
* Usage in app.config.ts:
|
|
1124
|
+
* ...provideMnLanguage({
|
|
1125
|
+
* urlPattern: 'assets/i18n/{locale}.json',
|
|
1126
|
+
* defaultLocale: 'en',
|
|
1127
|
+
* preload: ['en', 'nl'],
|
|
1128
|
+
* })
|
|
1129
|
+
*/
|
|
1130
|
+
function provideMnLanguage(config) {
|
|
1131
|
+
return [
|
|
1132
|
+
{
|
|
1133
|
+
provide: APP_INITIALIZER,
|
|
1134
|
+
multi: true,
|
|
1135
|
+
useFactory: (svc) => async () => {
|
|
1136
|
+
if (config.debug) {
|
|
1137
|
+
svc.setDebug(true);
|
|
1138
|
+
}
|
|
1139
|
+
svc.configure(config.urlPattern);
|
|
1140
|
+
const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
|
|
1141
|
+
const localesToLoad = config.preload ?? [effectiveLocale];
|
|
1142
|
+
await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
|
|
1143
|
+
await svc.setLocale(effectiveLocale);
|
|
1144
|
+
},
|
|
1145
|
+
deps: [MnLanguageService],
|
|
1146
|
+
},
|
|
1147
|
+
];
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
/**
|
|
1151
|
+
* Pipe that translates a key via MnLanguageService.
|
|
1152
|
+
*
|
|
1153
|
+
* Usage in templates:
|
|
1154
|
+
* {{ 'form.email.label' | mnTranslate }}
|
|
1155
|
+
* {{ 'greeting' | mnTranslate:{ name: 'World' } }}
|
|
1156
|
+
*
|
|
1157
|
+
* Note: This pipe is impure so it re-evaluates when the locale changes.
|
|
1158
|
+
*/
|
|
1159
|
+
class MnTranslatePipe {
|
|
1160
|
+
lang;
|
|
1161
|
+
constructor(lang) {
|
|
1162
|
+
this.lang = lang;
|
|
1163
|
+
}
|
|
1164
|
+
transform(key, params) {
|
|
1165
|
+
return this.lang.translate(key, params);
|
|
1166
|
+
}
|
|
1167
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [{ token: MnLanguageService }], target: i0.ɵɵFactoryTarget.Pipe });
|
|
1168
|
+
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
|
|
1169
|
+
}
|
|
1170
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
|
|
1171
|
+
type: Pipe,
|
|
1172
|
+
args: [{
|
|
1173
|
+
name: 'mnTranslate',
|
|
1174
|
+
standalone: true,
|
|
1175
|
+
pure: false,
|
|
1176
|
+
}]
|
|
1177
|
+
}], ctorParameters: () => [{ type: MnLanguageService }] });
|
|
1178
|
+
|
|
972
1179
|
const MN_INPUT_FIELD_CONFIG = new InjectionToken('MN_INPUT_FIELD_CONFIG');
|
|
973
1180
|
/**
|
|
974
1181
|
* MnInputField Component
|
|
@@ -1067,10 +1274,8 @@ class MnInputField {
|
|
|
1067
1274
|
const instanceId = this.explicitInstanceId || `mn-input-${this.props.id}`;
|
|
1068
1275
|
this.uiConfig = this.configService.resolve('mn-input-field', this.sectionPath, instanceId);
|
|
1069
1276
|
// Allow props to override uiConfig for label and placeholder
|
|
1070
|
-
if (this.props
|
|
1277
|
+
if (this.props) {
|
|
1071
1278
|
this.uiConfig = { ...this.uiConfig, label: this.props.label };
|
|
1072
|
-
}
|
|
1073
|
-
if (this.props.placeholder) {
|
|
1074
1279
|
this.uiConfig = { ...this.uiConfig, placeholder: this.props.placeholder };
|
|
1075
1280
|
}
|
|
1076
1281
|
}
|
|
@@ -1306,71 +1511,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
1306
1511
|
args: [{ required: true }]
|
|
1307
1512
|
}] } });
|
|
1308
1513
|
|
|
1309
|
-
/**
|
|
1310
|
-
* Types for mn-lib configuration.
|
|
1311
|
-
*/
|
|
1312
|
-
|
|
1313
|
-
/**
|
|
1314
|
-
* Provides an APP_INITIALIZER that loads the mn-lib configuration from the given URL
|
|
1315
|
-
* during application bootstrap. The consuming application is responsible for providing
|
|
1316
|
-
* HttpClient (e.g., via HttpClientModule or provideHttpClient()).
|
|
1317
|
-
*/
|
|
1318
|
-
function provideMnConfig(url, debugMode = false) {
|
|
1319
|
-
return [
|
|
1320
|
-
{
|
|
1321
|
-
provide: APP_INITIALIZER,
|
|
1322
|
-
multi: true,
|
|
1323
|
-
useFactory: (svc) => () => svc.load(url, debugMode),
|
|
1324
|
-
deps: [MnConfigService],
|
|
1325
|
-
},
|
|
1326
|
-
];
|
|
1327
|
-
}
|
|
1328
|
-
|
|
1329
|
-
/**
|
|
1330
|
-
* Helper to provide a resolved, typed component config via DI.
|
|
1331
|
-
*
|
|
1332
|
-
* Usage in a component/module providers:
|
|
1333
|
-
* const MY_CFG = new InjectionToken<MyCfg>('MY_CFG');
|
|
1334
|
-
* providers: [ provideMnComponentConfig(MY_CFG, 'my-component') ]
|
|
1335
|
-
* Then in the component:
|
|
1336
|
-
* readonly cfg = inject(MY_CFG)
|
|
1337
|
-
*
|
|
1338
|
-
* The returned config object is **reactive**: when the active locale changes,
|
|
1339
|
-
* all translatable values are re-resolved in place so that templates using
|
|
1340
|
-
* `cfg.someLabel` automatically reflect the new language on the next change-detection cycle.
|
|
1341
|
-
*/
|
|
1342
|
-
function provideMnComponentConfig(token, componentName, initial) {
|
|
1343
|
-
return {
|
|
1344
|
-
provide: token,
|
|
1345
|
-
deps: [
|
|
1346
|
-
MnConfigService,
|
|
1347
|
-
MnLanguageService,
|
|
1348
|
-
DestroyRef,
|
|
1349
|
-
[new Optional(), MN_SECTION_PATH],
|
|
1350
|
-
[new Optional(), MN_INSTANCE_ID],
|
|
1351
|
-
],
|
|
1352
|
-
useFactory: (svc, lang, destroyRef, sectionPath, instanceId) => {
|
|
1353
|
-
const resolveConfig = () => {
|
|
1354
|
-
const resolved = svc.resolve(componentName, sectionPath ?? [], instanceId ?? undefined);
|
|
1355
|
-
return Object.assign({}, initial ?? {}, resolved);
|
|
1356
|
-
};
|
|
1357
|
-
// Create the initial config object that will be shared by reference.
|
|
1358
|
-
const cfg = resolveConfig();
|
|
1359
|
-
// Re-resolve translatable values whenever the locale changes.
|
|
1360
|
-
// skip(1) because the current locale was already used for the initial resolve.
|
|
1361
|
-
const sub = lang.locale$.subscribe(() => {
|
|
1362
|
-
const updated = resolveConfig();
|
|
1363
|
-
// Mutate the existing object in place so all template bindings pick up the new values.
|
|
1364
|
-
for (const key of Object.keys(updated)) {
|
|
1365
|
-
cfg[key] = updated[key];
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
destroyRef.onDestroy(() => sub.unsubscribe());
|
|
1369
|
-
return cfg;
|
|
1370
|
-
},
|
|
1371
|
-
};
|
|
1372
|
-
}
|
|
1373
|
-
|
|
1374
1514
|
const MN_LIB_DUAL_HORIZONTAL_IMAGE = new InjectionToken('MN_LIB_DUAL_HORIZONTAL_IMAGE');
|
|
1375
1515
|
class MnDualHorizontalImage {
|
|
1376
1516
|
componentConfig = inject(MN_LIB_DUAL_HORIZONTAL_IMAGE);
|
|
@@ -1924,163 +2064,29 @@ const mnDatetimeVariants = tv({
|
|
|
1924
2064
|
lg: 'p-4',
|
|
1925
2065
|
},
|
|
1926
2066
|
borderRadius: {
|
|
1927
|
-
none: 'rounded-none',
|
|
1928
|
-
xs: 'rounded-xs',
|
|
1929
|
-
sm: 'rounded-sm',
|
|
1930
|
-
md: 'rounded-md',
|
|
1931
|
-
lg: 'rounded-lg',
|
|
1932
|
-
xl: 'rounded-xl',
|
|
1933
|
-
two_xl: 'rounded-2xl',
|
|
1934
|
-
three_xl: 'rounded-3xl',
|
|
1935
|
-
four_xl: 'rounded-4xl',
|
|
1936
|
-
},
|
|
1937
|
-
fullWidth: {
|
|
1938
|
-
true: 'w-full',
|
|
1939
|
-
},
|
|
1940
|
-
hover: {
|
|
1941
|
-
true: 'hover:cursor-pointer hover:bg-base-200 transition-colors duration-300 ease-in-out',
|
|
1942
|
-
},
|
|
1943
|
-
},
|
|
1944
|
-
defaultVariants: {
|
|
1945
|
-
size: 'md',
|
|
1946
|
-
borderRadius: 'md',
|
|
1947
|
-
hover: true,
|
|
1948
|
-
},
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
class MnSectionDirective {
|
|
1952
|
-
/** Section name contributed by this DOM node to the section path */
|
|
1953
|
-
mnSection;
|
|
1954
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1955
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnSectionDirective, isStandalone: true, selector: "[mn-section]", inputs: { mnSection: ["mn-section", "mnSection"] }, providers: [
|
|
1956
|
-
{
|
|
1957
|
-
provide: MN_SECTION_PATH,
|
|
1958
|
-
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
1959
|
-
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
1960
|
-
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
1961
|
-
useFactory: (parentPath, attr) => {
|
|
1962
|
-
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
1963
|
-
const name = (attr ?? '').trim();
|
|
1964
|
-
return name ? [...parent, name] : [...parent];
|
|
1965
|
-
},
|
|
1966
|
-
},
|
|
1967
|
-
], ngImport: i0 });
|
|
1968
|
-
}
|
|
1969
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnSectionDirective, decorators: [{
|
|
1970
|
-
type: Directive,
|
|
1971
|
-
args: [{
|
|
1972
|
-
selector: '[mn-section]',
|
|
1973
|
-
standalone: true,
|
|
1974
|
-
providers: [
|
|
1975
|
-
{
|
|
1976
|
-
provide: MN_SECTION_PATH,
|
|
1977
|
-
// Read parent MN_SECTION_PATH from ancestor injector (skipSelf to avoid self-reference),
|
|
1978
|
-
// and read the attribute value using Attribute so it's available at provider creation time.
|
|
1979
|
-
deps: [[new Optional(), new SkipSelf(), MN_SECTION_PATH], new Attribute('mn-section')],
|
|
1980
|
-
useFactory: (parentPath, attr) => {
|
|
1981
|
-
const parent = Array.isArray(parentPath) ? parentPath : [];
|
|
1982
|
-
const name = (attr ?? '').trim();
|
|
1983
|
-
return name ? [...parent, name] : [...parent];
|
|
1984
|
-
},
|
|
1985
|
-
},
|
|
1986
|
-
],
|
|
1987
|
-
}]
|
|
1988
|
-
}], propDecorators: { mnSection: [{
|
|
1989
|
-
type: Input,
|
|
1990
|
-
args: ['mn-section']
|
|
1991
|
-
}] } });
|
|
1992
|
-
|
|
1993
|
-
class MnInstanceDirective {
|
|
1994
|
-
/** Instance id for targeting per-component instance overrides */
|
|
1995
|
-
mnInstance;
|
|
1996
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1997
|
-
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: MnInstanceDirective, isStandalone: true, selector: "[mn-instance]", inputs: { mnInstance: ["mn-instance", "mnInstance"] }, providers: [
|
|
1998
|
-
{
|
|
1999
|
-
provide: MN_INSTANCE_ID,
|
|
2000
|
-
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
2001
|
-
deps: [new Attribute('mn-instance')],
|
|
2002
|
-
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
2003
|
-
},
|
|
2004
|
-
], ngImport: i0 });
|
|
2005
|
-
}
|
|
2006
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnInstanceDirective, decorators: [{
|
|
2007
|
-
type: Directive,
|
|
2008
|
-
args: [{
|
|
2009
|
-
selector: '[mn-instance]',
|
|
2010
|
-
standalone: true,
|
|
2011
|
-
providers: [
|
|
2012
|
-
{
|
|
2013
|
-
provide: MN_INSTANCE_ID,
|
|
2014
|
-
// Read the attribute at provider creation time using Attribute token; Inputs may not be set yet.
|
|
2015
|
-
deps: [new Attribute('mn-instance')],
|
|
2016
|
-
useFactory: (attr) => (attr ?? '').trim() || null,
|
|
2017
|
-
},
|
|
2018
|
-
],
|
|
2019
|
-
}]
|
|
2020
|
-
}], propDecorators: { mnInstance: [{
|
|
2021
|
-
type: Input,
|
|
2022
|
-
args: ['mn-instance']
|
|
2023
|
-
}] } });
|
|
2024
|
-
|
|
2025
|
-
/**
|
|
2026
|
-
* Provides an APP_INITIALIZER that configures the MnLanguageService and
|
|
2027
|
-
* preloads the requested locales during application bootstrap.
|
|
2028
|
-
*
|
|
2029
|
-
* Usage in app.config.ts:
|
|
2030
|
-
* ...provideMnLanguage({
|
|
2031
|
-
* urlPattern: 'assets/i18n/{locale}.json',
|
|
2032
|
-
* defaultLocale: 'en',
|
|
2033
|
-
* preload: ['en', 'nl'],
|
|
2034
|
-
* })
|
|
2035
|
-
*/
|
|
2036
|
-
function provideMnLanguage(config) {
|
|
2037
|
-
return [
|
|
2038
|
-
{
|
|
2039
|
-
provide: APP_INITIALIZER,
|
|
2040
|
-
multi: true,
|
|
2041
|
-
useFactory: (svc) => async () => {
|
|
2042
|
-
if (config.debug) {
|
|
2043
|
-
svc.setDebug(true);
|
|
2044
|
-
}
|
|
2045
|
-
svc.configure(config.urlPattern);
|
|
2046
|
-
const effectiveLocale = svc.resolveLocaleForDomain(config.domainLocaleMap, config.defaultLocale);
|
|
2047
|
-
const localesToLoad = config.preload ?? [effectiveLocale];
|
|
2048
|
-
await Promise.all(localesToLoad.map(l => svc.loadLocale(l)));
|
|
2049
|
-
await svc.setLocale(effectiveLocale);
|
|
2050
|
-
},
|
|
2051
|
-
deps: [MnLanguageService],
|
|
2067
|
+
none: 'rounded-none',
|
|
2068
|
+
xs: 'rounded-xs',
|
|
2069
|
+
sm: 'rounded-sm',
|
|
2070
|
+
md: 'rounded-md',
|
|
2071
|
+
lg: 'rounded-lg',
|
|
2072
|
+
xl: 'rounded-xl',
|
|
2073
|
+
two_xl: 'rounded-2xl',
|
|
2074
|
+
three_xl: 'rounded-3xl',
|
|
2075
|
+
four_xl: 'rounded-4xl',
|
|
2052
2076
|
},
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
lang;
|
|
2067
|
-
constructor(lang) {
|
|
2068
|
-
this.lang = lang;
|
|
2069
|
-
}
|
|
2070
|
-
transform(key, params) {
|
|
2071
|
-
return this.lang.translate(key, params);
|
|
2072
|
-
}
|
|
2073
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, deps: [{ token: MnLanguageService }], target: i0.ɵɵFactoryTarget.Pipe });
|
|
2074
|
-
static ɵpipe = i0.ɵɵngDeclarePipe({ minVersion: "14.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, isStandalone: true, name: "mnTranslate", pure: false });
|
|
2075
|
-
}
|
|
2076
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTranslatePipe, decorators: [{
|
|
2077
|
-
type: Pipe,
|
|
2078
|
-
args: [{
|
|
2079
|
-
name: 'mnTranslate',
|
|
2080
|
-
standalone: true,
|
|
2081
|
-
pure: false,
|
|
2082
|
-
}]
|
|
2083
|
-
}], ctorParameters: () => [{ type: MnLanguageService }] });
|
|
2077
|
+
fullWidth: {
|
|
2078
|
+
true: 'w-full',
|
|
2079
|
+
},
|
|
2080
|
+
hover: {
|
|
2081
|
+
true: 'hover:cursor-pointer hover:bg-base-200 transition-colors duration-300 ease-in-out',
|
|
2082
|
+
},
|
|
2083
|
+
},
|
|
2084
|
+
defaultVariants: {
|
|
2085
|
+
size: 'md',
|
|
2086
|
+
borderRadius: 'md',
|
|
2087
|
+
hover: true,
|
|
2088
|
+
},
|
|
2089
|
+
});
|
|
2084
2090
|
|
|
2085
2091
|
const MN_DATETIME_CONFIG = new InjectionToken('MN_DATETIME_CONFIG');
|
|
2086
2092
|
class MnDatetime {
|
|
@@ -3576,45 +3582,25 @@ class MnTable {
|
|
|
3576
3582
|
this.cdr.markForCheck();
|
|
3577
3583
|
}
|
|
3578
3584
|
}
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
this.columnFilters[col.key] = '';
|
|
3586
|
-
}
|
|
3587
|
-
}
|
|
3588
|
-
// Pre-select rows from initialSelectedIds if provided
|
|
3589
|
-
if (this.dataSource.initialSelectedIds?.length) {
|
|
3590
|
-
for (const id of this.dataSource.initialSelectedIds) {
|
|
3591
|
-
this.selectedIds.add(id);
|
|
3592
|
-
}
|
|
3593
|
-
this.emitSelection();
|
|
3585
|
+
get showLoadMore() {
|
|
3586
|
+
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
3587
|
+
// Server-side load-more: check if there are more items to load.
|
|
3588
|
+
if (this.dataSource.onLoadMore) {
|
|
3589
|
+
const totalItems = this.dataSource.totalItems ?? 0;
|
|
3590
|
+
return mode === 'load-more' && this.filteredItems.length < totalItems;
|
|
3594
3591
|
}
|
|
3595
|
-
this.
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
|
|
3599
|
-
this.applyFilterAndSort(false);
|
|
3600
|
-
this.cdr.markForCheck();
|
|
3601
|
-
});
|
|
3602
|
-
this.searchSubscription = this.searchSubject
|
|
3603
|
-
.pipe(debounceTime(300))
|
|
3604
|
-
.subscribe(value => {
|
|
3605
|
-
this.searchValue = value;
|
|
3606
|
-
this.applyFilterAndSort(true);
|
|
3607
|
-
this.cdr.markForCheck();
|
|
3608
|
-
});
|
|
3592
|
+
const strategy = this.dataSource.paginationStrategy;
|
|
3593
|
+
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
3594
|
+
return mode === 'load-more' && hasMore;
|
|
3609
3595
|
}
|
|
3610
3596
|
ngOnDestroy() {
|
|
3611
3597
|
this.dataSubscription?.unsubscribe();
|
|
3612
3598
|
this.searchSubscription?.unsubscribe();
|
|
3613
3599
|
}
|
|
3614
3600
|
// ── Search ──
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3601
|
+
get isPaginated() {
|
|
3602
|
+
const mode = this.dataSource.paginationMode;
|
|
3603
|
+
return mode === 'paginated' || mode === 'client-side-pagination';
|
|
3618
3604
|
}
|
|
3619
3605
|
// ── Column Filters ──
|
|
3620
3606
|
/** Whether any column has filtering enabled. */
|
|
@@ -3696,7 +3682,75 @@ class MnTable {
|
|
|
3696
3682
|
this.rowClick.emit(row);
|
|
3697
3683
|
}
|
|
3698
3684
|
// ── Pagination ──
|
|
3685
|
+
/** Whether the table delegates pagination to the consumer (server-side). */
|
|
3686
|
+
get isServerPaginated() {
|
|
3687
|
+
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
3688
|
+
return mode === 'paginated' || mode === 'load-more';
|
|
3689
|
+
}
|
|
3690
|
+
/** Whether the table delegates search to the consumer (server-side). */
|
|
3691
|
+
get isServerSearched() {
|
|
3692
|
+
return !!this.dataSource.onServerSearch;
|
|
3693
|
+
}
|
|
3694
|
+
// ── Paginated Mode ──
|
|
3695
|
+
/** Total number of items, accounting for server-side pagination. */
|
|
3696
|
+
get totalItemCount() {
|
|
3697
|
+
if (this.isServerPaginated && this.dataSource.totalItems != null) {
|
|
3698
|
+
return this.dataSource.totalItems;
|
|
3699
|
+
}
|
|
3700
|
+
return this.filteredItems.length;
|
|
3701
|
+
}
|
|
3702
|
+
get totalPages() {
|
|
3703
|
+
return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
|
|
3704
|
+
}
|
|
3705
|
+
ngOnInit() {
|
|
3706
|
+
this.validateDataSource();
|
|
3707
|
+
this.currentSort = this.dataSource.defaultSort ?? null;
|
|
3708
|
+
this.pageSize = this.dataSource.pageSize ?? 10;
|
|
3709
|
+
// Initialize all filterable columns with empty string to avoid undefined values.
|
|
3710
|
+
for (const col of this.dataSource.columns) {
|
|
3711
|
+
if (col.filterable) {
|
|
3712
|
+
this.columnFilters[col.key] = '';
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
// Pre-select rows from initialSelectedIds if provided
|
|
3716
|
+
if (this.dataSource.initialSelectedIds?.length) {
|
|
3717
|
+
for (const id of this.dataSource.initialSelectedIds) {
|
|
3718
|
+
this.selectedIds.add(id);
|
|
3719
|
+
}
|
|
3720
|
+
this.emitSelection();
|
|
3721
|
+
}
|
|
3722
|
+
this.applyFilterAndSort(false);
|
|
3723
|
+
// Skip the initial BehaviorSubject emission (already handled above)
|
|
3724
|
+
// to avoid triggering markForCheck during the first change detection cycle.
|
|
3725
|
+
this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
|
|
3726
|
+
this.applyFilterAndSort(false);
|
|
3727
|
+
this.cdr.markForCheck();
|
|
3728
|
+
});
|
|
3729
|
+
this.searchSubscription = this.searchSubject
|
|
3730
|
+
.pipe(debounceTime(300))
|
|
3731
|
+
.subscribe(value => {
|
|
3732
|
+
this.searchValue = value;
|
|
3733
|
+
this.applyFilterAndSort(true);
|
|
3734
|
+
this.cdr.markForCheck();
|
|
3735
|
+
});
|
|
3736
|
+
}
|
|
3737
|
+
onSearch(searchString) {
|
|
3738
|
+
this.currentPage = 1;
|
|
3739
|
+
if (this.isServerSearched) {
|
|
3740
|
+
this.searchValue = searchString;
|
|
3741
|
+
this.dataSource.onServerSearch(searchString);
|
|
3742
|
+
this.cdr.markForCheck();
|
|
3743
|
+
}
|
|
3744
|
+
else {
|
|
3745
|
+
this.searchSubject.next(searchString);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3699
3748
|
loadMoreRows() {
|
|
3749
|
+
// Server-side infinite scroll: delegate to consumer callback.
|
|
3750
|
+
if (this.dataSource.onLoadMore) {
|
|
3751
|
+
this.dataSource.onLoadMore();
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3700
3754
|
if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
|
|
3701
3755
|
return;
|
|
3702
3756
|
this.loadingMoreRows = true;
|
|
@@ -3707,19 +3761,6 @@ class MnTable {
|
|
|
3707
3761
|
.then(rows => this.processLoadedRows(rows))
|
|
3708
3762
|
.catch(() => this.loadingMoreRows = false);
|
|
3709
3763
|
}
|
|
3710
|
-
get showLoadMore() {
|
|
3711
|
-
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
3712
|
-
const strategy = this.dataSource.paginationStrategy;
|
|
3713
|
-
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
3714
|
-
return mode === 'load-more' && hasMore;
|
|
3715
|
-
}
|
|
3716
|
-
// ── Paginated Mode ──
|
|
3717
|
-
get isPaginated() {
|
|
3718
|
-
return this.dataSource.paginationMode === 'paginated';
|
|
3719
|
-
}
|
|
3720
|
-
get totalPages() {
|
|
3721
|
-
return Math.max(1, Math.ceil(this.filteredItems.length / this.pageSize));
|
|
3722
|
-
}
|
|
3723
3764
|
get resolvedPageSizeOptions() {
|
|
3724
3765
|
return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
3725
3766
|
}
|
|
@@ -3727,15 +3768,24 @@ class MnTable {
|
|
|
3727
3768
|
if (page < 1 || page > this.totalPages)
|
|
3728
3769
|
return;
|
|
3729
3770
|
this.currentPage = page;
|
|
3730
|
-
this.
|
|
3771
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
3772
|
+
this.applyPagination();
|
|
3773
|
+
}
|
|
3774
|
+
else {
|
|
3775
|
+
this.dataSource.onPageChange?.(page);
|
|
3776
|
+
}
|
|
3731
3777
|
this.cdr.markForCheck();
|
|
3732
3778
|
}
|
|
3733
3779
|
onPageSizeChange(newSize) {
|
|
3734
3780
|
this.pageSize = newSize;
|
|
3735
3781
|
this.currentPage = 1;
|
|
3736
|
-
this.
|
|
3782
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
3783
|
+
this.applyPagination();
|
|
3784
|
+
}
|
|
3785
|
+
else {
|
|
3786
|
+
this.dataSource.onPageSizeChange?.(newSize);
|
|
3787
|
+
}
|
|
3737
3788
|
this.cdr.markForCheck();
|
|
3738
|
-
this.dataSource.onPageSizeChange?.(newSize);
|
|
3739
3789
|
}
|
|
3740
3790
|
get visiblePages() {
|
|
3741
3791
|
const total = this.totalPages;
|
|
@@ -3754,14 +3804,12 @@ class MnTable {
|
|
|
3754
3804
|
return pages;
|
|
3755
3805
|
}
|
|
3756
3806
|
applyPagination() {
|
|
3757
|
-
if (this.
|
|
3758
|
-
if (this.currentPage > this.totalPages) {
|
|
3759
|
-
this.currentPage = this.totalPages;
|
|
3760
|
-
}
|
|
3807
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
3761
3808
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
3762
3809
|
this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
|
|
3763
3810
|
}
|
|
3764
3811
|
else {
|
|
3812
|
+
// Server always provides the correct page/slice — no client-side slicing.
|
|
3765
3813
|
this.paginatedItems = this.filteredItems;
|
|
3766
3814
|
}
|
|
3767
3815
|
}
|
|
@@ -3797,8 +3845,8 @@ class MnTable {
|
|
|
3797
3845
|
// ── Private ──
|
|
3798
3846
|
applyFilterAndSort(searchForItems) {
|
|
3799
3847
|
let items = this.dataSource.dataRows.value;
|
|
3800
|
-
//
|
|
3801
|
-
if (this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
|
|
3848
|
+
// Skip client-side search filtering when server handles it.
|
|
3849
|
+
if (!this.isServerSearched && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
|
|
3802
3850
|
const term = this.searchValue.toLowerCase();
|
|
3803
3851
|
items = items.filter(row => this.dataSource.isInSearch(row, term));
|
|
3804
3852
|
}
|
|
@@ -3864,17 +3912,33 @@ class MnTable {
|
|
|
3864
3912
|
this.loadingMoreRows = false;
|
|
3865
3913
|
this.applyFilterAndSort(false);
|
|
3866
3914
|
}
|
|
3915
|
+
validateDataSource() {
|
|
3916
|
+
const mode = this.dataSource.paginationMode;
|
|
3917
|
+
if (mode === 'paginated') {
|
|
3918
|
+
if (!this.dataSource.onPageChange) {
|
|
3919
|
+
throw new Error(`[MnTable] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
|
|
3920
|
+
}
|
|
3921
|
+
if (this.dataSource.totalItems == null) {
|
|
3922
|
+
throw new Error(`[MnTable] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
if (mode === 'load-more' || mode === 'infinite-scroll') {
|
|
3926
|
+
if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
|
|
3927
|
+
throw new Error(`[MnTable] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3867
3931
|
emitSelection() {
|
|
3868
3932
|
const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
3869
3933
|
this.dataSource.selectedRows?.next(rows);
|
|
3870
3934
|
this.selectionChange.emit(rows);
|
|
3871
3935
|
}
|
|
3872
3936
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
3873
|
-
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: 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-100 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-primary w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search table\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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-1\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <input\n type=\"text\"\n class=\"input input-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content placeholder-base-content/50 mb-2 focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer\"\n [placeholder]=\"column.filterPlaceholder ?? ''\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n [attr.autocomplete]=\"column.filterAutocomplete ?? null\"\n [attr.maxlength]=\"column.filterMaxLength ?? null\"\n (input)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n />\n } @else if (column.filterType === 'select') {\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer mb-2\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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) {\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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\u2013{{ currentPage * pageSize > filteredItems.length ? filteredItems.length : currentPage * pageSize }} of {{ filteredItems.length }}</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: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3937
|
+
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: search + custom toolbar template -->\n<div class=\"flex flex-row items-center justify-end gap-2 mb-3\">\n @if (dataSource.canSearch) {\n <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-sm text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer \"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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: "directive", type: MnHiddenBelowDirective, selector: "[mnHiddenBelow]", inputs: ["mnHiddenBelow"] }, { kind: "component", type: MnInputField, selector: "mn-lib-input-field", inputs: ["props"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
3874
3938
|
}
|
|
3875
3939
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
3876
3940
|
type: Component,
|
|
3877
|
-
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnHiddenBelowDirective], 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-100 px-3 py-1.5 text-sm text-base-content placeholder-base-content/50 focus:outline-none focus:border-primary w-full max-w-xs\"\n [placeholder]=\"dataSource.searchPlaceholder ?? 'Search...'\"\n (input)=\"onSearch($any($event.target).value)\"\n aria-label=\"Search table\"\n />\n </div>\n }\n @if (dataSource.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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-1\"\n [mnHiddenBelow]=\"column.hiddenBelow\"\n >\n @if (column.filterable) {\n @if ((column.filterType ?? 'text') === 'text') {\n <input\n type=\"text\"\n class=\"input input-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content placeholder-base-content/50 mb-2 focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer\"\n [placeholder]=\"column.filterPlaceholder ?? ''\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n [attr.autocomplete]=\"column.filterAutocomplete ?? null\"\n [attr.maxlength]=\"column.filterMaxLength ?? null\"\n (input)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n />\n } @else if (column.filterType === 'select') {\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-xs text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer mb-2\"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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) {\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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </select>\n </div>\n\n <div class=\"flex items-center gap-1\">\n <span class=\"text-xs mr-2\">{{ (currentPage - 1) * pageSize + 1 }}\u2013{{ currentPage * pageSize > filteredItems.length ? filteredItems.length : currentPage * pageSize }} of {{ filteredItems.length }}</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" }]
|
|
3941
|
+
args: [{ selector: 'mn-table', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton, MnHiddenBelowDirective, MnInputField, FormsModule], 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 <mn-lib-input-field\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.toolbarTemplate) {\n <ng-container [ngTemplateOutlet]=\"dataSource.toolbarTemplate\"></ng-container>\n }\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 <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all rows\"\n />\n </label>\n }\n </th>\n }\n\n <!-- Data columns -->\n @for (column of dataSource.columns; track column.key) {\n <th\n class=\"text-sm px-4 py-2\"\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-100 p-2 text-sm text-base-content focus:outline-none focus:border-primary w-full hover:bg-base-200 hover:cursor-pointer \"\n [value]=\"columnFilters[column.key]\"\n [disabled]=\"column.filterDisabled ?? false\"\n (change)=\"onColumnFilter(column.key, $any($event.target).value)\"\n (click)=\"$event.stopPropagation()\"\n [attr.aria-label]=\"'Filter ' + column.key\"\n >\n <option value=\"\">{{ column.filterPlaceholder ?? 'All' }}</option>\n @for (opt of column.filterOptions ?? []; track opt.value) {\n <option [value]=\"opt.value\">{{ opt.label }}</option>\n }\n </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 class=\"w-10 text-center px-2 py-2\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(row)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleRow(row)\"\n />\n </label>\n </td>\n }\n\n <!-- Data cells -->\n @for (column of dataSource.columns; track column.key) {\n <td\n class=\"text-xs px-4 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 (isTemplateRef(column.cell)) {\n <ng-container [ngTemplateOutlet]=\"$any(column.cell)\" [ngTemplateOutletContext]=\"{ $implicit: row, data: row }\"></ng-container>\n } @else {\n {{ getCellValue(column, row) }}\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: '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 || 'Rows per page:' }}</span>\n <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Rows per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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" }]
|
|
3878
3942
|
}], propDecorators: { dataSource: [{
|
|
3879
3943
|
type: Input
|
|
3880
3944
|
}], sortChange: [{
|
|
@@ -5480,36 +5544,25 @@ class MnList {
|
|
|
5480
5544
|
this.cdr.markForCheck();
|
|
5481
5545
|
}
|
|
5482
5546
|
}
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
//
|
|
5486
|
-
if (this.dataSource.
|
|
5487
|
-
|
|
5488
|
-
|
|
5489
|
-
}
|
|
5490
|
-
this.emitSelection();
|
|
5547
|
+
get showLoadMore() {
|
|
5548
|
+
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
5549
|
+
// Server-side load-more: check if there are more items to load.
|
|
5550
|
+
if (this.dataSource.onLoadMore) {
|
|
5551
|
+
const totalItems = this.dataSource.totalItems ?? 0;
|
|
5552
|
+
return mode === 'load-more' && this.filteredItems.length < totalItems;
|
|
5491
5553
|
}
|
|
5492
|
-
this.
|
|
5493
|
-
|
|
5494
|
-
|
|
5495
|
-
this.cdr.markForCheck();
|
|
5496
|
-
});
|
|
5497
|
-
this.searchSubscription = this.searchSubject
|
|
5498
|
-
.pipe(debounceTime(300))
|
|
5499
|
-
.subscribe(value => {
|
|
5500
|
-
this.searchValue = value;
|
|
5501
|
-
this.applyFilter(true);
|
|
5502
|
-
this.cdr.markForCheck();
|
|
5503
|
-
});
|
|
5554
|
+
const strategy = this.dataSource.paginationStrategy;
|
|
5555
|
+
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
5556
|
+
return mode === 'load-more' && hasMore;
|
|
5504
5557
|
}
|
|
5505
5558
|
ngOnDestroy() {
|
|
5506
5559
|
this.dataSubscription?.unsubscribe();
|
|
5507
5560
|
this.searchSubscription?.unsubscribe();
|
|
5508
5561
|
}
|
|
5509
5562
|
// ── Search ──
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5563
|
+
get isPaginated() {
|
|
5564
|
+
const mode = this.dataSource.paginationMode;
|
|
5565
|
+
return mode === 'paginated' || mode === 'client-side-pagination';
|
|
5513
5566
|
}
|
|
5514
5567
|
// ── Selection ──
|
|
5515
5568
|
isSelected(item) {
|
|
@@ -5556,7 +5609,62 @@ class MnList {
|
|
|
5556
5609
|
this.itemClick.emit(item);
|
|
5557
5610
|
}
|
|
5558
5611
|
// ── Pagination ──
|
|
5612
|
+
/** Whether the list is in server-side paginated mode (always true when paginated or load-more). */
|
|
5613
|
+
get isServerPaginated() {
|
|
5614
|
+
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
5615
|
+
return mode === 'paginated' || mode === 'load-more';
|
|
5616
|
+
}
|
|
5617
|
+
/** Total number of items, accounting for server-side pagination. */
|
|
5618
|
+
get totalItemCount() {
|
|
5619
|
+
if (this.isServerPaginated && this.dataSource.totalItems != null) {
|
|
5620
|
+
return this.dataSource.totalItems;
|
|
5621
|
+
}
|
|
5622
|
+
return this.filteredItems.length;
|
|
5623
|
+
}
|
|
5624
|
+
// ── Paginated Mode ──
|
|
5625
|
+
get totalPages() {
|
|
5626
|
+
return Math.max(1, Math.ceil(this.totalItemCount / this.pageSize));
|
|
5627
|
+
}
|
|
5628
|
+
ngOnInit() {
|
|
5629
|
+
this.validateDataSource();
|
|
5630
|
+
this.pageSize = this.dataSource.pageSize ?? 10;
|
|
5631
|
+
// Pre-select items from initialSelectedIds if provided
|
|
5632
|
+
if (this.dataSource.initialSelectedIds?.length) {
|
|
5633
|
+
for (const id of this.dataSource.initialSelectedIds) {
|
|
5634
|
+
this.selectedIds.add(id);
|
|
5635
|
+
}
|
|
5636
|
+
this.emitSelection();
|
|
5637
|
+
}
|
|
5638
|
+
this.applyFilter(false);
|
|
5639
|
+
this.dataSubscription = this.dataSource.dataRows.pipe(skip(1)).subscribe(() => {
|
|
5640
|
+
this.applyFilter(false);
|
|
5641
|
+
this.cdr.markForCheck();
|
|
5642
|
+
});
|
|
5643
|
+
this.searchSubscription = this.searchSubject
|
|
5644
|
+
.pipe(debounceTime(300))
|
|
5645
|
+
.subscribe(value => {
|
|
5646
|
+
this.searchValue = value;
|
|
5647
|
+
this.applyFilter(true);
|
|
5648
|
+
this.cdr.markForCheck();
|
|
5649
|
+
});
|
|
5650
|
+
}
|
|
5651
|
+
onSearch(searchString) {
|
|
5652
|
+
this.currentPage = 1;
|
|
5653
|
+
if (this.isServerPaginated) {
|
|
5654
|
+
this.searchValue = searchString;
|
|
5655
|
+
this.dataSource.onServerSearch?.(searchString);
|
|
5656
|
+
this.cdr.markForCheck();
|
|
5657
|
+
}
|
|
5658
|
+
else {
|
|
5659
|
+
this.searchSubject.next(searchString);
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5559
5662
|
loadMoreRows() {
|
|
5663
|
+
// Server-side infinite scroll: delegate to consumer callback.
|
|
5664
|
+
if (this.dataSource.onLoadMore) {
|
|
5665
|
+
this.dataSource.onLoadMore();
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5560
5668
|
if (!this.dataSource.loadAdditionalRows || this.loadingMoreRows)
|
|
5561
5669
|
return;
|
|
5562
5670
|
this.loadingMoreRows = true;
|
|
@@ -5567,19 +5675,6 @@ class MnList {
|
|
|
5567
5675
|
.then(rows => this.processLoadedRows(rows))
|
|
5568
5676
|
.catch(() => this.loadingMoreRows = false);
|
|
5569
5677
|
}
|
|
5570
|
-
get showLoadMore() {
|
|
5571
|
-
const mode = this.dataSource.paginationMode ?? 'load-more';
|
|
5572
|
-
const strategy = this.dataSource.paginationStrategy;
|
|
5573
|
-
const hasMore = strategy ? strategy.hasMoreRows : !!this.dataSource.loadAdditionalRows;
|
|
5574
|
-
return mode === 'load-more' && hasMore;
|
|
5575
|
-
}
|
|
5576
|
-
// ── Paginated Mode ──
|
|
5577
|
-
get isPaginated() {
|
|
5578
|
-
return this.dataSource.paginationMode === 'paginated';
|
|
5579
|
-
}
|
|
5580
|
-
get totalPages() {
|
|
5581
|
-
return Math.max(1, Math.ceil(this.filteredItems.length / this.pageSize));
|
|
5582
|
-
}
|
|
5583
5678
|
get resolvedPageSizeOptions() {
|
|
5584
5679
|
return this.dataSource.pageSizeOptions ?? [5, 10, 25, 50];
|
|
5585
5680
|
}
|
|
@@ -5587,13 +5682,23 @@ class MnList {
|
|
|
5587
5682
|
if (page < 1 || page > this.totalPages)
|
|
5588
5683
|
return;
|
|
5589
5684
|
this.currentPage = page;
|
|
5590
|
-
this.
|
|
5685
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
5686
|
+
this.applyPagination();
|
|
5687
|
+
}
|
|
5688
|
+
else {
|
|
5689
|
+
this.dataSource.onPageChange?.(page);
|
|
5690
|
+
}
|
|
5591
5691
|
this.cdr.markForCheck();
|
|
5592
5692
|
}
|
|
5593
5693
|
onPageSizeChange(newSize) {
|
|
5594
5694
|
this.pageSize = newSize;
|
|
5595
5695
|
this.currentPage = 1;
|
|
5596
|
-
this.
|
|
5696
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
5697
|
+
this.applyPagination();
|
|
5698
|
+
}
|
|
5699
|
+
else {
|
|
5700
|
+
this.dataSource.onPageSizeChange?.(newSize);
|
|
5701
|
+
}
|
|
5597
5702
|
this.cdr.markForCheck();
|
|
5598
5703
|
}
|
|
5599
5704
|
get visiblePages() {
|
|
@@ -5621,21 +5726,19 @@ class MnList {
|
|
|
5621
5726
|
}
|
|
5622
5727
|
// ── Private ──
|
|
5623
5728
|
applyPagination() {
|
|
5624
|
-
if (this.
|
|
5625
|
-
if (this.currentPage > this.totalPages) {
|
|
5626
|
-
this.currentPage = this.totalPages;
|
|
5627
|
-
}
|
|
5729
|
+
if (this.dataSource.paginationMode === 'client-side-pagination') {
|
|
5628
5730
|
const start = (this.currentPage - 1) * this.pageSize;
|
|
5629
5731
|
this.paginatedItems = this.filteredItems.slice(start, start + this.pageSize);
|
|
5630
5732
|
}
|
|
5631
5733
|
else {
|
|
5734
|
+
// Server always provides the correct page/slice — no client-side slicing.
|
|
5632
5735
|
this.paginatedItems = this.filteredItems;
|
|
5633
5736
|
}
|
|
5634
5737
|
}
|
|
5635
5738
|
applyFilter(searchForItems) {
|
|
5636
5739
|
let items = this.dataSource.dataRows.value;
|
|
5637
|
-
//
|
|
5638
|
-
if (this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
|
|
5740
|
+
// Skip client-side search filtering when server handles it.
|
|
5741
|
+
if (!this.isServerPaginated && this.dataSource.isInSearch && this.dataSource.canSearch && this.searchValue.length > 0) {
|
|
5639
5742
|
const term = this.searchValue.toLowerCase();
|
|
5640
5743
|
items = items.filter(row => this.dataSource.isInSearch(row, term));
|
|
5641
5744
|
}
|
|
@@ -5651,17 +5754,33 @@ class MnList {
|
|
|
5651
5754
|
this.loadingMoreRows = false;
|
|
5652
5755
|
this.applyFilter(false);
|
|
5653
5756
|
}
|
|
5757
|
+
validateDataSource() {
|
|
5758
|
+
const mode = this.dataSource.paginationMode;
|
|
5759
|
+
if (mode === 'paginated') {
|
|
5760
|
+
if (!this.dataSource.onPageChange) {
|
|
5761
|
+
throw new Error(`[MnList] paginationMode is 'paginated' but 'onPageChange' callback is missing. Server-side pagination requires 'onPageChange'.`);
|
|
5762
|
+
}
|
|
5763
|
+
if (this.dataSource.totalItems == null) {
|
|
5764
|
+
throw new Error(`[MnList] paginationMode is 'paginated' but 'totalItems' is missing. Server-side pagination requires 'totalItems'.`);
|
|
5765
|
+
}
|
|
5766
|
+
}
|
|
5767
|
+
if (mode === 'load-more' || mode === 'infinite-scroll') {
|
|
5768
|
+
if (!this.dataSource.onLoadMore && !this.dataSource.loadAdditionalRows && !this.dataSource.paginationStrategy) {
|
|
5769
|
+
throw new Error(`[MnList] paginationMode is '${mode}' but no load-more mechanism is provided. Provide 'onLoadMore', 'loadAdditionalRows', or 'paginationStrategy'.`);
|
|
5770
|
+
}
|
|
5771
|
+
}
|
|
5772
|
+
}
|
|
5654
5773
|
emitSelection() {
|
|
5655
5774
|
const rows = this.dataSource.dataRows.value.filter(r => this.selectedIds.has(this.dataSource.getID(r)));
|
|
5656
5775
|
this.dataSource.selectedRows?.next(rows);
|
|
5657
5776
|
this.selectionChange.emit(rows);
|
|
5658
5777
|
}
|
|
5659
5778
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
5660
|
-
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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"
|
|
5779
|
+
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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Items per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
5661
5780
|
}
|
|
5662
5781
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnList, decorators: [{
|
|
5663
5782
|
type: Component,
|
|
5664
|
-
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton], 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=\"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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"
|
|
5783
|
+
args: [{ selector: 'mn-list', standalone: true, imports: [NgClass, NgTemplateOutlet, MnButton], 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=\"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 <label class=\"flex items-center gap-2 cursor-pointer\">\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"allSelected\"\n (change)=\"toggleAll()\"\n aria-label=\"Select all items\"\n />\n <span class=\"text-xs text-base-content/70\">Select all</span>\n </label>\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 (click)=\"onItemClick(item)\"\n >\n <!-- Selection checkbox -->\n @if (hasSelection) {\n <div class=\"shrink-0\">\n <label>\n <input\n type=\"checkbox\"\n class=\"checkbox checkbox-sm checkbox-primary\"\n [checked]=\"isSelected(item)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleItem(item)\"\n />\n </label>\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 <select\n class=\"select select-xs rounded border border-base-300 bg-base-200 px-2 py-1 text-xs focus:outline-none focus:border-brand-500\"\n [value]=\"pageSize\"\n (change)=\"onPageSizeChange(+$any($event.target).value)\"\n aria-label=\"Items per page\"\n >\n @for (opt of resolvedPageSizeOptions; track opt) {\n <option [value]=\"opt\" [selected]=\"opt === pageSize\">{{ opt }}</option>\n }\n </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" }]
|
|
5665
5784
|
}], propDecorators: { dataSource: [{
|
|
5666
5785
|
type: Input
|
|
5667
5786
|
}], selectionChange: [{
|