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