mn-angular-lib 1.0.2 → 1.0.3
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.
|
@@ -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);
|
|
@@ -1948,140 +2088,6 @@ const mnDatetimeVariants = tv({
|
|
|
1948
2088
|
},
|
|
1949
2089
|
});
|
|
1950
2090
|
|
|
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],
|
|
2052
|
-
},
|
|
2053
|
-
];
|
|
2054
|
-
}
|
|
2055
|
-
|
|
2056
|
-
/**
|
|
2057
|
-
* Pipe that translates a key via MnLanguageService.
|
|
2058
|
-
*
|
|
2059
|
-
* Usage in templates:
|
|
2060
|
-
* {{ 'form.email.label' | mnTranslate }}
|
|
2061
|
-
* {{ 'greeting' | mnTranslate:{ name: 'World' } }}
|
|
2062
|
-
*
|
|
2063
|
-
* Note: This pipe is impure so it re-evaluates when the locale changes.
|
|
2064
|
-
*/
|
|
2065
|
-
class MnTranslatePipe {
|
|
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 }] });
|
|
2084
|
-
|
|
2085
2091
|
const MN_DATETIME_CONFIG = new InjectionToken('MN_DATETIME_CONFIG');
|
|
2086
2092
|
class MnDatetime {
|
|
2087
2093
|
ngControl;
|
|
@@ -3870,11 +3876,11 @@ class MnTable {
|
|
|
3870
3876
|
this.selectionChange.emit(rows);
|
|
3871
3877
|
}
|
|
3872
3878
|
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 });
|
|
3879
|
+
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) {\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"] }, { 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
3880
|
}
|
|
3875
3881
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: MnTable, decorators: [{
|
|
3876
3882
|
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" }]
|
|
3883
|
+
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) {\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" }]
|
|
3878
3884
|
}], propDecorators: { dataSource: [{
|
|
3879
3885
|
type: Input
|
|
3880
3886
|
}], sortChange: [{
|