@nexterajs/ui 0.0.2 → 0.1.0

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.
Files changed (59) hide show
  1. package/README.md +279 -34
  2. package/fesm2022/nexterajs-ui-charts.mjs +1096 -0
  3. package/fesm2022/nexterajs-ui-charts.mjs.map +1 -0
  4. package/fesm2022/nexterajs-ui-core.mjs +652 -12
  5. package/fesm2022/nexterajs-ui-core.mjs.map +1 -1
  6. package/fesm2022/nexterajs-ui-display.mjs +1142 -0
  7. package/fesm2022/nexterajs-ui-display.mjs.map +1 -0
  8. package/fesm2022/nexterajs-ui-dropdowns.mjs +321 -33
  9. package/fesm2022/nexterajs-ui-dropdowns.mjs.map +1 -1
  10. package/fesm2022/nexterajs-ui-file-uploader.mjs +572 -10
  11. package/fesm2022/nexterajs-ui-file-uploader.mjs.map +1 -1
  12. package/fesm2022/nexterajs-ui-forms.mjs +2027 -136
  13. package/fesm2022/nexterajs-ui-forms.mjs.map +1 -1
  14. package/fesm2022/nexterajs-ui-grid.mjs +1222 -188
  15. package/fesm2022/nexterajs-ui-grid.mjs.map +1 -1
  16. package/fesm2022/nexterajs-ui-intl.mjs +103 -11
  17. package/fesm2022/nexterajs-ui-intl.mjs.map +1 -1
  18. package/fesm2022/nexterajs-ui-navigation.mjs +1054 -0
  19. package/fesm2022/nexterajs-ui-navigation.mjs.map +1 -0
  20. package/fesm2022/nexterajs-ui-notify.mjs +46 -44
  21. package/fesm2022/nexterajs-ui-notify.mjs.map +1 -1
  22. package/fesm2022/nexterajs-ui-overlay.mjs +1671 -0
  23. package/fesm2022/nexterajs-ui-overlay.mjs.map +1 -0
  24. package/fesm2022/nexterajs-ui-pivot.mjs +51 -26
  25. package/fesm2022/nexterajs-ui-pivot.mjs.map +1 -1
  26. package/fesm2022/nexterajs-ui-shell.mjs +542 -0
  27. package/fesm2022/nexterajs-ui-shell.mjs.map +1 -0
  28. package/fesm2022/nexterajs-ui-tree-view.mjs +5 -2
  29. package/fesm2022/nexterajs-ui-tree-view.mjs.map +1 -1
  30. package/fesm2022/nexterajs-ui-validation.mjs +42 -11
  31. package/fesm2022/nexterajs-ui-validation.mjs.map +1 -1
  32. package/fesm2022/nexterajs-ui.mjs +5 -0
  33. package/fesm2022/nexterajs-ui.mjs.map +1 -1
  34. package/package.json +33 -2
  35. package/styles/_dropdown.scss +5 -5
  36. package/styles/_editor.scss +52 -17
  37. package/styles/_mixins.scss +1 -1
  38. package/styles/_scrollbar.scss +5 -5
  39. package/styles/_status.scss +21 -0
  40. package/styles/_toast.scss +14 -14
  41. package/styles/_tokens.scss +420 -3
  42. package/styles/_widget.scss +14 -3
  43. package/styles/index.scss +6 -4
  44. package/types/nexterajs-ui-charts.d.ts +446 -0
  45. package/types/nexterajs-ui-core.d.ts +387 -22
  46. package/types/nexterajs-ui-display.d.ts +720 -0
  47. package/types/nexterajs-ui-dropdowns.d.ts +191 -48
  48. package/types/nexterajs-ui-file-uploader.d.ts +236 -7
  49. package/types/nexterajs-ui-forms.d.ts +675 -30
  50. package/types/nexterajs-ui-grid.d.ts +448 -93
  51. package/types/nexterajs-ui-intl.d.ts +39 -4
  52. package/types/nexterajs-ui-navigation.d.ts +531 -0
  53. package/types/nexterajs-ui-notify.d.ts +4 -2
  54. package/types/nexterajs-ui-overlay.d.ts +703 -0
  55. package/types/nexterajs-ui-pivot.d.ts +56 -48
  56. package/types/nexterajs-ui-shell.d.ts +294 -0
  57. package/types/nexterajs-ui-tree-view.d.ts +6 -3
  58. package/types/nexterajs-ui-validation.d.ts +20 -11
  59. package/types/nexterajs-ui.d.ts +5 -0
@@ -1,6 +1,10 @@
1
1
  import * as i0 from '@angular/core';
2
- import { input, output, viewChild, signal, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
- import { NXE_VALIDATION_TARGET } from '@nexterajs/ui/core';
2
+ import { input, output, viewChild, inject, signal, forwardRef, ViewEncapsulation, ChangeDetectionStrategy, Component, booleanAttribute, Injector, DestroyRef, computed, afterNextRender } from '@angular/core';
3
+ import { NxeMessageService, NXE_VALIDATION_TARGET, NxeFormControlBase, nxeFormatFileSize } from '@nexterajs/ui/core';
4
+ import { HttpClient, HttpEventType, HttpErrorResponse } from '@angular/common/http';
5
+ import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
6
+ import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
7
+ import { Subscription, throwError } from 'rxjs';
4
8
 
5
9
  /**
6
10
  * `nxe-file-uploader` — drop-in replacement for DevExtreme `dx-file-uploader`.
@@ -20,9 +24,11 @@ class NxeFileUploaderComponent {
20
24
  ...(ngDevMode ? [{ debugName: "allowCanceling" }] : /* istanbul ignore next */ []));
21
25
  accept = input('*', /* @ts-ignore */
22
26
  ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
23
- labelText = input('or Drop here', /* @ts-ignore */
27
+ /** Text beside the button; the active language's "or Drop here" when unset. */
28
+ labelText = input(undefined, /* @ts-ignore */
24
29
  ...(ngDevMode ? [{ debugName: "labelText" }] : /* istanbul ignore next */ []));
25
- selectButtonText = input('Select File', /* @ts-ignore */
30
+ /** The button's text; the active language's "Select File" when unset. */
31
+ selectButtonText = input(undefined, /* @ts-ignore */
26
32
  ...(ngDevMode ? [{ debugName: "selectButtonText" }] : /* istanbul ignore next */ []));
27
33
  /** Accepted for parity; only `useForm` is used (no server upload by the widget). It also
28
34
  * gates the cancel button, which dx omits in `useForm` mode. */
@@ -38,6 +44,8 @@ class NxeFileUploaderComponent {
38
44
  change = output();
39
45
  fileInput = viewChild.required('fileInput', /* @ts-ignore */
40
46
  ...(ngDevMode ? [{ debugName: "fileInput" }] : /* istanbul ignore next */ []));
47
+ /** The library's runtime-switchable messages. */
48
+ messages = inject(NxeMessageService);
41
49
  file = signal(null, /* @ts-ignore */
42
50
  ...(ngDevMode ? [{ debugName: "file" }] : /* istanbul ignore next */ []));
43
51
  dragOver = signal(false, /* @ts-ignore */
@@ -89,17 +97,17 @@ class NxeFileUploaderComponent {
89
97
  }
90
98
  formatSize(bytes) {
91
99
  if (bytes < 1024) {
92
- return `${bytes} b`;
100
+ return this.messages.t('uploader.sizeB', bytes);
93
101
  }
94
102
  if (bytes < 1024 * 1024) {
95
- return `${(bytes / 1024).toFixed(1)} Kb`;
103
+ return this.messages.t('uploader.sizeKb', (bytes / 1024).toFixed(1));
96
104
  }
97
- return `${(bytes / 1024 / 1024).toFixed(1)} Mb`;
105
+ return this.messages.t('uploader.sizeMb', (bytes / 1024 / 1024).toFixed(1));
98
106
  }
99
107
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFileUploaderComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
100
108
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeFileUploaderComponent, isStandalone: true, selector: "nxe-file-uploader", inputs: { allowCanceling: { classPropertyName: "allowCanceling", publicName: "allowCanceling", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, labelText: { classPropertyName: "labelText", publicName: "labelText", isSignal: true, isRequired: false, transformFunction: null }, selectButtonText: { classPropertyName: "selectButtonText", publicName: "selectButtonText", isSignal: true, isRequired: false, transformFunction: null }, uploadMode: { classPropertyName: "uploadMode", publicName: "uploadMode", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, name: { classPropertyName: "name", publicName: "name", isSignal: true, isRequired: false, transformFunction: null }, validationGroupInput: { classPropertyName: "validationGroupInput", publicName: "validationGroup", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { change: "change" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave()", "drop": "onDrop($event)" }, properties: { "class.nxe-fileuploader-empty": "!file()", "class.nxe-fileuploader-dragover": "dragOver()", "class.nxe-fileuploader-invalid": "!!validationError()", "class.nxe-invalid": "!!validationError()", "class.nxe-focused": "focused()" }, classAttribute: "nxe-widget nxe-file-uploader" }, providers: [
101
109
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeFileUploaderComponent) },
102
- ], viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"nxe-fileuploader-wrapper\">\r\n <div class=\"nxe-fileuploader-container\">\r\n <div class=\"nxe-fileuploader-content\">\r\n <div class=\"nxe-fileuploader-input-wrapper\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-button nxe-fileuploader-upload-button\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (click)=\"browse()\">\r\n {{ selectButtonText() }}\r\n </button>\r\n <div class=\"nxe-fileuploader-input-container\">\r\n <input\r\n #fileInput\r\n class=\"nxe-fileuploader-input\"\r\n type=\"file\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"multiple()\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (change)=\"onNativeChange($event)\" />\r\n <span class=\"nxe-fileuploader-input-label\">{{ labelText() }}</span>\r\n </div>\r\n </div>\r\n\r\n @if (file(); as f) {\r\n <div class=\"nxe-fileuploader-files-container\">\r\n <div class=\"nxe-fileuploader-file-container\">\r\n <div class=\"nxe-fileuploader-file\">\r\n <div class=\"nxe-fileuploader-file-info\">\r\n <span class=\"nxe-fileuploader-file-name\">{{ f.name }}</span>\r\n <span class=\"nxe-fileuploader-file-size\">{{ formatSize(f.size) }}</span>\r\n </div>\r\n </div>\r\n <!-- dx renders no cancel button at all in useForm mode: _getCancelButton\r\n returns null before it reads allowCanceling\r\n (m_file_uploader.js:535-537). -->\r\n @if (allowCanceling() && uploadMode() !== 'useForm') {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-cancel-button\"\r\n title=\"Cancel\"\r\n (click)=\"cancel()\">\r\n &times;\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n\r\n@if (validationError(); as err) {\r\n <div class=\"nxe-fileuploader-error\">{{ err }}</div>\r\n}\r\n\r\n<ng-content />\r\n", styles: ["nxe-file-uploader{display:block;position:relative}nxe-file-uploader .nxe-fileuploader-wrapper{padding-inline:0;padding-bottom:0}nxe-file-uploader.nxe-invalid .nxe-fileuploader-button,nxe-file-uploader.nxe-fileuploader-invalid .nxe-fileuploader-button{border-color:#d9534f}nxe-file-uploader .nxe-fileuploader-input-wrapper{display:flex;align-items:center;gap:8px;padding:4px 0;border:0}nxe-file-uploader .nxe-fileuploader-button{border:1px solid #ddd;border-radius:4px;background:#fff;color:#333;padding:6px 12px;font-size:var(--nxe-ui-font-size);line-height:1;cursor:pointer}nxe-file-uploader .nxe-fileuploader-button:hover{background:#f5f5f5}nxe-file-uploader .nxe-fileuploader-input{display:none}nxe-file-uploader .nxe-fileuploader-input-label{color:#999}nxe-file-uploader .nxe-fileuploader-files-container{margin-top:6px;padding-top:0}nxe-file-uploader .nxe-fileuploader-file{display:block}nxe-file-uploader .nxe-fileuploader-file-container{display:flex;align-items:center;gap:8px}nxe-file-uploader .nxe-fileuploader-file-name{font-weight:500;margin-right:6px}nxe-file-uploader .nxe-fileuploader-file-size{color:#999;font-size:10px}nxe-file-uploader .nxe-fileuploader-cancel-button{border:none;background:transparent;cursor:pointer;font-size:18px;line-height:1;color:#999}nxe-file-uploader .nxe-fileuploader-cancel-button:hover{color:#333}nxe-file-uploader.nxe-fileuploader-dragover .nxe-fileuploader-wrapper{outline:2px dashed #30374c;outline-offset:2px}nxe-file-uploader .nxe-fileuploader-error{color:#d9534f;font-size:12px;margin-top:4px;line-height:1.4}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
110
+ ], viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true, isSignal: true }], ngImport: i0, template: "<div class=\"nxe-fileuploader-wrapper\">\r\n <div class=\"nxe-fileuploader-container\">\r\n <div class=\"nxe-fileuploader-content\">\r\n <div class=\"nxe-fileuploader-input-wrapper\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-button nxe-fileuploader-upload-button\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (click)=\"browse()\">\r\n {{ selectButtonText() ?? messages.t('uploader.selectFile') }}\r\n </button>\r\n <div class=\"nxe-fileuploader-input-container\">\r\n <input\r\n #fileInput\r\n class=\"nxe-fileuploader-input\"\r\n type=\"file\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"multiple()\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (change)=\"onNativeChange($event)\" />\r\n <span class=\"nxe-fileuploader-input-label\">{{ labelText() ?? messages.t('uploader.dropHere') }}</span>\r\n </div>\r\n </div>\r\n\r\n @if (file(); as f) {\r\n <div class=\"nxe-fileuploader-files-container\">\r\n <div class=\"nxe-fileuploader-file-container\">\r\n <div class=\"nxe-fileuploader-file\">\r\n <div class=\"nxe-fileuploader-file-info\">\r\n <span class=\"nxe-fileuploader-file-name\">{{ f.name }}</span>\r\n <span class=\"nxe-fileuploader-file-size\">{{ formatSize(f.size) }}</span>\r\n </div>\r\n </div>\r\n <!-- dx renders no cancel button at all in useForm mode: _getCancelButton\r\n returns null before it reads allowCanceling\r\n (m_file_uploader.js:535-537). -->\r\n @if (allowCanceling() && uploadMode() !== 'useForm') {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-cancel-button\"\r\n [attr.title]=\"messages.t('uploader.cancel')\"\r\n (click)=\"cancel()\">\r\n &times;\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n\r\n@if (validationError(); as err) {\r\n <div class=\"nxe-fileuploader-error\">{{ err }}</div>\r\n}\r\n\r\n<ng-content />\r\n", styles: ["nxe-file-uploader{display:block;position:relative}nxe-file-uploader .nxe-fileuploader-wrapper{padding-inline:0;padding-bottom:0}nxe-file-uploader.nxe-invalid .nxe-fileuploader-button,nxe-file-uploader.nxe-fileuploader-invalid .nxe-fileuploader-button{border-color:var(--nxe-ui-invalid, #d9534f)}nxe-file-uploader .nxe-fileuploader-input-wrapper{display:flex;align-items:center;gap:8px;padding:4px 0;border:0}nxe-file-uploader .nxe-fileuploader-button{border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);padding:6px 12px;font-size:var(--nxe-ui-font-size);line-height:1;cursor:pointer}nxe-file-uploader .nxe-fileuploader-button:hover{background:var(--nxe-ui-surface-alt, #f5f5f5)}nxe-file-uploader .nxe-fileuploader-input{display:none}nxe-file-uploader .nxe-fileuploader-input-label{color:var(--nxe-ui-text-muted, #999)}nxe-file-uploader .nxe-fileuploader-files-container{margin-top:6px;padding-top:0}nxe-file-uploader .nxe-fileuploader-file{display:block}nxe-file-uploader .nxe-fileuploader-file-container{display:flex;align-items:center;gap:8px}nxe-file-uploader .nxe-fileuploader-file-name{font-weight:500;margin-right:6px}nxe-file-uploader .nxe-fileuploader-file-size{color:var(--nxe-ui-text-muted, #999);font-size:10px}nxe-file-uploader .nxe-fileuploader-cancel-button{border:none;background:transparent;cursor:pointer;font-size:18px;line-height:1;color:var(--nxe-ui-text-muted, #999)}nxe-file-uploader .nxe-fileuploader-cancel-button:hover{color:var(--nxe-ui-text, #333)}nxe-file-uploader.nxe-fileuploader-dragover .nxe-fileuploader-wrapper{outline:2px dashed var(--nxe-ui-ink-strong, #30374c);outline-offset:2px}nxe-file-uploader .nxe-fileuploader-error{color:var(--nxe-ui-invalid, #d9534f);font-size:12px;margin-top:4px;line-height:1.4}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
103
111
  }
104
112
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFileUploaderComponent, decorators: [{
105
113
  type: Component,
@@ -115,14 +123,568 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImpor
115
123
  '(drop)': 'onDrop($event)',
116
124
  }, providers: [
117
125
  { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeFileUploaderComponent) },
118
- ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-fileuploader-wrapper\">\r\n <div class=\"nxe-fileuploader-container\">\r\n <div class=\"nxe-fileuploader-content\">\r\n <div class=\"nxe-fileuploader-input-wrapper\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-button nxe-fileuploader-upload-button\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (click)=\"browse()\">\r\n {{ selectButtonText() }}\r\n </button>\r\n <div class=\"nxe-fileuploader-input-container\">\r\n <input\r\n #fileInput\r\n class=\"nxe-fileuploader-input\"\r\n type=\"file\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"multiple()\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (change)=\"onNativeChange($event)\" />\r\n <span class=\"nxe-fileuploader-input-label\">{{ labelText() }}</span>\r\n </div>\r\n </div>\r\n\r\n @if (file(); as f) {\r\n <div class=\"nxe-fileuploader-files-container\">\r\n <div class=\"nxe-fileuploader-file-container\">\r\n <div class=\"nxe-fileuploader-file\">\r\n <div class=\"nxe-fileuploader-file-info\">\r\n <span class=\"nxe-fileuploader-file-name\">{{ f.name }}</span>\r\n <span class=\"nxe-fileuploader-file-size\">{{ formatSize(f.size) }}</span>\r\n </div>\r\n </div>\r\n <!-- dx renders no cancel button at all in useForm mode: _getCancelButton\r\n returns null before it reads allowCanceling\r\n (m_file_uploader.js:535-537). -->\r\n @if (allowCanceling() && uploadMode() !== 'useForm') {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-cancel-button\"\r\n title=\"Cancel\"\r\n (click)=\"cancel()\">\r\n &times;\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n\r\n@if (validationError(); as err) {\r\n <div class=\"nxe-fileuploader-error\">{{ err }}</div>\r\n}\r\n\r\n<ng-content />\r\n", styles: ["nxe-file-uploader{display:block;position:relative}nxe-file-uploader .nxe-fileuploader-wrapper{padding-inline:0;padding-bottom:0}nxe-file-uploader.nxe-invalid .nxe-fileuploader-button,nxe-file-uploader.nxe-fileuploader-invalid .nxe-fileuploader-button{border-color:#d9534f}nxe-file-uploader .nxe-fileuploader-input-wrapper{display:flex;align-items:center;gap:8px;padding:4px 0;border:0}nxe-file-uploader .nxe-fileuploader-button{border:1px solid #ddd;border-radius:4px;background:#fff;color:#333;padding:6px 12px;font-size:var(--nxe-ui-font-size);line-height:1;cursor:pointer}nxe-file-uploader .nxe-fileuploader-button:hover{background:#f5f5f5}nxe-file-uploader .nxe-fileuploader-input{display:none}nxe-file-uploader .nxe-fileuploader-input-label{color:#999}nxe-file-uploader .nxe-fileuploader-files-container{margin-top:6px;padding-top:0}nxe-file-uploader .nxe-fileuploader-file{display:block}nxe-file-uploader .nxe-fileuploader-file-container{display:flex;align-items:center;gap:8px}nxe-file-uploader .nxe-fileuploader-file-name{font-weight:500;margin-right:6px}nxe-file-uploader .nxe-fileuploader-file-size{color:#999;font-size:10px}nxe-file-uploader .nxe-fileuploader-cancel-button{border:none;background:transparent;cursor:pointer;font-size:18px;line-height:1;color:#999}nxe-file-uploader .nxe-fileuploader-cancel-button:hover{color:#333}nxe-file-uploader.nxe-fileuploader-dragover .nxe-fileuploader-wrapper{outline:2px dashed #30374c;outline-offset:2px}nxe-file-uploader .nxe-fileuploader-error{color:#d9534f;font-size:12px;margin-top:4px;line-height:1.4}\n"] }]
126
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "<div class=\"nxe-fileuploader-wrapper\">\r\n <div class=\"nxe-fileuploader-container\">\r\n <div class=\"nxe-fileuploader-content\">\r\n <div class=\"nxe-fileuploader-input-wrapper\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-button nxe-fileuploader-upload-button\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (click)=\"browse()\">\r\n {{ selectButtonText() ?? messages.t('uploader.selectFile') }}\r\n </button>\r\n <div class=\"nxe-fileuploader-input-container\">\r\n <input\r\n #fileInput\r\n class=\"nxe-fileuploader-input\"\r\n type=\"file\"\r\n [accept]=\"accept()\"\r\n [multiple]=\"multiple()\"\r\n (focus)=\"focused.set(true)\"\r\n (blur)=\"focused.set(false)\"\r\n (change)=\"onNativeChange($event)\" />\r\n <span class=\"nxe-fileuploader-input-label\">{{ labelText() ?? messages.t('uploader.dropHere') }}</span>\r\n </div>\r\n </div>\r\n\r\n @if (file(); as f) {\r\n <div class=\"nxe-fileuploader-files-container\">\r\n <div class=\"nxe-fileuploader-file-container\">\r\n <div class=\"nxe-fileuploader-file\">\r\n <div class=\"nxe-fileuploader-file-info\">\r\n <span class=\"nxe-fileuploader-file-name\">{{ f.name }}</span>\r\n <span class=\"nxe-fileuploader-file-size\">{{ formatSize(f.size) }}</span>\r\n </div>\r\n </div>\r\n <!-- dx renders no cancel button at all in useForm mode: _getCancelButton\r\n returns null before it reads allowCanceling\r\n (m_file_uploader.js:535-537). -->\r\n @if (allowCanceling() && uploadMode() !== 'useForm') {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileuploader-cancel-button\"\r\n [attr.title]=\"messages.t('uploader.cancel')\"\r\n (click)=\"cancel()\">\r\n &times;\r\n </button>\r\n }\r\n </div>\r\n </div>\r\n }\r\n </div>\r\n </div>\r\n</div>\r\n\r\n@if (validationError(); as err) {\r\n <div class=\"nxe-fileuploader-error\">{{ err }}</div>\r\n}\r\n\r\n<ng-content />\r\n", styles: ["nxe-file-uploader{display:block;position:relative}nxe-file-uploader .nxe-fileuploader-wrapper{padding-inline:0;padding-bottom:0}nxe-file-uploader.nxe-invalid .nxe-fileuploader-button,nxe-file-uploader.nxe-fileuploader-invalid .nxe-fileuploader-button{border-color:var(--nxe-ui-invalid, #d9534f)}nxe-file-uploader .nxe-fileuploader-input-wrapper{display:flex;align-items:center;gap:8px;padding:4px 0;border:0}nxe-file-uploader .nxe-fileuploader-button{border:1px solid var(--nxe-ui-border, #ddd);border-radius:4px;background:var(--nxe-ui-surface, #fff);color:var(--nxe-ui-text, #333);padding:6px 12px;font-size:var(--nxe-ui-font-size);line-height:1;cursor:pointer}nxe-file-uploader .nxe-fileuploader-button:hover{background:var(--nxe-ui-surface-alt, #f5f5f5)}nxe-file-uploader .nxe-fileuploader-input{display:none}nxe-file-uploader .nxe-fileuploader-input-label{color:var(--nxe-ui-text-muted, #999)}nxe-file-uploader .nxe-fileuploader-files-container{margin-top:6px;padding-top:0}nxe-file-uploader .nxe-fileuploader-file{display:block}nxe-file-uploader .nxe-fileuploader-file-container{display:flex;align-items:center;gap:8px}nxe-file-uploader .nxe-fileuploader-file-name{font-weight:500;margin-right:6px}nxe-file-uploader .nxe-fileuploader-file-size{color:var(--nxe-ui-text-muted, #999);font-size:10px}nxe-file-uploader .nxe-fileuploader-cancel-button{border:none;background:transparent;cursor:pointer;font-size:18px;line-height:1;color:var(--nxe-ui-text-muted, #999)}nxe-file-uploader .nxe-fileuploader-cancel-button:hover{color:var(--nxe-ui-text, #333)}nxe-file-uploader.nxe-fileuploader-dragover .nxe-fileuploader-wrapper{outline:2px dashed var(--nxe-ui-ink-strong, #30374c);outline-offset:2px}nxe-file-uploader .nxe-fileuploader-error{color:var(--nxe-ui-invalid, #d9534f);font-size:12px;margin-top:4px;line-height:1.4}\n"] }]
119
127
  }], propDecorators: { allowCanceling: [{ type: i0.Input, args: [{ isSignal: true, alias: "allowCanceling", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], labelText: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelText", required: false }] }], selectButtonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectButtonText", required: false }] }], uploadMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadMode", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], name: [{ type: i0.Input, args: [{ isSignal: true, alias: "name", required: false }] }], validationGroupInput: [{ type: i0.Input, args: [{ isSignal: true, alias: "validationGroup", required: false }] }], change: [{ type: i0.Output, args: ["change"] }], fileInput: [{ type: i0.ViewChild, args: ['fileInput', { isSignal: true }] }] } });
120
128
 
129
+ /** Whether `file` matches an `accept` list: `.pdf`, `application/pdf`, `image/*`, comma separated. Empty or `*` accepts all. */
130
+ function nxeFileAccepted(file, accept) {
131
+ const tokens = accept
132
+ .split(',')
133
+ .map((token) => token.trim().toLowerCase())
134
+ .filter((token) => token !== '');
135
+ if (tokens.length === 0 || tokens.includes('*') || tokens.includes('*/*')) {
136
+ return true;
137
+ }
138
+ const name = file.name.toLowerCase();
139
+ const type = file.type.toLowerCase();
140
+ return tokens.some((token) => {
141
+ if (token.startsWith('.')) {
142
+ return name.endsWith(token);
143
+ }
144
+ if (token.endsWith('/*')) {
145
+ return type.startsWith(token.slice(0, -1));
146
+ }
147
+ return type === token;
148
+ });
149
+ }
150
+
151
+ function numberOrUndefined(value) {
152
+ if (value === undefined || value === null || value === '') {
153
+ return undefined;
154
+ }
155
+ const n = Number(value);
156
+ return Number.isFinite(n) ? n : undefined;
157
+ }
158
+ /** The server's reply to an upload, read as `{ serverId | id, url }`. */
159
+ function defaultResult(body) {
160
+ if (typeof body !== 'object' || body === null) {
161
+ return {};
162
+ }
163
+ const record = body;
164
+ const id = record['serverId'] ?? record['id'];
165
+ return {
166
+ serverId: id === undefined || id === null ? undefined : String(id),
167
+ url: typeof record['url'] === 'string' ? record['url'] : undefined,
168
+ };
169
+ }
170
+ let nextFileId = 0;
171
+ /**
172
+ * `nxe-file-input` is a file field for forms: the value is the list of files, `NxeFileDescriptor[]`, and it
173
+ * works with `formControlName`, `[formControl]`, `ngModel` and `nxe-form-field`.
174
+ *
175
+ * ```html
176
+ * <nxe-form-field label="Invoice and supporting documents">
177
+ * <nxe-file-input
178
+ * formControlName="attachments"
179
+ * accept=".pdf,image/*"
180
+ * [maxSize]="5 * 1024 * 1024"
181
+ * [maxFiles]="3"
182
+ * uploadUrl="/api/files/upload" />
183
+ * </nxe-form-field>
184
+ * ```
185
+ *
186
+ * **Value.** A file the user picks or drops becomes `{ id, name, size, type, status, progress, file }`. With no
187
+ * upload configured it stays `pending` and the form reads the `File` from the value (to put in a `FormData`
188
+ * itself). With one, it is uploaded as soon as it is added (`autoUpload`, default) and moves through
189
+ * `uploading` to `uploaded` with the server's `serverId` and `url`, or to `error`. A descriptor that came from the
190
+ * server (`writeValue` with `status: 'uploaded'`) shows as an existing attachment, a link when it has a `url`.
191
+ * The form is told at each status change; the progress bar between them is live without churning the form.
192
+ *
193
+ * **Validation.** The control is invalid, with these error keys (which `nxe-form-field` words in the active
194
+ * language), until the problem is removed:
195
+ *
196
+ * | key | when | error value |
197
+ * |---|---|---|
198
+ * | `fileType` | a file does not match `accept` | `{ accept, files }` |
199
+ * | `fileSize` | a file is over `maxSize` bytes | `{ maxSize, files }` |
200
+ * | `maxFiles` | more files than `maxFiles` (1 when not `multiple`) | `{ maxFiles, actual }` |
201
+ * | `uploading` | an upload is running, or a file is waiting for one | `{ count }` |
202
+ * | `uploadFailed` | an upload failed or was cancelled | `{ files }` |
203
+ *
204
+ * A file that breaks a rule is kept in the list, marked, so the user sees which one and can remove it; it is never
205
+ * uploaded. `required` needs a file in the list.
206
+ *
207
+ * **Uploading.** `uploadUrl` posts a `FormData` (field `uploadField`) with `HttpClient` and `reportProgress`, so the
208
+ * app needs `provideHttpClient()` (and its interceptors apply). `uploadFn` is the same job in your hands:
209
+ * `(file, { signal }) => Observable` of `HttpEvent`s, `{ progress, serverId?, url? }` or percentages, completing
210
+ * on success. Cancel and remove unsubscribe, and abort `signal`. `uploadResult` maps the response body to
211
+ * `{ serverId, url }` (default: `serverId` or `id`, and `url`).
212
+ */
213
+ class NxeFileInputComponent extends NxeFormControlBase {
214
+ /** Accepted files, as in `<input accept>`: `.pdf`, `application/pdf`, `image/*`, comma separated. Empty accepts all. */
215
+ accept = input('', /* @ts-ignore */
216
+ ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
217
+ /** Largest file, in bytes. */
218
+ maxSize = input(undefined, { ...(ngDevMode ? { debugName: "maxSize" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
219
+ /** Most files the control may hold. */
220
+ maxFiles = input(undefined, { ...(ngDevMode ? { debugName: "maxFiles" } : /* istanbul ignore next */ {}), transform: numberOrUndefined });
221
+ /** More than one file. When off, choosing a file replaces the current one. */
222
+ multiple = input(true, { ...(ngDevMode ? { debugName: "multiple" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
223
+ /** Uploads a file; see {@link NxeUploadFn}. Wins over `uploadUrl`. */
224
+ uploadFn = input(undefined, /* @ts-ignore */
225
+ ...(ngDevMode ? [{ debugName: "uploadFn" }] : /* istanbul ignore next */ []));
226
+ /** Where to POST each file, with `HttpClient`. */
227
+ uploadUrl = input(undefined, /* @ts-ignore */
228
+ ...(ngDevMode ? [{ debugName: "uploadUrl" }] : /* istanbul ignore next */ []));
229
+ /** Name of the form field the file is sent as. */
230
+ uploadField = input('file', /* @ts-ignore */
231
+ ...(ngDevMode ? [{ debugName: "uploadField" }] : /* istanbul ignore next */ []));
232
+ /** Headers for the `uploadUrl` request. */
233
+ uploadHeaders = input(undefined, /* @ts-ignore */
234
+ ...(ngDevMode ? [{ debugName: "uploadHeaders" }] : /* istanbul ignore next */ []));
235
+ /** Send cookies with the `uploadUrl` request. */
236
+ withCredentials = input(false, { ...(ngDevMode ? { debugName: "withCredentials" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
237
+ /** Reads `{ serverId, url }` from the response body. */
238
+ uploadResult = input(undefined, /* @ts-ignore */
239
+ ...(ngDevMode ? [{ debugName: "uploadResult" }] : /* istanbul ignore next */ []));
240
+ /** Start uploading as soon as a file is added. Off, call {@link upload}. */
241
+ autoUpload = input(true, { ...(ngDevMode ? { debugName: "autoUpload" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
242
+ /** The button's text; the active language's "Select files" when unset. */
243
+ selectButtonText = input(undefined, /* @ts-ignore */
244
+ ...(ngDevMode ? [{ debugName: "selectButtonText" }] : /* istanbul ignore next */ []));
245
+ /** Text beside the button; the active language's "or drop files here" when unset. */
246
+ labelText = input(undefined, /* @ts-ignore */
247
+ ...(ngDevMode ? [{ debugName: "labelText" }] : /* istanbul ignore next */ []));
248
+ /** Show the accepted types, size and count under the button. */
249
+ showHints = input(true, { ...(ngDevMode ? { debugName: "showHints" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
250
+ /** Files were added, with their descriptors. */
251
+ filesAdded = output();
252
+ /** A file was removed (or replaced). Delete it from the server here, if it was stored. */
253
+ fileRemoved = output();
254
+ /** An upload finished. */
255
+ fileUploaded = output();
256
+ /** An upload failed or was cancelled. */
257
+ fileFailed = output();
258
+ picker = viewChild.required('picker', /* @ts-ignore */
259
+ ...(ngDevMode ? [{ debugName: "picker" }] : /* istanbul ignore next */ []));
260
+ http = inject(HttpClient, { optional: true });
261
+ injector = inject(Injector);
262
+ destroyRef = inject(DestroyRef);
263
+ running = new Map();
264
+ results = new Map();
265
+ validatorChange = null;
266
+ announceToggle = false;
267
+ dragOver = signal(false, /* @ts-ignore */
268
+ ...(ngDevMode ? [{ debugName: "dragOver" }] : /* istanbul ignore next */ []));
269
+ announcement = signal('', /* @ts-ignore */
270
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
271
+ /** The files in the control. */
272
+ files = computed(() => this.value() ?? [], /* @ts-ignore */
273
+ ...(ngDevMode ? [{ debugName: "files" }] : /* istanbul ignore next */ []));
274
+ /** An upload is configured. */
275
+ uploads = computed(() => !!this.uploadFn() || !!this.uploadUrl(), /* @ts-ignore */
276
+ ...(ngDevMode ? [{ debugName: "uploads" }] : /* istanbul ignore next */ []));
277
+ /** Most files the control accepts: `maxFiles`, or one when not `multiple`. */
278
+ limit = computed(() => (this.multiple() ? (this.maxFiles() ?? Infinity) : 1), /* @ts-ignore */
279
+ ...(ngDevMode ? [{ debugName: "limit" }] : /* istanbul ignore next */ []));
280
+ canAdd = computed(() => !this.disabled() && !this.readOnly(), /* @ts-ignore */
281
+ ...(ngDevMode ? [{ debugName: "canAdd" }] : /* istanbul ignore next */ []));
282
+ rows = computed(() => this.files().map((file, index) => {
283
+ const issue = this.issueOf(file, index);
284
+ const failed = !issue && file.status === 'error';
285
+ return {
286
+ file,
287
+ issue,
288
+ sizeText: nxeFormatFileSize(file.size, this.nxeMessages),
289
+ statusText: this.statusText(file, issue?.message ?? null),
290
+ failed,
291
+ canRetry: failed && !!file.file && this.uploads(),
292
+ };
293
+ }), /* @ts-ignore */
294
+ ...(ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []));
295
+ hints = computed(() => {
296
+ const hints = [];
297
+ const accept = this.accept().trim();
298
+ if (accept && accept !== '*') {
299
+ hints.push(this.nxeMessages.t('uploader.acceptHint', accept.split(',').map((t) => t.trim()).join(', ')));
300
+ }
301
+ const size = this.maxSize();
302
+ if (size !== undefined) {
303
+ hints.push(this.nxeMessages.t('uploader.maxSizeHint', nxeFormatFileSize(size, this.nxeMessages)));
304
+ }
305
+ const limit = this.limit();
306
+ if (this.multiple() && Number.isFinite(limit)) {
307
+ hints.push(this.nxeMessages.t('uploader.maxFilesHint', limit));
308
+ }
309
+ return hints;
310
+ }, /* @ts-ignore */
311
+ ...(ngDevMode ? [{ debugName: "hints" }] : /* istanbul ignore next */ []));
312
+ constructor() {
313
+ super();
314
+ // The rules are inputs, so a change to one re-runs the control's validators.
315
+ toObservable(computed(() => [this.accept(), this.maxSize(), this.limit(), this.uploads(), this.autoUpload()].join('|')))
316
+ .pipe(takeUntilDestroyed())
317
+ .subscribe(() => this.validatorChange?.());
318
+ this.destroyRef.onDestroy(() => this.running.forEach((run) => this.stop(run)));
319
+ }
320
+ // --- Validator -------------------------------------------------------------------------
321
+ validate(control) {
322
+ const files = control.value ?? [];
323
+ if (files.length === 0) {
324
+ return null;
325
+ }
326
+ const errors = {};
327
+ const wrongType = files.filter((f) => !nxeFileAccepted(f, this.accept())).map((f) => f.name);
328
+ if (wrongType.length) {
329
+ errors['fileType'] = { accept: this.accept(), files: wrongType };
330
+ }
331
+ const maxSize = this.maxSize();
332
+ const tooBig = maxSize === undefined ? [] : files.filter((f) => f.size > maxSize).map((f) => f.name);
333
+ if (maxSize !== undefined && tooBig.length) {
334
+ errors['fileSize'] = { maxSize, files: tooBig };
335
+ }
336
+ if (files.length > this.limit()) {
337
+ errors['maxFiles'] = { maxFiles: this.limit(), actual: files.length };
338
+ }
339
+ const unfinished = files.filter((f, i) => f.status === 'uploading' || (f.status === 'pending' && this.uploads() && !this.issueOf(f, i))).length;
340
+ if (unfinished) {
341
+ errors['uploading'] = { count: unfinished };
342
+ }
343
+ const failed = files.filter((f) => f.status === 'error').map((f) => f.name);
344
+ if (failed.length) {
345
+ errors['uploadFailed'] = { files: failed };
346
+ }
347
+ return Object.keys(errors).length ? errors : null;
348
+ }
349
+ registerOnValidatorChange(fn) {
350
+ this.validatorChange = fn;
351
+ }
352
+ // --- Public API ------------------------------------------------------------------------
353
+ /** Opens the browser's file picker. */
354
+ open() {
355
+ if (this.canAdd()) {
356
+ this.picker().nativeElement.click();
357
+ }
358
+ }
359
+ /** Adds files as if the user had chosen them. */
360
+ addFiles(list, event = null) {
361
+ if (!this.canAdd() || list.length === 0) {
362
+ return;
363
+ }
364
+ const added = (this.multiple() ? list : list.slice(0, 1)).map((file) => this.describe(file));
365
+ // Not multiple: the new file replaces the old one, and anything the old one was doing stops.
366
+ const replaced = this.multiple() ? [] : this.files();
367
+ replaced.forEach((f) => this.stopById(f.id));
368
+ this.commitValue([...(this.multiple() ? this.files() : []), ...added], event);
369
+ replaced.forEach((f) => this.fileRemoved.emit(f));
370
+ this.afterAdd(added);
371
+ }
372
+ /** Removes a file, cancelling its upload if it is running. */
373
+ remove(id) {
374
+ const index = this.files().findIndex((f) => f.id === id);
375
+ if (index < 0 || !this.canAdd()) {
376
+ return;
377
+ }
378
+ const removed = this.files()[index];
379
+ this.stopById(id);
380
+ this.results.delete(id);
381
+ this.commitValue(this.files().filter((f) => f.id !== id));
382
+ this.fileRemoved.emit(removed);
383
+ this.say(this.nxeMessages.t('uploader.announceRemoved', removed.name));
384
+ this.markAsTouched();
385
+ this.focusAfterRemove(index);
386
+ // A file that was over the limit may fit now.
387
+ this.startEligible();
388
+ }
389
+ /** Stops a running upload. The file stays, as an error, so it can be retried or removed. */
390
+ cancel(id) {
391
+ const file = this.files().find((f) => f.id === id);
392
+ if (!file || file.status !== 'uploading') {
393
+ return;
394
+ }
395
+ this.stopById(id);
396
+ this.fail(id, this.nxeMessages.t('uploader.cancelled'));
397
+ }
398
+ /** Uploads a failed or cancelled file again. */
399
+ retry(id) {
400
+ const index = this.files().findIndex((f) => f.id === id);
401
+ const file = this.files()[index];
402
+ if (!file || file.status !== 'error' || !file.file || !this.uploads() || this.issueOf(file, index)) {
403
+ return;
404
+ }
405
+ this.patch(new Map([[id, { status: 'pending', progress: 0, error: undefined }]]), true);
406
+ this.begin([id]);
407
+ }
408
+ /** Uploads every file that is waiting, for `autoUpload` off. */
409
+ upload() {
410
+ this.startEligible(true);
411
+ }
412
+ // --- Template handlers -----------------------------------------------------------------
413
+ onPicked(event) {
414
+ const input = event.target;
415
+ this.addFiles(Array.from(input.files ?? []), event);
416
+ // Choosing the same file again must fire `change` again.
417
+ input.value = '';
418
+ }
419
+ onDragOver(event) {
420
+ if (!this.canAdd() || !Array.from(event.dataTransfer?.types ?? []).includes('Files')) {
421
+ return;
422
+ }
423
+ event.preventDefault();
424
+ if (event.dataTransfer) {
425
+ event.dataTransfer.dropEffect = 'copy';
426
+ }
427
+ this.dragOver.set(true);
428
+ }
429
+ onDragLeave(event) {
430
+ const next = event.relatedTarget;
431
+ if (!next || !this.elementRef.nativeElement.contains(next)) {
432
+ this.dragOver.set(false);
433
+ }
434
+ }
435
+ onDrop(event) {
436
+ if (!this.canAdd()) {
437
+ return;
438
+ }
439
+ event.preventDefault();
440
+ this.dragOver.set(false);
441
+ this.addFiles(Array.from(event.dataTransfer?.files ?? []), event);
442
+ }
443
+ onZoneFocusOut(event) {
444
+ const next = event.relatedTarget;
445
+ if (!next || !this.elementRef.nativeElement.contains(next)) {
446
+ this.markAsTouched();
447
+ }
448
+ }
449
+ progressLabel(file) {
450
+ return this.nxeMessages.t('uploader.uploadingFile', file.name);
451
+ }
452
+ round(value) {
453
+ return Math.round(value);
454
+ }
455
+ t(key, ...args) {
456
+ return this.nxeMessages.t(key, ...args);
457
+ }
458
+ // --- Rules -----------------------------------------------------------------------------
459
+ /** Why `file` at `index` cannot be kept; checked in the order type, size, count. */
460
+ issueOf(file, index) {
461
+ const messages = this.nxeMessages;
462
+ if (!nxeFileAccepted(file, this.accept())) {
463
+ return { kind: 'fileType', message: messages.t('uploader.rejectType', file.name) };
464
+ }
465
+ const maxSize = this.maxSize();
466
+ if (maxSize !== undefined && file.size > maxSize) {
467
+ return { kind: 'fileSize', message: messages.t('uploader.rejectSize', file.name, nxeFormatFileSize(maxSize, messages)) };
468
+ }
469
+ if (index >= this.limit()) {
470
+ return { kind: 'maxFiles', message: messages.t('uploader.rejectCount', file.name, this.limit()) };
471
+ }
472
+ return null;
473
+ }
474
+ statusText(file, issue) {
475
+ if (issue) {
476
+ return issue;
477
+ }
478
+ switch (file.status) {
479
+ case 'uploading':
480
+ return this.nxeMessages.t('uploader.statusUploading', Math.round(file.progress));
481
+ case 'uploaded':
482
+ return this.nxeMessages.t('uploader.statusUploaded');
483
+ case 'error':
484
+ return file.error || this.nxeMessages.t('uploader.statusFailed');
485
+ default:
486
+ return this.uploads() ? this.nxeMessages.t('uploader.statusPending') : '';
487
+ }
488
+ }
489
+ // --- Adding and uploading --------------------------------------------------------------
490
+ describe(file) {
491
+ return {
492
+ id: `nxe-file-${++nextFileId}`,
493
+ name: file.name,
494
+ size: file.size,
495
+ type: file.type,
496
+ status: 'pending',
497
+ progress: 0,
498
+ file,
499
+ };
500
+ }
501
+ afterAdd(added) {
502
+ this.filesAdded.emit(added);
503
+ this.say(this.nxeMessages.t('uploader.announceAdded', added.map((f) => f.name).join(', ')));
504
+ this.markAsTouched();
505
+ this.startEligible();
506
+ }
507
+ /** Starts every pending file that breaks no rule, when uploads are on (`force` ignores `autoUpload`). */
508
+ startEligible(force = false) {
509
+ if ((!force && !this.autoUpload()) || !this.uploads()) {
510
+ return;
511
+ }
512
+ const ids = this.files()
513
+ .filter((f, i) => f.status === 'pending' && !!f.file && !this.issueOf(f, i))
514
+ .map((f) => f.id);
515
+ if (ids.length) {
516
+ this.begin(ids);
517
+ }
518
+ }
519
+ /** Marks `ids` as uploading in one change, then starts each request. */
520
+ begin(ids) {
521
+ this.patch(new Map(ids.map((id) => [id, { status: 'uploading', progress: 0, error: undefined }])), true);
522
+ for (const id of ids) {
523
+ const descriptor = this.files().find((f) => f.id === id);
524
+ if (!descriptor?.file) {
525
+ continue;
526
+ }
527
+ const abort = new AbortController();
528
+ const subscription = new Subscription();
529
+ this.running.set(id, { abort, subscription });
530
+ subscription.add(this.request(descriptor, abort.signal).subscribe({
531
+ next: (event) => this.onEvent(id, event),
532
+ error: (error) => {
533
+ this.running.delete(id);
534
+ this.fail(id, this.messageOf(error));
535
+ },
536
+ complete: () => {
537
+ this.running.delete(id);
538
+ this.succeed(id);
539
+ },
540
+ }));
541
+ }
542
+ }
543
+ request(descriptor, signal) {
544
+ const file = descriptor.file;
545
+ const fn = this.uploadFn();
546
+ if (fn) {
547
+ return fn(file, { signal, descriptor });
548
+ }
549
+ const url = this.uploadUrl();
550
+ if (!this.http) {
551
+ return throwError(() => new Error('nxe-file-input needs provideHttpClient() to upload to an uploadUrl.'));
552
+ }
553
+ const body = new FormData();
554
+ body.append(this.uploadField(), file, file.name);
555
+ return this.http.post(url, body, {
556
+ reportProgress: true,
557
+ observe: 'events',
558
+ headers: this.uploadHeaders(),
559
+ withCredentials: this.withCredentials(),
560
+ });
561
+ }
562
+ onEvent(id, event) {
563
+ let progress;
564
+ if (typeof event === 'number') {
565
+ progress = event;
566
+ }
567
+ else if ('type' in event) {
568
+ if (event.type === HttpEventType.UploadProgress && event.total) {
569
+ progress = (event.loaded / event.total) * 100;
570
+ }
571
+ else if (event.type === HttpEventType.Response) {
572
+ this.results.set(id, (this.uploadResult() ?? defaultResult)(event.body));
573
+ progress = 100;
574
+ }
575
+ }
576
+ else {
577
+ progress = event.progress;
578
+ if (event.serverId !== undefined || event.url !== undefined) {
579
+ this.results.set(id, { serverId: event.serverId, url: event.url });
580
+ }
581
+ }
582
+ if (progress !== undefined) {
583
+ // Live, but not worth a valueChanges per tick: the form hears about the status changes.
584
+ this.patch(new Map([[id, { progress: Math.min(100, Math.max(0, progress)) }]]), false);
585
+ }
586
+ }
587
+ succeed(id) {
588
+ const current = this.files().find((f) => f.id === id);
589
+ if (!current || current.status !== 'uploading') {
590
+ return;
591
+ }
592
+ const result = this.results.get(id);
593
+ this.results.delete(id);
594
+ this.patch(new Map([[id, { status: 'uploaded', progress: 100, error: undefined, serverId: result?.serverId, url: result?.url }]]), true);
595
+ this.fileUploaded.emit(this.files().find((f) => f.id === id));
596
+ }
597
+ fail(id, message) {
598
+ const current = this.files().find((f) => f.id === id);
599
+ if (!current || (current.status !== 'uploading' && current.status !== 'pending')) {
600
+ return;
601
+ }
602
+ this.results.delete(id);
603
+ this.patch(new Map([[id, { status: 'error', error: message }]]), true);
604
+ const failed = this.files().find((f) => f.id === id);
605
+ this.say(`${failed.name}: ${message}`);
606
+ this.fileFailed.emit(failed);
607
+ }
608
+ messageOf(error) {
609
+ if (error instanceof HttpErrorResponse) {
610
+ const own = error.error?.message;
611
+ return typeof own === 'string' && own ? own : this.nxeMessages.t('uploader.uploadFailed');
612
+ }
613
+ if (error instanceof Error && error.message) {
614
+ return error.message;
615
+ }
616
+ return typeof error === 'string' && error ? error : this.nxeMessages.t('uploader.uploadFailed');
617
+ }
618
+ stopById(id) {
619
+ const run = this.running.get(id);
620
+ if (run) {
621
+ this.stop(run);
622
+ this.running.delete(id);
623
+ }
624
+ }
625
+ stop(run) {
626
+ run.abort.abort();
627
+ run.subscription.unsubscribe();
628
+ }
629
+ /** Applies changes to descriptors; `notify` tells the form, otherwise only the component's own value moves. */
630
+ patch(changes, notify) {
631
+ const next = this.files().map((f) => (changes.has(f.id) ? { ...f, ...changes.get(f.id) } : f));
632
+ if (notify) {
633
+ this.commitValue(next);
634
+ }
635
+ else {
636
+ this.value.set(next);
637
+ }
638
+ }
639
+ // --- Announcements and focus -----------------------------------------------------------
640
+ say(text) {
641
+ this.announceToggle = !this.announceToggle;
642
+ // The trailing no-break space makes a repeat of the same message a different string.
643
+ this.announcement.set(text + (this.announceToggle ? ' ' : ''));
644
+ }
645
+ /** After a removal the focused button is gone: move to the neighbouring file, or to the picker button. */
646
+ focusAfterRemove(index) {
647
+ afterNextRender(() => {
648
+ const host = this.elementRef.nativeElement;
649
+ const rows = Array.from(host.querySelectorAll('.nxe-fileinput-file'));
650
+ const target = rows[Math.min(index, rows.length - 1)]?.querySelector('button') ??
651
+ host.querySelector('.nxe-fileinput-button');
652
+ target?.focus();
653
+ }, { injector: this.injector });
654
+ }
655
+ // --- NxeFormControlBase ----------------------------------------------------------------
656
+ writeValue(value) {
657
+ // A new value from the form replaces the list: nothing from the old one should keep running.
658
+ this.running.forEach((run) => this.stop(run));
659
+ this.running.clear();
660
+ this.value.set(value);
661
+ }
662
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFileInputComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
663
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.5", type: NxeFileInputComponent, isStandalone: true, selector: "nxe-file-input", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, maxFiles: { classPropertyName: "maxFiles", publicName: "maxFiles", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, uploadFn: { classPropertyName: "uploadFn", publicName: "uploadFn", isSignal: true, isRequired: false, transformFunction: null }, uploadUrl: { classPropertyName: "uploadUrl", publicName: "uploadUrl", isSignal: true, isRequired: false, transformFunction: null }, uploadField: { classPropertyName: "uploadField", publicName: "uploadField", isSignal: true, isRequired: false, transformFunction: null }, uploadHeaders: { classPropertyName: "uploadHeaders", publicName: "uploadHeaders", isSignal: true, isRequired: false, transformFunction: null }, withCredentials: { classPropertyName: "withCredentials", publicName: "withCredentials", isSignal: true, isRequired: false, transformFunction: null }, uploadResult: { classPropertyName: "uploadResult", publicName: "uploadResult", isSignal: true, isRequired: false, transformFunction: null }, autoUpload: { classPropertyName: "autoUpload", publicName: "autoUpload", isSignal: true, isRequired: false, transformFunction: null }, selectButtonText: { classPropertyName: "selectButtonText", publicName: "selectButtonText", isSignal: true, isRequired: false, transformFunction: null }, labelText: { classPropertyName: "labelText", publicName: "labelText", isSignal: true, isRequired: false, transformFunction: null }, showHints: { classPropertyName: "showHints", publicName: "showHints", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { filesAdded: "filesAdded", fileRemoved: "fileRemoved", fileUploaded: "fileUploaded", fileFailed: "fileFailed" }, host: { properties: { "class.nxe-fileinput-dragover": "dragOver()", "class.nxe-fileinput-has-files": "rows().length > 0", "class.nxe-invalid": "invalidState()" }, classAttribute: "nxe-widget nxe-file-input" }, providers: [
664
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeFileInputComponent), multi: true },
665
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => NxeFileInputComponent), multi: true },
666
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeFileInputComponent) },
667
+ ], viewQueries: [{ propertyName: "picker", first: true, predicate: ["picker"], descendants: true, isSignal: true }], usesInheritance: true, ngImport: i0, template: "@if (canAdd() || disabled()) {\r\n <div\r\n class=\"nxe-fileinput-zone\"\r\n (dragenter)=\"onDragOver($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (focusout)=\"onZoneFocusOut($event)\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-button\"\r\n [id]=\"controlId()\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"open()\">\r\n {{ selectButtonText() ?? t('uploader.selectFiles') }}\r\n </button>\r\n <span class=\"nxe-fileinput-drop\">{{ labelText() ?? t('uploader.dropFiles') }}</span>\r\n <input\r\n #picker\r\n class=\"nxe-fileinput-picker\"\r\n type=\"file\"\r\n tabindex=\"-1\"\r\n aria-hidden=\"true\"\r\n [attr.accept]=\"accept() || null\"\r\n [multiple]=\"multiple()\"\r\n (change)=\"onPicked($event)\" />\r\n @if (showHints() && hints().length) {\r\n <span class=\"nxe-fileinput-hints\">{{ hints().join(' \u00B7 ') }}</span>\r\n }\r\n </div>\r\n} @else {\r\n <input #picker class=\"nxe-fileinput-picker\" type=\"file\" tabindex=\"-1\" aria-hidden=\"true\" />\r\n}\r\n\r\n@if (rows().length) {\r\n <ul class=\"nxe-fileinput-list\" [attr.aria-label]=\"t('uploader.listLabel')\">\r\n @for (row of rows(); track row.file.id) {\r\n <li\r\n class=\"nxe-fileinput-file nxe-fileinput-file-{{ row.file.status }}\"\r\n [class.nxe-fileinput-file-invalid]=\"!!row.issue || row.failed\">\r\n <div class=\"nxe-fileinput-file-main\">\r\n @if (row.file.status === 'uploaded' && row.file.url) {\r\n <a class=\"nxe-fileinput-file-name\" [href]=\"row.file.url\" target=\"_blank\" rel=\"noopener\">{{ row.file.name }}</a>\r\n } @else {\r\n <span class=\"nxe-fileinput-file-name\">{{ row.file.name }}</span>\r\n }\r\n <span class=\"nxe-fileinput-file-size\">{{ row.sizeText }}</span>\r\n @if (row.statusText) {\r\n <span class=\"nxe-fileinput-file-status\">{{ row.statusText }}</span>\r\n }\r\n </div>\r\n @if (row.file.status === 'uploading') {\r\n <div\r\n class=\"nxe-fileinput-progress\"\r\n role=\"progressbar\"\r\n aria-valuemin=\"0\"\r\n aria-valuemax=\"100\"\r\n [attr.aria-valuenow]=\"round(row.file.progress)\"\r\n [attr.aria-label]=\"progressLabel(row.file)\">\r\n <span class=\"nxe-fileinput-progress-bar\" [style.width.%]=\"row.file.progress\"></span>\r\n </div>\r\n }\r\n <div class=\"nxe-fileinput-file-actions\">\r\n @if (row.file.status === 'uploading') {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-action\"\r\n [attr.aria-label]=\"t('uploader.cancelFile', row.file.name)\"\r\n (click)=\"cancel(row.file.id)\">\r\n {{ t('uploader.cancel') }}\r\n </button>\r\n } @else {\r\n @if (row.canRetry) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-action\"\r\n [attr.aria-label]=\"t('uploader.retryFile', row.file.name)\"\r\n (click)=\"retry(row.file.id)\">\r\n {{ t('uploader.retry') }}\r\n </button>\r\n }\r\n @if (canAdd()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-action\"\r\n [attr.aria-label]=\"t('uploader.removeFile', row.file.name)\"\r\n (click)=\"remove(row.file.id)\">\r\n {{ t('uploader.remove') }}\r\n </button>\r\n }\r\n }\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n<div class=\"nxe-fileinput-live\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</div>\r\n", styles: ["nxe-file-input.nxe-file-input{display:block;color:var(--nxe-ui-text);font-family:var(--nxe-ui-font-family);font-size:var(--nxe-ui-font-size)}nxe-file-input.nxe-file-input .nxe-fileinput-picker{display:none}nxe-file-input.nxe-file-input .nxe-fileinput-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}nxe-file-input.nxe-file-input .nxe-fileinput-zone{display:flex;flex-wrap:wrap;align-items:center;gap:var(--nxe-ui-space-2) var(--nxe-ui-space-3);box-sizing:border-box;padding:var(--nxe-ui-space-3) var(--nxe-ui-space-4);border:1px dashed var(--nxe-ui-border);border-radius:var(--nxe-ui-radius-md);background:var(--nxe-ui-surface)}nxe-file-input.nxe-file-input.nxe-fileinput-dragover .nxe-fileinput-zone{border-color:var(--nxe-ui-accent);background:var(--nxe-ui-surface-alt)}nxe-file-input.nxe-file-input.nxe-invalid .nxe-fileinput-zone{border-color:var(--nxe-ui-invalid)}nxe-file-input.nxe-file-input.nxe-disabled .nxe-fileinput-zone{opacity:.5}nxe-file-input.nxe-file-input .nxe-fileinput-button{box-sizing:border-box;min-height:var(--nxe-ui-control-min-height);padding:0 var(--nxe-ui-space-3);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-radius-sm);background:var(--nxe-ui-surface);color:var(--nxe-ui-text);font:inherit;cursor:pointer}nxe-file-input.nxe-file-input .nxe-fileinput-button:hover:not(:disabled){background:var(--nxe-ui-surface-alt)}nxe-file-input.nxe-file-input .nxe-fileinput-button:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:1px}nxe-file-input.nxe-file-input .nxe-fileinput-button:disabled{cursor:not-allowed}nxe-file-input.nxe-file-input .nxe-fileinput-drop{color:var(--nxe-ui-text-muted)}nxe-file-input.nxe-file-input .nxe-fileinput-hints{flex-basis:100%;color:var(--nxe-ui-text-muted);font-size:var(--nxe-ui-font-size-xs)}nxe-file-input.nxe-file-input .nxe-fileinput-list{margin:var(--nxe-ui-space-2) 0 0;padding:0;list-style:none}nxe-file-input.nxe-file-input .nxe-fileinput-file{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--nxe-ui-space-1) var(--nxe-ui-space-3);padding:var(--nxe-ui-space-2) 0;border-bottom:1px solid var(--nxe-ui-border)}nxe-file-input.nxe-file-input .nxe-fileinput-file:last-child{border-bottom:0}nxe-file-input.nxe-file-input .nxe-fileinput-file-main{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 var(--nxe-ui-space-2);min-width:0}nxe-file-input.nxe-file-input .nxe-fileinput-file-name{overflow-wrap:anywhere;font-weight:var(--nxe-ui-font-weight-medium)}nxe-file-input.nxe-file-input .nxe-fileinput-file-size,nxe-file-input.nxe-file-input .nxe-fileinput-file-status{color:var(--nxe-ui-text-muted);font-size:var(--nxe-ui-font-size-xs)}nxe-file-input.nxe-file-input .nxe-fileinput-file-uploaded .nxe-fileinput-file-status{color:var(--nxe-ui-status-success-fg)}nxe-file-input.nxe-file-input .nxe-fileinput-file-invalid .nxe-fileinput-file-status,nxe-file-input.nxe-file-input .nxe-fileinput-file-error .nxe-fileinput-file-status{color:var(--nxe-ui-status-danger-fg)}nxe-file-input.nxe-file-input .nxe-fileinput-file-actions{display:flex;gap:var(--nxe-ui-space-1)}nxe-file-input.nxe-file-input .nxe-fileinput-action{padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:var(--nxe-ui-accent);font:inherit;font-size:var(--nxe-ui-font-size-sm);cursor:pointer}nxe-file-input.nxe-file-input .nxe-fileinput-action:hover{background:var(--nxe-ui-surface-alt)}nxe-file-input.nxe-file-input .nxe-fileinput-action:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:1px}nxe-file-input.nxe-file-input .nxe-fileinput-progress{grid-column:1/-1;height:var(--nxe-ui-space-1);overflow:hidden;border-radius:var(--nxe-ui-radius-pill);background:var(--nxe-ui-neutral-110)}nxe-file-input.nxe-file-input .nxe-fileinput-progress-bar{display:block;height:100%;background:var(--nxe-ui-accent);transition:width .15s linear}@media(prefers-reduced-motion:reduce){nxe-file-input.nxe-file-input .nxe-fileinput-progress-bar{transition:none}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
668
+ }
669
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.5", ngImport: i0, type: NxeFileInputComponent, decorators: [{
670
+ type: Component,
671
+ args: [{ selector: 'nxe-file-input', host: {
672
+ class: 'nxe-widget nxe-file-input',
673
+ '[class.nxe-fileinput-dragover]': 'dragOver()',
674
+ '[class.nxe-fileinput-has-files]': 'rows().length > 0',
675
+ '[class.nxe-invalid]': 'invalidState()',
676
+ }, providers: [
677
+ { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => NxeFileInputComponent), multi: true },
678
+ { provide: NG_VALIDATORS, useExisting: forwardRef(() => NxeFileInputComponent), multi: true },
679
+ { provide: NXE_VALIDATION_TARGET, useExisting: forwardRef(() => NxeFileInputComponent) },
680
+ ], changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.None, template: "@if (canAdd() || disabled()) {\r\n <div\r\n class=\"nxe-fileinput-zone\"\r\n (dragenter)=\"onDragOver($event)\"\r\n (dragover)=\"onDragOver($event)\"\r\n (dragleave)=\"onDragLeave($event)\"\r\n (drop)=\"onDrop($event)\"\r\n (focusout)=\"onZoneFocusOut($event)\">\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-button\"\r\n [id]=\"controlId()\"\r\n [disabled]=\"disabled()\"\r\n (click)=\"open()\">\r\n {{ selectButtonText() ?? t('uploader.selectFiles') }}\r\n </button>\r\n <span class=\"nxe-fileinput-drop\">{{ labelText() ?? t('uploader.dropFiles') }}</span>\r\n <input\r\n #picker\r\n class=\"nxe-fileinput-picker\"\r\n type=\"file\"\r\n tabindex=\"-1\"\r\n aria-hidden=\"true\"\r\n [attr.accept]=\"accept() || null\"\r\n [multiple]=\"multiple()\"\r\n (change)=\"onPicked($event)\" />\r\n @if (showHints() && hints().length) {\r\n <span class=\"nxe-fileinput-hints\">{{ hints().join(' \u00B7 ') }}</span>\r\n }\r\n </div>\r\n} @else {\r\n <input #picker class=\"nxe-fileinput-picker\" type=\"file\" tabindex=\"-1\" aria-hidden=\"true\" />\r\n}\r\n\r\n@if (rows().length) {\r\n <ul class=\"nxe-fileinput-list\" [attr.aria-label]=\"t('uploader.listLabel')\">\r\n @for (row of rows(); track row.file.id) {\r\n <li\r\n class=\"nxe-fileinput-file nxe-fileinput-file-{{ row.file.status }}\"\r\n [class.nxe-fileinput-file-invalid]=\"!!row.issue || row.failed\">\r\n <div class=\"nxe-fileinput-file-main\">\r\n @if (row.file.status === 'uploaded' && row.file.url) {\r\n <a class=\"nxe-fileinput-file-name\" [href]=\"row.file.url\" target=\"_blank\" rel=\"noopener\">{{ row.file.name }}</a>\r\n } @else {\r\n <span class=\"nxe-fileinput-file-name\">{{ row.file.name }}</span>\r\n }\r\n <span class=\"nxe-fileinput-file-size\">{{ row.sizeText }}</span>\r\n @if (row.statusText) {\r\n <span class=\"nxe-fileinput-file-status\">{{ row.statusText }}</span>\r\n }\r\n </div>\r\n @if (row.file.status === 'uploading') {\r\n <div\r\n class=\"nxe-fileinput-progress\"\r\n role=\"progressbar\"\r\n aria-valuemin=\"0\"\r\n aria-valuemax=\"100\"\r\n [attr.aria-valuenow]=\"round(row.file.progress)\"\r\n [attr.aria-label]=\"progressLabel(row.file)\">\r\n <span class=\"nxe-fileinput-progress-bar\" [style.width.%]=\"row.file.progress\"></span>\r\n </div>\r\n }\r\n <div class=\"nxe-fileinput-file-actions\">\r\n @if (row.file.status === 'uploading') {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-action\"\r\n [attr.aria-label]=\"t('uploader.cancelFile', row.file.name)\"\r\n (click)=\"cancel(row.file.id)\">\r\n {{ t('uploader.cancel') }}\r\n </button>\r\n } @else {\r\n @if (row.canRetry) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-action\"\r\n [attr.aria-label]=\"t('uploader.retryFile', row.file.name)\"\r\n (click)=\"retry(row.file.id)\">\r\n {{ t('uploader.retry') }}\r\n </button>\r\n }\r\n @if (canAdd()) {\r\n <button\r\n type=\"button\"\r\n class=\"nxe-fileinput-action\"\r\n [attr.aria-label]=\"t('uploader.removeFile', row.file.name)\"\r\n (click)=\"remove(row.file.id)\">\r\n {{ t('uploader.remove') }}\r\n </button>\r\n }\r\n }\r\n </div>\r\n </li>\r\n }\r\n </ul>\r\n}\r\n<div class=\"nxe-fileinput-live\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">{{ announcement() }}</div>\r\n", styles: ["nxe-file-input.nxe-file-input{display:block;color:var(--nxe-ui-text);font-family:var(--nxe-ui-font-family);font-size:var(--nxe-ui-font-size)}nxe-file-input.nxe-file-input .nxe-fileinput-picker{display:none}nxe-file-input.nxe-file-input .nxe-fileinput-live{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}nxe-file-input.nxe-file-input .nxe-fileinput-zone{display:flex;flex-wrap:wrap;align-items:center;gap:var(--nxe-ui-space-2) var(--nxe-ui-space-3);box-sizing:border-box;padding:var(--nxe-ui-space-3) var(--nxe-ui-space-4);border:1px dashed var(--nxe-ui-border);border-radius:var(--nxe-ui-radius-md);background:var(--nxe-ui-surface)}nxe-file-input.nxe-file-input.nxe-fileinput-dragover .nxe-fileinput-zone{border-color:var(--nxe-ui-accent);background:var(--nxe-ui-surface-alt)}nxe-file-input.nxe-file-input.nxe-invalid .nxe-fileinput-zone{border-color:var(--nxe-ui-invalid)}nxe-file-input.nxe-file-input.nxe-disabled .nxe-fileinput-zone{opacity:.5}nxe-file-input.nxe-file-input .nxe-fileinput-button{box-sizing:border-box;min-height:var(--nxe-ui-control-min-height);padding:0 var(--nxe-ui-space-3);border:1px solid var(--nxe-ui-border);border-radius:var(--nxe-ui-radius-sm);background:var(--nxe-ui-surface);color:var(--nxe-ui-text);font:inherit;cursor:pointer}nxe-file-input.nxe-file-input .nxe-fileinput-button:hover:not(:disabled){background:var(--nxe-ui-surface-alt)}nxe-file-input.nxe-file-input .nxe-fileinput-button:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:1px}nxe-file-input.nxe-file-input .nxe-fileinput-button:disabled{cursor:not-allowed}nxe-file-input.nxe-file-input .nxe-fileinput-drop{color:var(--nxe-ui-text-muted)}nxe-file-input.nxe-file-input .nxe-fileinput-hints{flex-basis:100%;color:var(--nxe-ui-text-muted);font-size:var(--nxe-ui-font-size-xs)}nxe-file-input.nxe-file-input .nxe-fileinput-list{margin:var(--nxe-ui-space-2) 0 0;padding:0;list-style:none}nxe-file-input.nxe-file-input .nxe-fileinput-file{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--nxe-ui-space-1) var(--nxe-ui-space-3);padding:var(--nxe-ui-space-2) 0;border-bottom:1px solid var(--nxe-ui-border)}nxe-file-input.nxe-file-input .nxe-fileinput-file:last-child{border-bottom:0}nxe-file-input.nxe-file-input .nxe-fileinput-file-main{display:flex;flex-wrap:wrap;align-items:baseline;gap:0 var(--nxe-ui-space-2);min-width:0}nxe-file-input.nxe-file-input .nxe-fileinput-file-name{overflow-wrap:anywhere;font-weight:var(--nxe-ui-font-weight-medium)}nxe-file-input.nxe-file-input .nxe-fileinput-file-size,nxe-file-input.nxe-file-input .nxe-fileinput-file-status{color:var(--nxe-ui-text-muted);font-size:var(--nxe-ui-font-size-xs)}nxe-file-input.nxe-file-input .nxe-fileinput-file-uploaded .nxe-fileinput-file-status{color:var(--nxe-ui-status-success-fg)}nxe-file-input.nxe-file-input .nxe-fileinput-file-invalid .nxe-fileinput-file-status,nxe-file-input.nxe-file-input .nxe-fileinput-file-error .nxe-fileinput-file-status{color:var(--nxe-ui-status-danger-fg)}nxe-file-input.nxe-file-input .nxe-fileinput-file-actions{display:flex;gap:var(--nxe-ui-space-1)}nxe-file-input.nxe-file-input .nxe-fileinput-action{padding:var(--nxe-ui-space-1) var(--nxe-ui-space-2);border:0;border-radius:var(--nxe-ui-radius-sm);background:transparent;color:var(--nxe-ui-accent);font:inherit;font-size:var(--nxe-ui-font-size-sm);cursor:pointer}nxe-file-input.nxe-file-input .nxe-fileinput-action:hover{background:var(--nxe-ui-surface-alt)}nxe-file-input.nxe-file-input .nxe-fileinput-action:focus-visible{outline:2px solid var(--nxe-ui-focus-ring-color);outline-offset:1px}nxe-file-input.nxe-file-input .nxe-fileinput-progress{grid-column:1/-1;height:var(--nxe-ui-space-1);overflow:hidden;border-radius:var(--nxe-ui-radius-pill);background:var(--nxe-ui-neutral-110)}nxe-file-input.nxe-file-input .nxe-fileinput-progress-bar{display:block;height:100%;background:var(--nxe-ui-accent);transition:width .15s linear}@media(prefers-reduced-motion:reduce){nxe-file-input.nxe-file-input .nxe-fileinput-progress-bar{transition:none}}\n"] }]
681
+ }], ctorParameters: () => [], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], maxFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxFiles", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], uploadFn: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadFn", required: false }] }], uploadUrl: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadUrl", required: false }] }], uploadField: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadField", required: false }] }], uploadHeaders: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadHeaders", required: false }] }], withCredentials: [{ type: i0.Input, args: [{ isSignal: true, alias: "withCredentials", required: false }] }], uploadResult: [{ type: i0.Input, args: [{ isSignal: true, alias: "uploadResult", required: false }] }], autoUpload: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoUpload", required: false }] }], selectButtonText: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectButtonText", required: false }] }], labelText: [{ type: i0.Input, args: [{ isSignal: true, alias: "labelText", required: false }] }], showHints: [{ type: i0.Input, args: [{ isSignal: true, alias: "showHints", required: false }] }], filesAdded: [{ type: i0.Output, args: ["filesAdded"] }], fileRemoved: [{ type: i0.Output, args: ["fileRemoved"] }], fileUploaded: [{ type: i0.Output, args: ["fileUploaded"] }], fileFailed: [{ type: i0.Output, args: ["fileFailed"] }], picker: [{ type: i0.ViewChild, args: ['picker', { isSignal: true }] }] } });
682
+
121
683
  // @nexterajs/ui/file-uploader — public API.
122
684
 
123
685
  /**
124
686
  * Generated bundle index. Do not edit.
125
687
  */
126
688
 
127
- export { NxeFileUploaderComponent };
689
+ export { NxeFileInputComponent, NxeFileUploaderComponent, nxeFileAccepted };
128
690
  //# sourceMappingURL=nexterajs-ui-file-uploader.mjs.map