@acorex/cdk 21.0.2-next.22 → 21.0.2-next.23

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 (73) hide show
  1. package/{types/acorex-cdk-common.d.ts → common/index.d.ts} +7 -9
  2. package/fesm2022/acorex-cdk-accordion.mjs +24 -24
  3. package/fesm2022/acorex-cdk-accordion.mjs.map +1 -1
  4. package/fesm2022/acorex-cdk-carousel.mjs +3 -3
  5. package/fesm2022/acorex-cdk-carousel.mjs.map +1 -1
  6. package/fesm2022/acorex-cdk-clipboard.mjs +8 -8
  7. package/fesm2022/acorex-cdk-clipboard.mjs.map +1 -1
  8. package/fesm2022/acorex-cdk-common.mjs +106 -161
  9. package/fesm2022/acorex-cdk-common.mjs.map +1 -1
  10. package/fesm2022/acorex-cdk-dom.mjs +4 -4
  11. package/fesm2022/acorex-cdk-dom.mjs.map +1 -1
  12. package/fesm2022/acorex-cdk-double-click.mjs +4 -4
  13. package/fesm2022/acorex-cdk-double-click.mjs.map +1 -1
  14. package/fesm2022/acorex-cdk-drag-drop.mjs +22 -22
  15. package/fesm2022/acorex-cdk-drag-drop.mjs.map +1 -1
  16. package/fesm2022/acorex-cdk-drawer.mjs +13 -13
  17. package/fesm2022/acorex-cdk-drawer.mjs.map +1 -1
  18. package/fesm2022/acorex-cdk-focus-trap.mjs +3 -3
  19. package/fesm2022/acorex-cdk-focus-trap.mjs.map +1 -1
  20. package/fesm2022/acorex-cdk-full-screen.mjs +4 -4
  21. package/fesm2022/acorex-cdk-full-screen.mjs.map +1 -1
  22. package/fesm2022/acorex-cdk-input-mask.mjs +5 -11
  23. package/fesm2022/acorex-cdk-input-mask.mjs.map +1 -1
  24. package/fesm2022/acorex-cdk-list-navigation.mjs +13 -13
  25. package/fesm2022/acorex-cdk-list-navigation.mjs.map +1 -1
  26. package/fesm2022/acorex-cdk-outline.mjs +57 -68
  27. package/fesm2022/acorex-cdk-outline.mjs.map +1 -1
  28. package/fesm2022/acorex-cdk-overlay.mjs +3 -16
  29. package/fesm2022/acorex-cdk-overlay.mjs.map +1 -1
  30. package/fesm2022/acorex-cdk-pan-view.mjs +4 -4
  31. package/fesm2022/acorex-cdk-pan-view.mjs.map +1 -1
  32. package/fesm2022/acorex-cdk-qrcode.mjs.map +1 -1
  33. package/fesm2022/acorex-cdk-resizable.mjs +37 -14
  34. package/fesm2022/acorex-cdk-resizable.mjs.map +1 -1
  35. package/fesm2022/acorex-cdk-selection.mjs +13 -13
  36. package/fesm2022/acorex-cdk-selection.mjs.map +1 -1
  37. package/fesm2022/acorex-cdk-sliding-item.mjs +3 -3
  38. package/fesm2022/acorex-cdk-sliding-item.mjs.map +1 -1
  39. package/fesm2022/acorex-cdk-sticky.mjs +3 -3
  40. package/fesm2022/acorex-cdk-sticky.mjs.map +1 -1
  41. package/fesm2022/acorex-cdk-uploader.mjs +277 -171
  42. package/fesm2022/acorex-cdk-uploader.mjs.map +1 -1
  43. package/fesm2022/acorex-cdk-virtual-scroll.mjs +11 -11
  44. package/fesm2022/acorex-cdk-virtual-scroll.mjs.map +1 -1
  45. package/fesm2022/acorex-cdk-wysiwyg.mjs.map +1 -1
  46. package/fesm2022/acorex-cdk-z-index.mjs +3 -3
  47. package/fesm2022/acorex-cdk-z-index.mjs.map +1 -1
  48. package/fesm2022/acorex-cdk.mjs.map +1 -1
  49. package/{types/acorex-cdk-focus-trap.d.ts → focus-trap/index.d.ts} +1 -1
  50. package/{types/acorex-cdk-input-mask.d.ts → input-mask/index.d.ts} +0 -1
  51. package/{types/acorex-cdk-outline.d.ts → outline/index.d.ts} +0 -1
  52. package/{types/acorex-cdk-overlay.d.ts → overlay/index.d.ts} +0 -1
  53. package/package.json +31 -31
  54. package/{types/acorex-cdk-resizable.d.ts → resizable/index.d.ts} +2 -0
  55. package/{types/acorex-cdk-uploader.d.ts → uploader/index.d.ts} +119 -110
  56. /package/{types/acorex-cdk-accordion.d.ts → accordion/index.d.ts} +0 -0
  57. /package/{types/acorex-cdk-carousel.d.ts → carousel/index.d.ts} +0 -0
  58. /package/{types/acorex-cdk-clipboard.d.ts → clipboard/index.d.ts} +0 -0
  59. /package/{types/acorex-cdk-dom.d.ts → dom/index.d.ts} +0 -0
  60. /package/{types/acorex-cdk-double-click.d.ts → double-click/index.d.ts} +0 -0
  61. /package/{types/acorex-cdk-drag-drop.d.ts → drag-drop/index.d.ts} +0 -0
  62. /package/{types/acorex-cdk-drawer.d.ts → drawer/index.d.ts} +0 -0
  63. /package/{types/acorex-cdk-full-screen.d.ts → full-screen/index.d.ts} +0 -0
  64. /package/{types/acorex-cdk.d.ts → index.d.ts} +0 -0
  65. /package/{types/acorex-cdk-list-navigation.d.ts → list-navigation/index.d.ts} +0 -0
  66. /package/{types/acorex-cdk-pan-view.d.ts → pan-view/index.d.ts} +0 -0
  67. /package/{types/acorex-cdk-qrcode.d.ts → qrcode/index.d.ts} +0 -0
  68. /package/{types/acorex-cdk-selection.d.ts → selection/index.d.ts} +0 -0
  69. /package/{types/acorex-cdk-sliding-item.d.ts → sliding-item/index.d.ts} +0 -0
  70. /package/{types/acorex-cdk-sticky.d.ts → sticky/index.d.ts} +0 -0
  71. /package/{types/acorex-cdk-virtual-scroll.d.ts → virtual-scroll/index.d.ts} +0 -0
  72. /package/{types/acorex-cdk-wysiwyg.d.ts → wysiwyg/index.d.ts} +0 -0
  73. /package/{types/acorex-cdk-z-index.d.ts → z-index/index.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;
@@ -106,76 +127,165 @@ 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 validated files without enqueueing uploads.
168
246
  */
247
+ async chooseFiles(options = {}) {
248
+ try {
249
+ if (!options.fileType) {
250
+ console.warn('AXUploaderService.chooseFiles: fileType is required for validated file selection.');
251
+ return { accepted: [], rejected: [] };
252
+ }
253
+ const accept = options.accept ?? (await this.fileService.getAcceptAttribute(options.fileType));
254
+ return this.fileService.chooseValidated({
255
+ fileType: options.fileType,
256
+ multiple: options.multiple ?? false,
257
+ accept,
258
+ });
259
+ }
260
+ catch (error) {
261
+ console.error('File choose failed:', error);
262
+ return { accepted: [], rejected: [] };
263
+ }
264
+ }
265
+ async browse(options = { multiple: false }) {
266
+ const { accepted } = await this.chooseFiles(options);
267
+ if (!accepted.length) {
268
+ return [];
269
+ }
270
+ return this.add(accepted, { fileType: options.fileType });
271
+ }
272
+ async cancelAll() {
273
+ await Promise.all(this._files$.value.filter((c) => c.status() !== 'completed').map((c) => c.cancel()));
274
+ }
275
+ clearAll() {
276
+ const remainingFiles = this._files$.value.filter((c) => c.status() === 'inprogress');
277
+ this._files$.next(remainingFiles);
278
+ }
279
+ remove(item) {
280
+ const updatedFiles = this._files$.value.filter((c) => c !== item);
281
+ this._files$.next(updatedFiles);
282
+ }
169
283
  async startUpload() {
170
284
  const newFiles = this._files$.value.filter((c) => c.status() === 'new');
171
285
  for (const file of newFiles) {
172
286
  await this.bindEvents(file);
173
287
  }
174
288
  }
175
- /**
176
- * Binds event handlers to an upload request.
177
- * @private
178
- */
179
289
  async bindEvents(c) {
180
290
  c.onStart.subscribe(() => {
181
291
  this.onFileUploadStart.next({
@@ -208,62 +318,22 @@ class AXUploaderService {
208
318
  });
209
319
  });
210
320
  }
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 [];
321
+ async applyFileTypeValidation(requests, fileType) {
322
+ if (!fileType) {
323
+ return requests;
223
324
  }
224
- catch (error) {
225
- // It's good practice to log the error.
226
- console.error('File browse failed:', error);
227
- return [];
325
+ for (const request of requests) {
326
+ const errors = await this.fileService.validate(request.file, fileType);
327
+ if (errors.length > 0) {
328
+ request.error(errors[0]?.message ?? 'Invalid file');
329
+ }
228
330
  }
331
+ return requests;
229
332
  }
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: "21.1.3", ngImport: i0, type: AXUploaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
264
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXUploaderService, providedIn: 'root' }); }
333
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
334
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderService, providedIn: 'root' }); }
265
335
  }
266
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXUploaderService, decorators: [{
336
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderService, decorators: [{
267
337
  type: Injectable,
268
338
  args: [{ providedIn: 'root' }]
269
339
  }] });
@@ -298,6 +368,11 @@ class AXUploaderZoneDirective {
298
368
  * @defaultValue null
299
369
  */
300
370
  this.accept = input(null, ...(ngDevMode ? [{ debugName: "accept" }] : []));
371
+ /**
372
+ * Logical file type name from {@link AXFileService} (e.g. `conversation-image`).
373
+ * Required for browse handles and validated selection.
374
+ */
375
+ this.fileType = input(null, ...(ngDevMode ? [{ debugName: "fileType" }] : []));
301
376
  /**
302
377
  * Custom template for the drag overlay. If provided, this will be used instead of the default overlay.
303
378
  */
@@ -405,6 +480,7 @@ class AXUploaderZoneDirective {
405
480
  this.animationEndHandler = null;
406
481
  this.element = this.elementRef.nativeElement;
407
482
  this.element.style.position = 'relative';
483
+ bindUploaderZoneHost(this.element, this);
408
484
  //
409
485
  this.uploadService.onFileUploadComplete.pipe(this.unsubscriber.takeUntilDestroy).subscribe((c) => {
410
486
  this.onFileUploadComplete.emit(c);
@@ -430,6 +506,7 @@ class AXUploaderZoneDirective {
430
506
  * Cleans up event listeners when the directive is destroyed.
431
507
  */
432
508
  ngOnDestroy() {
509
+ unbindUploaderZoneHost(this.element);
433
510
  this.element.removeEventListener('click', this.browser.bind(this));
434
511
  this.element.removeEventListener('dragenter', this.handleDragEnter.bind(this));
435
512
  this.element.removeEventListener('drop', this.handleOnDrop.bind(this));
@@ -467,16 +544,25 @@ class AXUploaderZoneDirective {
467
544
  // Reset drag depth on drop
468
545
  this.dragDepth.set(0);
469
546
  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
- });
547
+ let files = Array.from(event.dataTransfer.files);
548
+ let rejected;
549
+ const catalog = this.fileType();
550
+ if (catalog) {
551
+ const result = await this.uploadService.validateFiles(files, catalog);
552
+ files = result.accepted;
553
+ rejected = result.rejected;
554
+ }
555
+ this.fileChange.emit({ event, files, rejected });
556
+ if (!this.disableBrowse()) {
557
+ const requests = await this.uploadService.add(files, { fileType: catalog ?? undefined });
558
+ if (requests.length > 0) {
559
+ this.onChanged.emit({
560
+ component: this,
561
+ requests,
562
+ isUserInteraction: true,
563
+ });
564
+ }
565
+ }
480
566
  }
481
567
  this.removeZone();
482
568
  this.cdr.detectChanges();
@@ -640,32 +726,60 @@ class AXUploaderZoneDirective {
640
726
  * @returns Promise that resolves when files are processed
641
727
  */
642
728
  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),
729
+ const catalog = this.fileType();
730
+ let selected = [];
731
+ let rejected;
732
+ if (catalog) {
733
+ const accept = this.accept() ?? (await this.uploadService.getAcceptAttribute(catalog));
734
+ const result = await this.uploadService.chooseFiles({
735
+ fileType: catalog,
736
+ multiple: this.multiple(),
737
+ accept,
738
+ });
739
+ selected = result.accepted;
740
+ rejected = result.rejected;
741
+ }
742
+ else if (!this.disableBrowse()) {
743
+ const requests = await this.uploadService.browse({
744
+ accept: this.accept() ?? undefined,
745
+ multiple: this.multiple(),
656
746
  });
657
- // Also emit the old onChanged event for backward compatibility
747
+ selected = requests.map((r) => r.file);
748
+ }
749
+ else {
750
+ console.warn('AXUploaderZone.browser: fileType is required when disableBrowse is enabled (use axUploaderBrowseHandle).');
751
+ return;
752
+ }
753
+ if (selected.length === 0 && !(rejected?.length ?? 0)) {
754
+ return;
755
+ }
756
+ const syntheticEvent = new Event('change');
757
+ const target = Object.create(EventTarget.prototype);
758
+ target.files = selected;
759
+ Object.defineProperty(syntheticEvent, 'target', { value: target, writable: false });
760
+ this.fileChange.emit({
761
+ event: syntheticEvent,
762
+ files: selected,
763
+ rejected,
764
+ });
765
+ if (this.disableBrowse()) {
766
+ return;
767
+ }
768
+ const requests = catalog
769
+ ? await this.uploadService.add(selected, { fileType: catalog })
770
+ : await this.uploadService.add(selected);
771
+ if (requests.length > 0) {
658
772
  this.onChanged.emit({
659
773
  component: this,
660
- requests: files,
774
+ requests,
661
775
  isUserInteraction: true,
662
776
  });
663
777
  }
664
778
  }
665
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXUploaderZoneDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
666
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.1.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 }); }
779
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderZoneDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
780
+ 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 }, 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
781
  }
668
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXUploaderZoneDirective, decorators: [{
782
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderZoneDirective, decorators: [{
669
783
  type: Directive,
670
784
  args: [{
671
785
  selector: '[axUploaderZone]',
@@ -674,66 +788,58 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
674
788
  class: 'ax-drop-zone',
675
789
  },
676
790
  }]
677
- }], ctorParameters: () => [], propDecorators: { multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", 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"] }] } });
791
+ }], ctorParameters: () => [] });
678
792
 
679
793
  /**
680
- * A directive that provides browse functionality for file uploads.
681
- * When applied to an element, clicking it will trigger the file browser dialog.
794
+ * Triggers the nearest {@link AXUploaderZoneDirective} file dialog on click.
795
+ * Resolves the zone via DI when possible, otherwise walks the DOM (supports content projection).
682
796
  * @category Directives
683
797
  */
684
798
  class AXUploaderBrowseDirective {
685
799
  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
- */
800
+ this.elementRef = inject(ElementRef);
700
801
  this.platformID = inject(PLATFORM_ID);
802
+ /** When browse + zone share the same host element. */
803
+ this.zoneFromInjector = inject(AXUploaderZoneDirective, { optional: true, self: true });
804
+ this.onClick = () => {
805
+ void this.handleClick();
806
+ };
701
807
  }
702
- /**
703
- * Initializes the directive by adding click event listener and data attribute.
704
- */
705
808
  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
- }
809
+ if (!isPlatformBrowser(this.platformID)) {
810
+ return;
811
+ }
812
+ const element = this.elementRef.nativeElement;
813
+ element.addEventListener('click', this.onClick);
814
+ if (element.dataset) {
815
+ element.dataset['axUploaderBrowseHandle'] = 'true';
816
+ }
817
+ else {
818
+ element.setAttribute('data-ax-uploader-browse-handle', 'true');
716
819
  }
717
820
  }
718
- /**
719
- * Cleans up the directive by removing event listeners.
720
- */
721
821
  ngOnDestroy() {
722
- if (isPlatformBrowser(this.platformID) && this.elementRef.nativeElement) {
723
- this.elementRef.nativeElement.removeEventListener('click', this.handleClick.bind(this));
822
+ if (isPlatformBrowser(this.platformID)) {
823
+ this.elementRef.nativeElement.removeEventListener('click', this.onClick);
724
824
  }
725
825
  }
726
- /**
727
- * Handles the click event to trigger file browser.
728
- * @private
729
- */
826
+ resolveZone() {
827
+ return (this.zoneFromInjector ??
828
+ findUploaderZoneFromDom(this.elementRef.nativeElement) ??
829
+ undefined);
830
+ }
730
831
  async handleClick() {
731
- await this.uploaderZone.browser();
832
+ const zone = this.resolveZone();
833
+ if (!zone) {
834
+ console.warn('[axUploaderBrowseHandle] No axUploaderZone found. Place axUploaderBrowseHandle inside an axUploaderZone.');
835
+ return;
836
+ }
837
+ await zone.browser();
732
838
  }
733
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXUploaderBrowseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
734
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.1.3", type: AXUploaderBrowseDirective, isStandalone: true, selector: "[axUploaderBrowseHandle]", ngImport: i0 }); }
839
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderBrowseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
840
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.3", type: AXUploaderBrowseDirective, isStandalone: true, selector: "[axUploaderBrowseHandle]", ngImport: i0 }); }
735
841
  }
736
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: AXUploaderBrowseDirective, decorators: [{
842
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.3", ngImport: i0, type: AXUploaderBrowseDirective, decorators: [{
737
843
  type: Directive,
738
844
  args: [{ selector: '[axUploaderBrowseHandle]' }]
739
845
  }] });