@acorex/cdk 21.0.2-next.31 → 21.0.2-next.33

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 (74) hide show
  1. package/fesm2022/acorex-cdk-accordion.mjs +38 -38
  2. package/fesm2022/acorex-cdk-accordion.mjs.map +1 -1
  3. package/fesm2022/acorex-cdk-carousel.mjs +4 -4
  4. package/fesm2022/acorex-cdk-carousel.mjs.map +1 -1
  5. package/fesm2022/acorex-cdk-clipboard.mjs +8 -8
  6. package/fesm2022/acorex-cdk-clipboard.mjs.map +1 -1
  7. package/fesm2022/acorex-cdk-common.mjs +171 -116
  8. package/fesm2022/acorex-cdk-common.mjs.map +1 -1
  9. package/fesm2022/acorex-cdk-dom.mjs +4 -4
  10. package/fesm2022/acorex-cdk-dom.mjs.map +1 -1
  11. package/fesm2022/acorex-cdk-double-click.mjs +6 -6
  12. package/fesm2022/acorex-cdk-double-click.mjs.map +1 -1
  13. package/fesm2022/acorex-cdk-drag-drop.mjs +71 -71
  14. package/fesm2022/acorex-cdk-drag-drop.mjs.map +1 -1
  15. package/fesm2022/acorex-cdk-drawer.mjs +27 -27
  16. package/fesm2022/acorex-cdk-drawer.mjs.map +1 -1
  17. package/fesm2022/acorex-cdk-focus-trap.mjs +4 -4
  18. package/fesm2022/acorex-cdk-focus-trap.mjs.map +1 -1
  19. package/fesm2022/acorex-cdk-full-screen.mjs +5 -5
  20. package/fesm2022/acorex-cdk-full-screen.mjs.map +1 -1
  21. package/fesm2022/acorex-cdk-input-mask.mjs +20 -14
  22. package/fesm2022/acorex-cdk-input-mask.mjs.map +1 -1
  23. package/fesm2022/acorex-cdk-list-navigation.mjs +19 -19
  24. package/fesm2022/acorex-cdk-list-navigation.mjs.map +1 -1
  25. package/fesm2022/acorex-cdk-outline.mjs +79 -68
  26. package/fesm2022/acorex-cdk-outline.mjs.map +1 -1
  27. package/fesm2022/acorex-cdk-overlay.mjs +16 -3
  28. package/fesm2022/acorex-cdk-overlay.mjs.map +1 -1
  29. package/fesm2022/acorex-cdk-pan-view.mjs +22 -22
  30. package/fesm2022/acorex-cdk-pan-view.mjs.map +1 -1
  31. package/fesm2022/acorex-cdk-qrcode.mjs.map +1 -1
  32. package/fesm2022/acorex-cdk-resizable.mjs +14 -13
  33. package/fesm2022/acorex-cdk-resizable.mjs.map +1 -1
  34. package/fesm2022/acorex-cdk-selection.mjs +23 -23
  35. package/fesm2022/acorex-cdk-selection.mjs.map +1 -1
  36. package/fesm2022/acorex-cdk-sliding-item.mjs +11 -11
  37. package/fesm2022/acorex-cdk-sliding-item.mjs.map +1 -1
  38. package/fesm2022/acorex-cdk-sticky.mjs +3 -3
  39. package/fesm2022/acorex-cdk-sticky.mjs.map +1 -1
  40. package/fesm2022/acorex-cdk-uploader.mjs +298 -188
  41. package/fesm2022/acorex-cdk-uploader.mjs.map +1 -1
  42. package/fesm2022/acorex-cdk-virtual-scroll.mjs +18 -18
  43. package/fesm2022/acorex-cdk-virtual-scroll.mjs.map +1 -1
  44. package/fesm2022/acorex-cdk-wysiwyg.mjs +1 -1
  45. package/fesm2022/acorex-cdk-wysiwyg.mjs.map +1 -1
  46. package/fesm2022/acorex-cdk-z-index.mjs +4 -4
  47. package/fesm2022/acorex-cdk-z-index.mjs.map +1 -1
  48. package/fesm2022/acorex-cdk.mjs.map +1 -1
  49. package/package.json +32 -31
  50. package/{common/index.d.ts → types/acorex-cdk-common.d.ts} +9 -7
  51. package/{focus-trap/index.d.ts → types/acorex-cdk-focus-trap.d.ts} +1 -1
  52. package/{input-mask/index.d.ts → types/acorex-cdk-input-mask.d.ts} +1 -0
  53. package/{outline/index.d.ts → types/acorex-cdk-outline.d.ts} +1 -0
  54. package/{overlay/index.d.ts → types/acorex-cdk-overlay.d.ts} +1 -0
  55. package/{uploader/index.d.ts → types/acorex-cdk-uploader.d.ts} +120 -110
  56. /package/{accordion/index.d.ts → types/acorex-cdk-accordion.d.ts} +0 -0
  57. /package/{carousel/index.d.ts → types/acorex-cdk-carousel.d.ts} +0 -0
  58. /package/{clipboard/index.d.ts → types/acorex-cdk-clipboard.d.ts} +0 -0
  59. /package/{dom/index.d.ts → types/acorex-cdk-dom.d.ts} +0 -0
  60. /package/{double-click/index.d.ts → types/acorex-cdk-double-click.d.ts} +0 -0
  61. /package/{drag-drop/index.d.ts → types/acorex-cdk-drag-drop.d.ts} +0 -0
  62. /package/{drawer/index.d.ts → types/acorex-cdk-drawer.d.ts} +0 -0
  63. /package/{full-screen/index.d.ts → types/acorex-cdk-full-screen.d.ts} +0 -0
  64. /package/{list-navigation/index.d.ts → types/acorex-cdk-list-navigation.d.ts} +0 -0
  65. /package/{pan-view/index.d.ts → types/acorex-cdk-pan-view.d.ts} +0 -0
  66. /package/{qrcode/index.d.ts → types/acorex-cdk-qrcode.d.ts} +0 -0
  67. /package/{resizable/index.d.ts → types/acorex-cdk-resizable.d.ts} +0 -0
  68. /package/{selection/index.d.ts → types/acorex-cdk-selection.d.ts} +0 -0
  69. /package/{sliding-item/index.d.ts → types/acorex-cdk-sliding-item.d.ts} +0 -0
  70. /package/{sticky/index.d.ts → types/acorex-cdk-sticky.d.ts} +0 -0
  71. /package/{virtual-scroll/index.d.ts → types/acorex-cdk-virtual-scroll.d.ts} +0 -0
  72. /package/{wysiwyg/index.d.ts → types/acorex-cdk-wysiwyg.d.ts} +0 -0
  73. /package/{z-index/index.d.ts → types/acorex-cdk-z-index.d.ts} +0 -0
  74. /package/{index.d.ts → types/acorex-cdk.d.ts} +0 -0
@@ -7,6 +7,27 @@ import { AXFileService } from '@acorex/core/file';
7
7
  import { sumBy } from 'lodash-es';
8
8
  import { Subject, BehaviorSubject, map } from 'rxjs';
9
9
 
10
+ /** DOM marker so browse handles can find a zone across content projection. */
11
+ const AX_UPLOADER_ZONE_HOST = Symbol('AX_UPLOADER_ZONE_HOST');
12
+ function bindUploaderZoneHost(element, zone) {
13
+ element[AX_UPLOADER_ZONE_HOST] = zone;
14
+ }
15
+ function unbindUploaderZoneHost(element) {
16
+ delete element[AX_UPLOADER_ZONE_HOST];
17
+ }
18
+ /** Walk ancestors to resolve the zone (works with projected browse handles). */
19
+ function findUploaderZoneFromDom(start) {
20
+ let el = start;
21
+ while (el) {
22
+ const zone = el[AX_UPLOADER_ZONE_HOST];
23
+ if (zone) {
24
+ return zone;
25
+ }
26
+ el = el.parentElement;
27
+ }
28
+ return undefined;
29
+ }
30
+
10
31
  class AXUploadRequest {
11
32
  get name() {
12
33
  return this.file.name;
@@ -29,16 +50,16 @@ class AXUploadRequest {
29
50
  }
30
51
  constructor(uploadFile) {
31
52
  this.uploadFile = uploadFile;
32
- this._progress = signal(0, ...(ngDevMode ? [{ debugName: "_progress" }] : []));
33
- this.progress = computed(() => this._progress(), ...(ngDevMode ? [{ debugName: "progress" }] : []));
34
- this._estimateTime = signal(0, ...(ngDevMode ? [{ debugName: "_estimateTime" }] : []));
35
- this.estimateTime = computed(() => this._estimateTime(), ...(ngDevMode ? [{ debugName: "estimateTime" }] : []));
36
- this._status = signal('new', ...(ngDevMode ? [{ debugName: "_status" }] : []));
37
- this.status = computed(() => this._status(), ...(ngDevMode ? [{ debugName: "status" }] : []));
38
- this._message = signal(null, ...(ngDevMode ? [{ debugName: "_message" }] : []));
39
- this.message = computed(() => this._message(), ...(ngDevMode ? [{ debugName: "message" }] : []));
40
- this._isDetermined = signal(false, ...(ngDevMode ? [{ debugName: "_isDetermined" }] : []));
41
- this.isDetermined = computed(() => this._isDetermined(), ...(ngDevMode ? [{ debugName: "isDetermined" }] : []));
53
+ this._progress = signal(0, ...(ngDevMode ? [{ debugName: "_progress" }] : /* istanbul ignore next */ []));
54
+ this.progress = computed(() => this._progress(), ...(ngDevMode ? [{ debugName: "progress" }] : /* istanbul ignore next */ []));
55
+ this._estimateTime = signal(0, ...(ngDevMode ? [{ debugName: "_estimateTime" }] : /* istanbul ignore next */ []));
56
+ this.estimateTime = computed(() => this._estimateTime(), ...(ngDevMode ? [{ debugName: "estimateTime" }] : /* istanbul ignore next */ []));
57
+ this._status = signal('new', ...(ngDevMode ? [{ debugName: "_status" }] : /* istanbul ignore next */ []));
58
+ this.status = computed(() => this._status(), ...(ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []));
59
+ this._message = signal(null, ...(ngDevMode ? [{ debugName: "_message" }] : /* istanbul ignore next */ []));
60
+ this.message = computed(() => this._message(), ...(ngDevMode ? [{ debugName: "message" }] : /* istanbul ignore next */ []));
61
+ this._isDetermined = signal(false, ...(ngDevMode ? [{ debugName: "_isDetermined" }] : /* istanbul ignore next */ []));
62
+ this.isDetermined = computed(() => this._isDetermined(), ...(ngDevMode ? [{ debugName: "isDetermined" }] : /* istanbul ignore next */ []));
42
63
  this.bytesTransferred = 0;
43
64
  this.onCancel = new Subject();
44
65
  this.onStart = new Subject();
@@ -106,76 +127,169 @@ class AXUploadRequest {
106
127
  }
107
128
 
108
129
  /**
109
- * Service for managing file uploads with drag-and-drop support, progress tracking, and dialog management.
130
+ * File upload queue and validation via {@link AXFileService}.
131
+ * Storage is implemented by subscribing to upload/resolve/delete events.
110
132
  * @category Services
111
133
  */
112
134
  class AXUploaderService {
113
135
  constructor() {
114
- /**
115
- * Translation service for localized text.
116
- * @ignore
117
- */
118
- this.translateService = inject(AXTranslationService);
119
- /**
120
- * File service for file operations.
121
- * @ignore
122
- */
123
136
  this.fileService = inject(AXFileService);
124
- /**
125
- * Behavior subject for managing upload requests.
126
- * @ignore
127
- */
128
137
  this._files$ = new BehaviorSubject([]);
129
- /**
130
- * Gets the files behavior subject for observing upload requests.
131
- */
132
138
  this.files = this._files$.asObservable();
133
- /**
134
- * Subject for file upload start events.
135
- */
136
139
  this.onFileUploadStart = new Subject();
137
- /**
138
- * Subject for file upload complete events.
139
- */
140
140
  this.onFileUploadComplete = new Subject();
141
- /**
142
- * Subject for all files upload complete events.
143
- */
144
141
  this.onFilesUploadComplete = new Subject();
145
- /**
146
- * Subject for file upload canceled events.
147
- */
148
142
  this.onFileUploadCanceled = new Subject();
149
- /**
150
- * Signal indicating if any upload has determined progress.
151
- */
152
- this.isAnyDetermined = computed(() => this._files$.value.some((file) => file.isDetermined()), ...(ngDevMode ? [{ debugName: "isAnyDetermined" }] : []));
153
- /**
154
- * Observable for total estimated upload time.
155
- */
143
+ /** Subscribe to perform uploads (e.g. HTTP, IndexedDB). */
144
+ this.onUpload = new Subject();
145
+ /** Subscribe to resolve a stored reference to a playback URL. */
146
+ this.onResolveUrl = new Subject();
147
+ /** Subscribe to delete stored media. */
148
+ this.onDeleteMedia = new Subject();
156
149
  this.totalEstimateTime = this._files$.pipe(map((files) => sumBy(files, (file) => (file.status() === 'inprogress' ? file.estimateTime() : 0))));
157
150
  }
158
- /**
159
- * Converts a File object to an AXUploadRequest.
160
- * @private
161
- */
162
- convertFileToRequest(file) {
163
- return new AXUploadRequest(file);
151
+ isAnyDetermined() {
152
+ return this._files$.value.some((file) => file.isDetermined());
153
+ }
154
+ validateFiles(files, fileType) {
155
+ return this.fileService.validateMany(files, fileType);
156
+ }
157
+ async getAcceptAttribute(fileType) {
158
+ return this.fileService.getAcceptAttribute(fileType);
159
+ }
160
+ upload(options) {
161
+ return new Promise((resolve, reject) => {
162
+ if (!this.hasUploadHandler()) {
163
+ reject(new Error('No upload handler subscribed to AXUploaderService.onUpload. Provide a service that handles upload events.'));
164
+ return;
165
+ }
166
+ this.onUpload.next({
167
+ component: this,
168
+ options,
169
+ resolve,
170
+ reject,
171
+ isUserInteraction: false,
172
+ });
173
+ });
174
+ }
175
+ resolveUrl(reference, signal) {
176
+ return new Promise((resolve, reject) => {
177
+ if (!this.hasResolveUrlHandler()) {
178
+ reject(new Error('No handler subscribed to AXUploaderService.onResolveUrl. Provide a service that handles resolve events.'));
179
+ return;
180
+ }
181
+ this.onResolveUrl.next({
182
+ component: this,
183
+ reference,
184
+ signal,
185
+ resolve,
186
+ reject,
187
+ isUserInteraction: false,
188
+ });
189
+ });
190
+ }
191
+ async resolvePlaybackUrl(reference, signal) {
192
+ const direct = reference.url?.trim();
193
+ if (direct && !reference.mediaId) {
194
+ return direct;
195
+ }
196
+ if (direct && reference.mediaId) {
197
+ try {
198
+ return await this.resolveUrl(reference, signal);
199
+ }
200
+ catch {
201
+ return direct;
202
+ }
203
+ }
204
+ if (reference.mediaId) {
205
+ return this.resolveUrl(reference, signal);
206
+ }
207
+ if (direct) {
208
+ return direct;
209
+ }
210
+ throw new Error('Upload reference has no url or mediaId');
211
+ }
212
+ deleteMedia(reference) {
213
+ return new Promise((resolve, reject) => {
214
+ if (!this.hasDeleteMediaHandler()) {
215
+ reject(new Error('No handler subscribed to AXUploaderService.onDeleteMedia. Provide a service that handles delete events.'));
216
+ return;
217
+ }
218
+ this.onDeleteMedia.next({
219
+ component: this,
220
+ reference,
221
+ resolve,
222
+ reject,
223
+ isUserInteraction: false,
224
+ });
225
+ });
226
+ }
227
+ hasUploadHandler() {
228
+ return this.onUpload.observers.length > 0;
229
+ }
230
+ hasResolveUrlHandler() {
231
+ return this.onResolveUrl.observers.length > 0;
232
+ }
233
+ hasDeleteMediaHandler() {
234
+ return this.onDeleteMedia.observers.length > 0;
235
+ }
236
+ async add(files, options) {
237
+ const list = Array.from(files).map((f) => new AXUploadRequest(f));
238
+ await this.applyFileTypeValidation(list, options?.fileType);
239
+ const newFiles = [...this._files$.value, ...list];
240
+ this._files$.next(newFiles);
241
+ void this.startUpload();
242
+ return list;
164
243
  }
165
244
  /**
166
- * Starts uploading files that are in 'new' status.
167
- * @private
245
+ * Opens the file dialog and returns selected files without enqueueing uploads.
246
+ * With `fileType`, applies catalog validation; otherwise uses legacy accept-only selection.
168
247
  */
248
+ async chooseFiles(options = {}) {
249
+ try {
250
+ if (options.fileType) {
251
+ const accept = options.accept ?? (await this.fileService.getAcceptAttribute(options.fileType));
252
+ return this.fileService.chooseValidated({
253
+ fileType: options.fileType,
254
+ multiple: options.multiple ?? false,
255
+ accept,
256
+ });
257
+ }
258
+ const accepted = await this.fileService.choose({
259
+ accept: options.accept,
260
+ multiple: options.multiple ?? false,
261
+ });
262
+ return { accepted, rejected: [] };
263
+ }
264
+ catch (error) {
265
+ console.error('File choose failed:', error);
266
+ return { accepted: [], rejected: [] };
267
+ }
268
+ }
269
+ async browse(options = { multiple: false }) {
270
+ const { accepted } = await this.chooseFiles(options);
271
+ if (!accepted.length) {
272
+ return [];
273
+ }
274
+ return this.add(accepted, { fileType: options.fileType });
275
+ }
276
+ async cancelAll() {
277
+ await Promise.all(this._files$.value.filter((c) => c.status() !== 'completed').map((c) => c.cancel()));
278
+ }
279
+ clearAll() {
280
+ const remainingFiles = this._files$.value.filter((c) => c.status() === 'inprogress');
281
+ this._files$.next(remainingFiles);
282
+ }
283
+ remove(item) {
284
+ const updatedFiles = this._files$.value.filter((c) => c !== item);
285
+ this._files$.next(updatedFiles);
286
+ }
169
287
  async startUpload() {
170
288
  const newFiles = this._files$.value.filter((c) => c.status() === 'new');
171
289
  for (const file of newFiles) {
172
290
  await this.bindEvents(file);
173
291
  }
174
292
  }
175
- /**
176
- * Binds event handlers to an upload request.
177
- * @private
178
- */
179
293
  async bindEvents(c) {
180
294
  c.onStart.subscribe(() => {
181
295
  this.onFileUploadStart.next({
@@ -208,62 +322,22 @@ class AXUploaderService {
208
322
  });
209
323
  });
210
324
  }
211
- /**
212
- * Opens the file browser dialog and returns selected files as upload requests.
213
- * @param options - Configuration options for file selection
214
- * @returns Promise that resolves to an array of upload requests
215
- */
216
- async browse(options = { multiple: false }) {
217
- try {
218
- const files = await this.fileService.choose({ multiple: options?.multiple || false, accept: options.accept });
219
- if (files.length) {
220
- return this.add(files);
221
- }
222
- return [];
325
+ async applyFileTypeValidation(requests, fileType) {
326
+ if (!fileType) {
327
+ return requests;
223
328
  }
224
- catch (error) {
225
- // It's good practice to log the error.
226
- console.error('File browse failed:', error);
227
- return [];
329
+ for (const request of requests) {
330
+ const errors = await this.fileService.validate(request.file, fileType);
331
+ if (errors.length > 0) {
332
+ request.error(errors[0]?.message ?? 'Invalid file');
333
+ }
228
334
  }
335
+ return requests;
229
336
  }
230
- /**
231
- * Adds files to the upload queue and starts the upload process.
232
- * @param files - Files to add to the upload queue
233
- * @returns Promise that resolves to an array of upload requests
234
- */
235
- async add(files) {
236
- const list = Array.from(files).map((f) => this.convertFileToRequest(f));
237
- const newFiles = [...this._files$.value, ...list];
238
- this._files$.next(newFiles);
239
- this.startUpload();
240
- return list;
241
- }
242
- /**
243
- * Cancels all pending and in-progress uploads.
244
- */
245
- async cancelAll() {
246
- await Promise.all(this._files$.value.filter((c) => c.status() !== 'completed').map((c) => c.cancel()));
247
- }
248
- /**
249
- * Clears all completed uploads from the queue.
250
- */
251
- clearAll() {
252
- const remainingFiles = this._files$.value.filter((c) => c.status() === 'inprogress');
253
- this._files$.next(remainingFiles);
254
- }
255
- /**
256
- * Removes a specific upload request from the queue.
257
- * @param item - The upload request to remove
258
- */
259
- remove(item) {
260
- const updatedFiles = this._files$.value.filter((c) => c !== item);
261
- this._files$.next(updatedFiles);
262
- }
263
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
264
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderService, providedIn: 'root' }); }
337
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
338
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderService, providedIn: 'root' }); }
265
339
  }
266
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderService, decorators: [{
340
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderService, decorators: [{
267
341
  type: Injectable,
268
342
  args: [{ providedIn: 'root' }]
269
343
  }] });
@@ -292,26 +366,31 @@ class AXUploaderZoneDirective {
292
366
  * Whether multiple files can be selected.
293
367
  * @defaultValue true
294
368
  */
295
- this.multiple = input(true, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
369
+ this.multiple = input(true, ...(ngDevMode ? [{ debugName: "multiple" }] : /* istanbul ignore next */ []));
296
370
  /**
297
371
  * File types that are accepted for upload.
298
372
  * @defaultValue null
299
373
  */
300
- this.accept = input(null, ...(ngDevMode ? [{ debugName: "accept" }] : []));
374
+ this.accept = input(null, ...(ngDevMode ? [{ debugName: "accept" }] : /* istanbul ignore next */ []));
375
+ /**
376
+ * Logical file type name from {@link AXFileService} (e.g. `conversation-image`).
377
+ * Required for browse handles and validated selection.
378
+ */
379
+ this.fileType = input(null, ...(ngDevMode ? [{ debugName: "fileType" }] : /* istanbul ignore next */ []));
301
380
  /**
302
381
  * Custom template for the drag overlay. If provided, this will be used instead of the default overlay.
303
382
  */
304
- this.overlayTemplate = input(...(ngDevMode ? [undefined, { debugName: "overlayTemplate" }] : []));
383
+ this.overlayTemplate = input(...(ngDevMode ? [undefined, { debugName: "overlayTemplate" }] : /* istanbul ignore next */ []));
305
384
  /**
306
385
  * Whether browsing files by clicking on the container is disabled.
307
386
  * @defaultValue false
308
387
  */
309
- this.disableBrowse = input(false, ...(ngDevMode ? [{ debugName: "disableBrowse" }] : []));
388
+ this.disableBrowse = input(false, ...(ngDevMode ? [{ debugName: "disableBrowse" }] : /* istanbul ignore next */ []));
310
389
  /**
311
390
  * Whether drag and drop functionality is disabled.
312
391
  * @defaultValue false
313
392
  */
314
- this.disableDragDrop = input(false, ...(ngDevMode ? [{ debugName: "disableDragDrop" }] : []));
393
+ this.disableDragDrop = input(false, ...(ngDevMode ? [{ debugName: "disableDragDrop" }] : /* istanbul ignore next */ []));
315
394
  /**
316
395
  * Change detector reference.
317
396
  * @ignore
@@ -391,13 +470,13 @@ class AXUploaderZoneDirective {
391
470
  * Flag to track if dragOver has been emitted for the current drag session.
392
471
  * @ignore
393
472
  */
394
- this.dragOverEmitted = signal(false, ...(ngDevMode ? [{ debugName: "dragOverEmitted" }] : []));
473
+ this.dragOverEmitted = signal(false, ...(ngDevMode ? [{ debugName: "dragOverEmitted" }] : /* istanbul ignore next */ []));
395
474
  /**
396
475
  * Counter to track drag enter/leave depth to prevent false dragleave events.
397
476
  * When moving from parent to child, dragleave fires even though we're still inside.
398
477
  * @ignore
399
478
  */
400
- this.dragDepth = signal(0, ...(ngDevMode ? [{ debugName: "dragDepth" }] : []));
479
+ this.dragDepth = signal(0, ...(ngDevMode ? [{ debugName: "dragDepth" }] : /* istanbul ignore next */ []));
401
480
  /**
402
481
  * Animation end handler for cleanup.
403
482
  * @ignore
@@ -405,6 +484,7 @@ class AXUploaderZoneDirective {
405
484
  this.animationEndHandler = null;
406
485
  this.element = this.elementRef.nativeElement;
407
486
  this.element.style.position = 'relative';
487
+ bindUploaderZoneHost(this.element, this);
408
488
  //
409
489
  this.uploadService.onFileUploadComplete.pipe(this.unsubscriber.takeUntilDestroy).subscribe((c) => {
410
490
  this.onFileUploadComplete.emit(c);
@@ -430,6 +510,7 @@ class AXUploaderZoneDirective {
430
510
  * Cleans up event listeners when the directive is destroyed.
431
511
  */
432
512
  ngOnDestroy() {
513
+ unbindUploaderZoneHost(this.element);
433
514
  this.element.removeEventListener('click', this.browser.bind(this));
434
515
  this.element.removeEventListener('dragenter', this.handleDragEnter.bind(this));
435
516
  this.element.removeEventListener('drop', this.handleOnDrop.bind(this));
@@ -467,16 +548,25 @@ class AXUploaderZoneDirective {
467
548
  // Reset drag depth on drop
468
549
  this.dragDepth.set(0);
469
550
  if (event.dataTransfer?.files && event.dataTransfer.files.length > 0) {
470
- const files = Array.from(event.dataTransfer.files);
471
- // Emit simple file change event (like HTML input)
472
- this.fileChange.emit({ event, files });
473
- // Also emit the old onChanged event for backward compatibility
474
- const requests = await this.uploadService.add(event.dataTransfer.files);
475
- this.onChanged.emit({
476
- component: this,
477
- requests,
478
- isUserInteraction: true,
479
- });
551
+ let files = Array.from(event.dataTransfer.files);
552
+ let rejected;
553
+ const catalog = this.fileType();
554
+ if (catalog) {
555
+ const result = await this.uploadService.validateFiles(files, catalog);
556
+ files = result.accepted;
557
+ rejected = result.rejected;
558
+ }
559
+ this.fileChange.emit({ event, files, rejected });
560
+ if (!this.disableBrowse()) {
561
+ const requests = await this.uploadService.add(files, { fileType: catalog ?? undefined });
562
+ if (requests.length > 0) {
563
+ this.onChanged.emit({
564
+ component: this,
565
+ requests,
566
+ isUserInteraction: true,
567
+ });
568
+ }
569
+ }
480
570
  }
481
571
  this.removeZone();
482
572
  this.cdr.detectChanges();
@@ -640,32 +730,60 @@ class AXUploaderZoneDirective {
640
730
  * @returns Promise that resolves when files are processed
641
731
  */
642
732
  async browser() {
643
- if (this.disableBrowse())
644
- return;
645
- const files = await this.uploadService.browse({ accept: this.accept() ?? undefined, multiple: this.multiple() });
646
- if (files.length > 0) {
647
- // Create a synthetic event for consistency
648
- const syntheticEvent = new Event('change');
649
- const target = Object.create(EventTarget.prototype);
650
- target.files = files.map((r) => r.file);
651
- Object.defineProperty(syntheticEvent, 'target', { value: target, writable: false });
652
- // Emit simple file change event (like HTML input)
653
- this.fileChange.emit({
654
- event: syntheticEvent,
655
- files: files.map((r) => r.file),
733
+ const catalog = this.fileType();
734
+ let selected = [];
735
+ let rejected;
736
+ if (catalog) {
737
+ const accept = this.accept() ?? (await this.uploadService.getAcceptAttribute(catalog));
738
+ const result = await this.uploadService.chooseFiles({
739
+ fileType: catalog,
740
+ multiple: this.multiple(),
741
+ accept,
742
+ });
743
+ selected = result.accepted;
744
+ rejected = result.rejected;
745
+ }
746
+ else if (!this.disableBrowse()) {
747
+ const requests = await this.uploadService.browse({
748
+ accept: this.accept() ?? undefined,
749
+ multiple: this.multiple(),
656
750
  });
657
- // Also emit the old onChanged event for backward compatibility
751
+ selected = requests.map((r) => r.file);
752
+ }
753
+ else {
754
+ console.warn('AXUploaderZone.browser: fileType is required when disableBrowse is enabled (use axUploaderBrowseHandle).');
755
+ return;
756
+ }
757
+ if (selected.length === 0 && !(rejected?.length ?? 0)) {
758
+ return;
759
+ }
760
+ const syntheticEvent = new Event('change');
761
+ const target = Object.create(EventTarget.prototype);
762
+ target.files = selected;
763
+ Object.defineProperty(syntheticEvent, 'target', { value: target, writable: false });
764
+ this.fileChange.emit({
765
+ event: syntheticEvent,
766
+ files: selected,
767
+ rejected,
768
+ });
769
+ if (this.disableBrowse()) {
770
+ return;
771
+ }
772
+ const requests = catalog
773
+ ? await this.uploadService.add(selected, { fileType: catalog })
774
+ : await this.uploadService.add(selected);
775
+ if (requests.length > 0) {
658
776
  this.onChanged.emit({
659
777
  component: this,
660
- requests: files,
778
+ requests,
661
779
  isUserInteraction: true,
662
780
  });
663
781
  }
664
782
  }
665
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderZoneDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
666
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.3", type: AXUploaderZoneDirective, isStandalone: true, selector: "[axUploaderZone]", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, overlayTemplate: { classPropertyName: "overlayTemplate", publicName: "overlayTemplate", isSignal: true, isRequired: false, transformFunction: null }, disableBrowse: { classPropertyName: "disableBrowse", publicName: "disableBrowse", isSignal: true, isRequired: false, transformFunction: null }, disableDragDrop: { classPropertyName: "disableDragDrop", publicName: "disableDragDrop", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileChange: "fileChange", onChanged: "onChanged", dragEnter: "dragEnter", dragLeave: "dragLeave", dragOver: "dragOver", onFileUploadComplete: "onFileUploadComplete", onFilesUploadComplete: "onFilesUploadComplete" }, host: { classAttribute: "ax-drop-zone" }, providers: [AXUnsubscriber], ngImport: i0 }); }
783
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderZoneDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
784
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.9", type: AXUploaderZoneDirective, isStandalone: true, selector: "[axUploaderZone]", inputs: { multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, fileType: { classPropertyName: "fileType", publicName: "fileType", isSignal: true, isRequired: false, transformFunction: null }, overlayTemplate: { classPropertyName: "overlayTemplate", publicName: "overlayTemplate", isSignal: true, isRequired: false, transformFunction: null }, disableBrowse: { classPropertyName: "disableBrowse", publicName: "disableBrowse", isSignal: true, isRequired: false, transformFunction: null }, disableDragDrop: { classPropertyName: "disableDragDrop", publicName: "disableDragDrop", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { fileChange: "fileChange", onChanged: "onChanged", dragEnter: "dragEnter", dragLeave: "dragLeave", dragOver: "dragOver", onFileUploadComplete: "onFileUploadComplete", onFilesUploadComplete: "onFilesUploadComplete" }, host: { classAttribute: "ax-drop-zone" }, providers: [AXUnsubscriber], ngImport: i0 }); }
667
785
  }
668
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderZoneDirective, decorators: [{
786
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderZoneDirective, decorators: [{
669
787
  type: Directive,
670
788
  args: [{
671
789
  selector: '[axUploaderZone]',
@@ -674,66 +792,58 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImpor
674
792
  class: 'ax-drop-zone',
675
793
  },
676
794
  }]
677
- }], ctorParameters: () => [] });
795
+ }], ctorParameters: () => [], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], fileType: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileType", required: false }] }], overlayTemplate: [{ type: i0.Input, args: [{ isSignal: true, alias: "overlayTemplate", required: false }] }], disableBrowse: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableBrowse", required: false }] }], disableDragDrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "disableDragDrop", required: false }] }], fileChange: [{ type: i0.Output, args: ["fileChange"] }], onChanged: [{ type: i0.Output, args: ["onChanged"] }], dragEnter: [{ type: i0.Output, args: ["dragEnter"] }], dragLeave: [{ type: i0.Output, args: ["dragLeave"] }], dragOver: [{ type: i0.Output, args: ["dragOver"] }], onFileUploadComplete: [{ type: i0.Output, args: ["onFileUploadComplete"] }], onFilesUploadComplete: [{ type: i0.Output, args: ["onFilesUploadComplete"] }] } });
678
796
 
679
797
  /**
680
- * A directive that provides browse functionality for file uploads.
681
- * When applied to an element, clicking it will trigger the file browser dialog.
798
+ * Triggers the nearest {@link AXUploaderZoneDirective} file dialog on click.
799
+ * Resolves the zone via DI when possible, otherwise walks the DOM (supports content projection).
682
800
  * @category Directives
683
801
  */
684
802
  class AXUploaderBrowseDirective {
685
803
  constructor() {
686
- /**
687
- * The uploader zone directive instance.
688
- * @ignore
689
- */
690
- this.uploaderZone = inject(AXUploaderZoneDirective);
691
- /**
692
- * The element reference for the directive host.
693
- * @ignore
694
- */
695
- this.elementRef = inject((ElementRef));
696
- /**
697
- * Platform ID for browser detection.
698
- * @ignore
699
- */
804
+ this.elementRef = inject(ElementRef);
700
805
  this.platformID = inject(PLATFORM_ID);
806
+ /** When browse + zone share the same host element. */
807
+ this.zoneFromInjector = inject(AXUploaderZoneDirective, { optional: true, self: true });
808
+ this.onClick = () => {
809
+ void this.handleClick();
810
+ };
701
811
  }
702
- /**
703
- * Initializes the directive by adding click event listener and data attribute.
704
- */
705
812
  ngOnInit() {
706
- if (isPlatformBrowser(this.platformID) && this.elementRef?.nativeElement) {
707
- const element = this.elementRef.nativeElement;
708
- element.addEventListener('click', this.handleClick.bind(this));
709
- // Use setAttribute for SSR compatibility
710
- if (element.dataset) {
711
- element.dataset['axUploaderBrowseHandle'] = 'true';
712
- }
713
- else {
714
- element.setAttribute('data-ax-uploader-browse-handle', 'true');
715
- }
813
+ if (!isPlatformBrowser(this.platformID)) {
814
+ return;
815
+ }
816
+ const element = this.elementRef.nativeElement;
817
+ element.addEventListener('click', this.onClick);
818
+ if (element.dataset) {
819
+ element.dataset['axUploaderBrowseHandle'] = 'true';
820
+ }
821
+ else {
822
+ element.setAttribute('data-ax-uploader-browse-handle', 'true');
716
823
  }
717
824
  }
718
- /**
719
- * Cleans up the directive by removing event listeners.
720
- */
721
825
  ngOnDestroy() {
722
- if (isPlatformBrowser(this.platformID) && this.elementRef.nativeElement) {
723
- this.elementRef.nativeElement.removeEventListener('click', this.handleClick.bind(this));
826
+ if (isPlatformBrowser(this.platformID)) {
827
+ this.elementRef.nativeElement.removeEventListener('click', this.onClick);
724
828
  }
725
829
  }
726
- /**
727
- * Handles the click event to trigger file browser.
728
- * @private
729
- */
830
+ resolveZone() {
831
+ return (this.zoneFromInjector ??
832
+ findUploaderZoneFromDom(this.elementRef.nativeElement) ??
833
+ undefined);
834
+ }
730
835
  async handleClick() {
731
- await this.uploaderZone.browser();
836
+ const zone = this.resolveZone();
837
+ if (!zone) {
838
+ console.warn('[axUploaderBrowseHandle] No axUploaderZone found. Place axUploaderBrowseHandle inside an axUploaderZone.');
839
+ return;
840
+ }
841
+ await zone.browser();
732
842
  }
733
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderBrowseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
734
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.3", type: AXUploaderBrowseDirective, isStandalone: true, selector: "[axUploaderBrowseHandle]", ngImport: i0 }); }
843
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderBrowseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
844
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.9", type: AXUploaderBrowseDirective, isStandalone: true, selector: "[axUploaderBrowseHandle]", ngImport: i0 }); }
735
845
  }
736
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderBrowseDirective, decorators: [{
846
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.9", ngImport: i0, type: AXUploaderBrowseDirective, decorators: [{
737
847
  type: Directive,
738
848
  args: [{ selector: '[axUploaderBrowseHandle]' }]
739
849
  }] });