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