@acorex/cdk 21.0.0-next.9 → 21.0.0-next51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/accordion/index.d.ts +1 -0
- package/drag-drop/index.d.ts +57 -44
- package/drawer/index.d.ts +12 -9
- package/fesm2022/acorex-cdk-accordion.mjs +52 -67
- package/fesm2022/acorex-cdk-accordion.mjs.map +1 -1
- package/fesm2022/acorex-cdk-carousel.mjs +13 -10
- package/fesm2022/acorex-cdk-carousel.mjs.map +1 -1
- package/fesm2022/acorex-cdk-clipboard.mjs +6 -6
- package/fesm2022/acorex-cdk-clipboard.mjs.map +1 -1
- package/fesm2022/acorex-cdk-common.mjs +104 -104
- package/fesm2022/acorex-cdk-common.mjs.map +1 -1
- package/fesm2022/acorex-cdk-dom.mjs +3 -3
- package/fesm2022/acorex-cdk-dom.mjs.map +1 -1
- package/fesm2022/acorex-cdk-drag-drop.mjs +278 -85
- package/fesm2022/acorex-cdk-drag-drop.mjs.map +1 -1
- package/fesm2022/acorex-cdk-drawer.mjs +44 -33
- package/fesm2022/acorex-cdk-drawer.mjs.map +1 -1
- package/fesm2022/acorex-cdk-focus-trap.mjs +3 -3
- package/fesm2022/acorex-cdk-focus-trap.mjs.map +1 -1
- package/fesm2022/acorex-cdk-input-mask.mjs +5 -3
- package/fesm2022/acorex-cdk-input-mask.mjs.map +1 -1
- package/fesm2022/acorex-cdk-list-navigation.mjs +39 -21
- package/fesm2022/acorex-cdk-list-navigation.mjs.map +1 -1
- package/fesm2022/acorex-cdk-outline.mjs +6 -6
- package/fesm2022/acorex-cdk-outline.mjs.map +1 -1
- package/fesm2022/acorex-cdk-overlay.mjs +3 -3
- package/fesm2022/acorex-cdk-overlay.mjs.map +1 -1
- package/fesm2022/acorex-cdk-pan-view.mjs +3 -3
- package/fesm2022/acorex-cdk-pan-view.mjs.map +1 -1
- package/fesm2022/acorex-cdk-resizable.mjs +166 -118
- package/fesm2022/acorex-cdk-resizable.mjs.map +1 -1
- package/fesm2022/acorex-cdk-selection.mjs +10 -10
- package/fesm2022/acorex-cdk-selection.mjs.map +1 -1
- package/fesm2022/acorex-cdk-sliding-item.mjs +3 -3
- package/fesm2022/acorex-cdk-sliding-item.mjs.map +1 -1
- package/fesm2022/acorex-cdk-sticky.mjs +3 -3
- package/fesm2022/acorex-cdk-sticky.mjs.map +1 -1
- package/fesm2022/acorex-cdk-uploader.mjs +746 -0
- package/fesm2022/acorex-cdk-uploader.mjs.map +1 -0
- package/fesm2022/acorex-cdk-virtual-scroll.mjs +10 -10
- package/fesm2022/acorex-cdk-virtual-scroll.mjs.map +1 -1
- package/list-navigation/index.d.ts +1 -0
- package/package.json +6 -2
- package/resizable/index.d.ts +19 -7
- package/uploader/README.md +3 -0
- package/uploader/index.d.ts +378 -0
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { signal, computed, inject, Injectable, ElementRef, ViewContainerRef, input, ChangeDetectorRef, DOCUMENT, PLATFORM_ID, output, Directive } from '@angular/core';
|
|
4
|
+
import { AXTranslationService } from '@acorex/core/translation';
|
|
5
|
+
import { AXUnsubscriber } from '@acorex/core/utils';
|
|
6
|
+
import { AXFileService } from '@acorex/core/file';
|
|
7
|
+
import { sumBy } from 'lodash-es';
|
|
8
|
+
import { Subject, BehaviorSubject, map } from 'rxjs';
|
|
9
|
+
|
|
10
|
+
class AXUploadRequest {
|
|
11
|
+
get name() {
|
|
12
|
+
return this.file.name;
|
|
13
|
+
}
|
|
14
|
+
get ext() {
|
|
15
|
+
const parts = this.name.split('.');
|
|
16
|
+
if (parts.length > 1) {
|
|
17
|
+
return parts[parts.length - 1];
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// No extension found
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
get size() {
|
|
25
|
+
return this.file.size;
|
|
26
|
+
}
|
|
27
|
+
get file() {
|
|
28
|
+
return this.uploadFile;
|
|
29
|
+
}
|
|
30
|
+
constructor(uploadFile) {
|
|
31
|
+
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" }] : []));
|
|
42
|
+
this.bytesTransferred = 0;
|
|
43
|
+
this.onCancel = new Subject();
|
|
44
|
+
this.onStart = new Subject();
|
|
45
|
+
this.onFailed = new Subject();
|
|
46
|
+
this.onComplete = new BehaviorSubject(null);
|
|
47
|
+
}
|
|
48
|
+
estimateTimeRemaining(bytesTransferred) {
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const elapsed = now - this.startTime; // Time in milliseconds
|
|
51
|
+
if (isNaN(elapsed) || elapsed === 0) {
|
|
52
|
+
return null; // Avoid division by zero
|
|
53
|
+
}
|
|
54
|
+
const speed = (bytesTransferred || 1) / elapsed; // Bytes per millisecond
|
|
55
|
+
const remainingBytes = this.size - bytesTransferred;
|
|
56
|
+
const estimatedTime = Math.ceil(remainingBytes / speed); // Time in milliseconds
|
|
57
|
+
return estimatedTime; // Return the estimated time in milliseconds
|
|
58
|
+
}
|
|
59
|
+
setTransferredBytes(value) {
|
|
60
|
+
this.bytesTransferred = value;
|
|
61
|
+
if (value > 0 && !this._isDetermined() && (this.status() == 'new' || this.status() == 'inprogress')) {
|
|
62
|
+
this._isDetermined.set(true);
|
|
63
|
+
}
|
|
64
|
+
this.updateEstimateTime();
|
|
65
|
+
}
|
|
66
|
+
updateEstimateTime() {
|
|
67
|
+
this._estimateTime.set(this.estimateTimeRemaining(this.bytesTransferred));
|
|
68
|
+
const progress = Math.floor((this.bytesTransferred / this.size) * 100);
|
|
69
|
+
this._progress.set(progress);
|
|
70
|
+
}
|
|
71
|
+
async upload() {
|
|
72
|
+
this.startTime = Date.now();
|
|
73
|
+
this._progress.set(0);
|
|
74
|
+
this._status.set('inprogress');
|
|
75
|
+
this.onStart.next();
|
|
76
|
+
}
|
|
77
|
+
cancel() {
|
|
78
|
+
this._status.set('canceled');
|
|
79
|
+
this.bytesTransferred = 0;
|
|
80
|
+
this._estimateTime.set(0);
|
|
81
|
+
this._progress.set(0);
|
|
82
|
+
this.onCancel.next();
|
|
83
|
+
}
|
|
84
|
+
redo() {
|
|
85
|
+
// this.startTime = Date.now();
|
|
86
|
+
this._progress.set(0);
|
|
87
|
+
this._status.set('inprogress');
|
|
88
|
+
this._message.set(null);
|
|
89
|
+
this.onStart.next();
|
|
90
|
+
}
|
|
91
|
+
error(message) {
|
|
92
|
+
this._status.set('failed');
|
|
93
|
+
this.bytesTransferred = 0;
|
|
94
|
+
this._estimateTime.set(0);
|
|
95
|
+
this._progress.set(0);
|
|
96
|
+
this._message.set(message);
|
|
97
|
+
this.onFailed.next();
|
|
98
|
+
}
|
|
99
|
+
finish(data) {
|
|
100
|
+
this._status.set('completed');
|
|
101
|
+
this.bytesTransferred = this.size;
|
|
102
|
+
this._estimateTime.set(0);
|
|
103
|
+
this._progress.set(100);
|
|
104
|
+
this.onComplete.next(data);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Service for managing file uploads with drag-and-drop support, progress tracking, and dialog management.
|
|
110
|
+
* @category Services
|
|
111
|
+
*/
|
|
112
|
+
class AXUploaderService {
|
|
113
|
+
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
|
+
this.fileService = inject(AXFileService);
|
|
124
|
+
/**
|
|
125
|
+
* Behavior subject for managing upload requests.
|
|
126
|
+
* @ignore
|
|
127
|
+
*/
|
|
128
|
+
this._files$ = new BehaviorSubject([]);
|
|
129
|
+
/**
|
|
130
|
+
* Gets the files behavior subject for observing upload requests.
|
|
131
|
+
*/
|
|
132
|
+
this.files = this._files$.asObservable();
|
|
133
|
+
/**
|
|
134
|
+
* Subject for file upload start events.
|
|
135
|
+
*/
|
|
136
|
+
this.onFileUploadStart = new Subject();
|
|
137
|
+
/**
|
|
138
|
+
* Subject for file upload complete events.
|
|
139
|
+
*/
|
|
140
|
+
this.onFileUploadComplete = new Subject();
|
|
141
|
+
/**
|
|
142
|
+
* Subject for all files upload complete events.
|
|
143
|
+
*/
|
|
144
|
+
this.onFilesUploadComplete = new Subject();
|
|
145
|
+
/**
|
|
146
|
+
* Subject for file upload canceled events.
|
|
147
|
+
*/
|
|
148
|
+
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
|
+
*/
|
|
156
|
+
this.totalEstimateTime = this._files$.pipe(map((files) => sumBy(files, (file) => (file.status() === 'inprogress' ? file.estimateTime() : 0))));
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Converts a File object to an AXUploadRequest.
|
|
160
|
+
* @private
|
|
161
|
+
*/
|
|
162
|
+
convertFileToRequest(file) {
|
|
163
|
+
return new AXUploadRequest(file);
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Starts uploading files that are in 'new' status.
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
async startUpload() {
|
|
170
|
+
const newFiles = this._files$.value.filter((c) => c.status() === 'new');
|
|
171
|
+
for (const file of newFiles) {
|
|
172
|
+
await this.bindEvents(file);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Binds event handlers to an upload request.
|
|
177
|
+
* @private
|
|
178
|
+
*/
|
|
179
|
+
async bindEvents(c) {
|
|
180
|
+
c.onStart.subscribe(() => {
|
|
181
|
+
this.onFileUploadStart.next({
|
|
182
|
+
component: this,
|
|
183
|
+
uploadedFile: c,
|
|
184
|
+
isUserInteraction: false,
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
c.onComplete.subscribe((data) => {
|
|
188
|
+
Object.assign(c, { metaData: data });
|
|
189
|
+
this.onFileUploadComplete.next({
|
|
190
|
+
component: this,
|
|
191
|
+
uploadedFile: c,
|
|
192
|
+
isUserInteraction: false,
|
|
193
|
+
});
|
|
194
|
+
const isAllDone = this._files$.value.every((f) => f.status() === 'completed' || f.status() === 'canceled' || f.status() === 'failed');
|
|
195
|
+
if (isAllDone) {
|
|
196
|
+
this.onFilesUploadComplete.next({
|
|
197
|
+
component: this,
|
|
198
|
+
uploadedFiles: this._files$.value,
|
|
199
|
+
isUserInteraction: false,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
c.onCancel.subscribe(() => {
|
|
204
|
+
this.onFileUploadCanceled.next({
|
|
205
|
+
component: this,
|
|
206
|
+
uploadedFile: c,
|
|
207
|
+
isUserInteraction: false,
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
}
|
|
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 [];
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// It's good practice to log the error.
|
|
226
|
+
console.error('File browse failed:', error);
|
|
227
|
+
return [];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
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.9", ngImport: i0, type: AXUploaderService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
264
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXUploaderService, providedIn: 'root' }); }
|
|
265
|
+
}
|
|
266
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXUploaderService, decorators: [{
|
|
267
|
+
type: Injectable,
|
|
268
|
+
args: [{ providedIn: 'root' }]
|
|
269
|
+
}] });
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* A directive that provides drag-and-drop and file upload functionality.
|
|
273
|
+
* When applied to an element, it enables drag-and-drop file uploads and file browsing.
|
|
274
|
+
* @category Directives
|
|
275
|
+
*/
|
|
276
|
+
class AXUploaderZoneDirective {
|
|
277
|
+
/**
|
|
278
|
+
* Initializes the directive with event listeners and service subscriptions.
|
|
279
|
+
*/
|
|
280
|
+
constructor() {
|
|
281
|
+
/**
|
|
282
|
+
* The element reference for the directive host.
|
|
283
|
+
* @ignore
|
|
284
|
+
*/
|
|
285
|
+
this.elementRef = inject(ElementRef);
|
|
286
|
+
/**
|
|
287
|
+
* View container reference for rendering custom templates.
|
|
288
|
+
* @ignore
|
|
289
|
+
*/
|
|
290
|
+
this.viewContainerRef = inject(ViewContainerRef);
|
|
291
|
+
/**
|
|
292
|
+
* Whether multiple files can be selected.
|
|
293
|
+
* @defaultValue true
|
|
294
|
+
*/
|
|
295
|
+
this.multiple = input(true, ...(ngDevMode ? [{ debugName: "multiple" }] : []));
|
|
296
|
+
/**
|
|
297
|
+
* File types that are accepted for upload.
|
|
298
|
+
* @defaultValue null
|
|
299
|
+
*/
|
|
300
|
+
this.accept = input(null, ...(ngDevMode ? [{ debugName: "accept" }] : []));
|
|
301
|
+
/**
|
|
302
|
+
* Custom template for the drag overlay. If provided, this will be used instead of the default overlay.
|
|
303
|
+
*/
|
|
304
|
+
this.overlayTemplate = input(...(ngDevMode ? [undefined, { debugName: "overlayTemplate" }] : []));
|
|
305
|
+
/**
|
|
306
|
+
* Whether browsing files by clicking on the container is disabled.
|
|
307
|
+
* @defaultValue false
|
|
308
|
+
*/
|
|
309
|
+
this.disableBrowse = input(false, ...(ngDevMode ? [{ debugName: "disableBrowse" }] : []));
|
|
310
|
+
/**
|
|
311
|
+
* Whether drag and drop functionality is disabled.
|
|
312
|
+
* @defaultValue false
|
|
313
|
+
*/
|
|
314
|
+
this.disableDragDrop = input(false, ...(ngDevMode ? [{ debugName: "disableDragDrop" }] : []));
|
|
315
|
+
/**
|
|
316
|
+
* Change detector reference.
|
|
317
|
+
* @ignore
|
|
318
|
+
*/
|
|
319
|
+
this.cdr = inject(ChangeDetectorRef);
|
|
320
|
+
/**
|
|
321
|
+
* Document reference.
|
|
322
|
+
* @ignore
|
|
323
|
+
*/
|
|
324
|
+
this.document = inject(DOCUMENT);
|
|
325
|
+
/**
|
|
326
|
+
* Platform ID for browser detection.
|
|
327
|
+
* @ignore
|
|
328
|
+
*/
|
|
329
|
+
this.platformID = inject(PLATFORM_ID);
|
|
330
|
+
/**
|
|
331
|
+
* Upload service instance.
|
|
332
|
+
* @ignore
|
|
333
|
+
*/
|
|
334
|
+
this.uploadService = inject(AXUploaderService);
|
|
335
|
+
/**
|
|
336
|
+
* Unsubscriber service instance.
|
|
337
|
+
* @ignore
|
|
338
|
+
*/
|
|
339
|
+
this.unsubscriber = inject(AXUnsubscriber);
|
|
340
|
+
/**
|
|
341
|
+
* Translation service instance.
|
|
342
|
+
* @ignore
|
|
343
|
+
*/
|
|
344
|
+
this.translateService = inject(AXTranslationService);
|
|
345
|
+
/**
|
|
346
|
+
* Embedded view reference for custom template.
|
|
347
|
+
* @ignore
|
|
348
|
+
*/
|
|
349
|
+
this.templateViewRef = null;
|
|
350
|
+
/**
|
|
351
|
+
* Emitted when files are selected or dropped (like HTML file input).
|
|
352
|
+
* Emits an event with files property containing the selected files.
|
|
353
|
+
*/
|
|
354
|
+
this.fileChange = output();
|
|
355
|
+
/**
|
|
356
|
+
* Emitted when files are changed (added, removed, etc.).
|
|
357
|
+
* @deprecated Use fileChange instead for simpler file input-like behavior
|
|
358
|
+
*/
|
|
359
|
+
this.onChanged = output();
|
|
360
|
+
/**
|
|
361
|
+
* Emitted on drag enter event.
|
|
362
|
+
*/
|
|
363
|
+
this.dragEnter = output();
|
|
364
|
+
/**
|
|
365
|
+
* Emitted on drag leave event.
|
|
366
|
+
*/
|
|
367
|
+
this.dragLeave = output();
|
|
368
|
+
/**
|
|
369
|
+
* Emitted on drag over event.
|
|
370
|
+
*/
|
|
371
|
+
this.dragOver = output();
|
|
372
|
+
/**
|
|
373
|
+
* Emitted when a single file upload is completed.
|
|
374
|
+
*/
|
|
375
|
+
this.onFileUploadComplete = output();
|
|
376
|
+
/**
|
|
377
|
+
* Emitted when all files upload is completed.
|
|
378
|
+
*/
|
|
379
|
+
this.onFilesUploadComplete = output();
|
|
380
|
+
/**
|
|
381
|
+
* CSS class for the overlay state.
|
|
382
|
+
* @ignore
|
|
383
|
+
*/
|
|
384
|
+
this.stateClass = 'ax-uploader-overlay-state';
|
|
385
|
+
/**
|
|
386
|
+
* The overlay element for drag and drop visual feedback.
|
|
387
|
+
* @ignore
|
|
388
|
+
*/
|
|
389
|
+
this.overlayElement = null;
|
|
390
|
+
/**
|
|
391
|
+
* Flag to track if dragOver has been emitted for the current drag session.
|
|
392
|
+
* @ignore
|
|
393
|
+
*/
|
|
394
|
+
this.dragOverEmitted = signal(false, ...(ngDevMode ? [{ debugName: "dragOverEmitted" }] : []));
|
|
395
|
+
/**
|
|
396
|
+
* Counter to track drag enter/leave depth to prevent false dragleave events.
|
|
397
|
+
* When moving from parent to child, dragleave fires even though we're still inside.
|
|
398
|
+
* @ignore
|
|
399
|
+
*/
|
|
400
|
+
this.dragDepth = signal(0, ...(ngDevMode ? [{ debugName: "dragDepth" }] : []));
|
|
401
|
+
/**
|
|
402
|
+
* Animation end handler for cleanup.
|
|
403
|
+
* @ignore
|
|
404
|
+
*/
|
|
405
|
+
this.animationEndHandler = null;
|
|
406
|
+
this.element = this.elementRef.nativeElement;
|
|
407
|
+
this.element.style.position = 'relative';
|
|
408
|
+
//
|
|
409
|
+
this.uploadService.onFileUploadComplete.pipe(this.unsubscriber.takeUntilDestroy).subscribe((c) => {
|
|
410
|
+
this.onFileUploadComplete.emit(c);
|
|
411
|
+
});
|
|
412
|
+
this.uploadService.onFilesUploadComplete.pipe(this.unsubscriber.takeUntilDestroy).subscribe((c) => {
|
|
413
|
+
this.onFilesUploadComplete.emit(c);
|
|
414
|
+
});
|
|
415
|
+
//
|
|
416
|
+
setTimeout(() => {
|
|
417
|
+
const browseHandlers = this.element.querySelectorAll('[data-ax-uploader-browse-handle="true"]');
|
|
418
|
+
if (browseHandlers.length <= 0 && !this.disableBrowse()) {
|
|
419
|
+
this.element.addEventListener('click', this.browser.bind(this), true);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
if (!this.disableDragDrop()) {
|
|
423
|
+
this.element.addEventListener('dragenter', this.handleDragEnter.bind(this), true);
|
|
424
|
+
this.element.addEventListener('dragover', this.handleDragOver.bind(this), true);
|
|
425
|
+
this.element.addEventListener('drop', this.handleOnDrop.bind(this), true);
|
|
426
|
+
this.element.addEventListener('dragleave', (e) => this.removeZone(e), true);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Cleans up event listeners when the directive is destroyed.
|
|
431
|
+
*/
|
|
432
|
+
ngOnDestroy() {
|
|
433
|
+
this.element.removeEventListener('click', this.browser.bind(this));
|
|
434
|
+
this.element.removeEventListener('dragenter', this.handleDragEnter.bind(this));
|
|
435
|
+
this.element.removeEventListener('drop', this.handleOnDrop.bind(this));
|
|
436
|
+
this.element.removeEventListener('dragover', this.handleDragOver.bind(this));
|
|
437
|
+
this.element.removeEventListener('dragleave', this.removeZone.bind(this));
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Handles drag enter events to show the upload overlay.
|
|
441
|
+
* @private
|
|
442
|
+
*/
|
|
443
|
+
async handleDragEnter(event) {
|
|
444
|
+
if (this.disableDragDrop())
|
|
445
|
+
return;
|
|
446
|
+
this.dragOverEmitted.set(false); // Reset flag when entering
|
|
447
|
+
this.dragDepth.update((depth) => depth + 1); // Increment depth counter
|
|
448
|
+
// Only create overlay on first enter (when depth is 1)
|
|
449
|
+
if (this.dragDepth() === 1) {
|
|
450
|
+
// Cancel any pending exit animation
|
|
451
|
+
this.cancelExitAnimation();
|
|
452
|
+
await this.createZone();
|
|
453
|
+
this.dragEnter.emit(event);
|
|
454
|
+
}
|
|
455
|
+
event.preventDefault();
|
|
456
|
+
event.stopImmediatePropagation();
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Handles drop events to process dropped files.
|
|
460
|
+
* @private
|
|
461
|
+
*/
|
|
462
|
+
async handleOnDrop(event) {
|
|
463
|
+
if (this.disableDragDrop())
|
|
464
|
+
return;
|
|
465
|
+
event.preventDefault();
|
|
466
|
+
event.stopImmediatePropagation();
|
|
467
|
+
// Reset drag depth on drop
|
|
468
|
+
this.dragDepth.set(0);
|
|
469
|
+
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
|
+
});
|
|
480
|
+
}
|
|
481
|
+
this.removeZone();
|
|
482
|
+
this.cdr.detectChanges();
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Handles drag over events to allow dropping.
|
|
486
|
+
* @private
|
|
487
|
+
*/
|
|
488
|
+
handleDragOver(event) {
|
|
489
|
+
if (this.disableDragDrop())
|
|
490
|
+
return;
|
|
491
|
+
// Only emit dragOver once per drag session to avoid spam
|
|
492
|
+
if (!this.dragOverEmitted()) {
|
|
493
|
+
this.dragOverEmitted.set(true);
|
|
494
|
+
this.dragOver.emit(event);
|
|
495
|
+
}
|
|
496
|
+
event.preventDefault();
|
|
497
|
+
event.stopImmediatePropagation();
|
|
498
|
+
}
|
|
499
|
+
/**
|
|
500
|
+
* Creates the visual overlay for drag and drop feedback.
|
|
501
|
+
* @private
|
|
502
|
+
*/
|
|
503
|
+
async createZone() {
|
|
504
|
+
if (isPlatformBrowser(this.platformID)) {
|
|
505
|
+
// Check if overlay already exists (might be exiting)
|
|
506
|
+
let existingOverlay = this.document.getElementById('ax-uploader-overlay-state') || this.overlayElement;
|
|
507
|
+
// Verify the overlay is actually in the DOM (not removed after animation)
|
|
508
|
+
if (existingOverlay && existingOverlay.parentNode === this.element) {
|
|
509
|
+
// Remove exiting class to cancel exit animation and show overlay again
|
|
510
|
+
existingOverlay.classList.remove('ax-uploader-overlay-exiting');
|
|
511
|
+
// Clean up any pending animation handler
|
|
512
|
+
if (this.animationEndHandler) {
|
|
513
|
+
existingOverlay.removeEventListener('animationend', this.animationEndHandler);
|
|
514
|
+
this.animationEndHandler = null;
|
|
515
|
+
}
|
|
516
|
+
// Reset overlayElement reference if it was cleared
|
|
517
|
+
if (!this.overlayElement && existingOverlay) {
|
|
518
|
+
this.overlayElement = existingOverlay;
|
|
519
|
+
}
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
else if (existingOverlay && existingOverlay.parentNode !== this.element) {
|
|
523
|
+
// Overlay exists but is not in the DOM, clear the reference
|
|
524
|
+
this.overlayElement = null;
|
|
525
|
+
existingOverlay = null;
|
|
526
|
+
}
|
|
527
|
+
// Check if custom template is provided
|
|
528
|
+
const template = this.overlayTemplate();
|
|
529
|
+
if (template) {
|
|
530
|
+
// Create container for template
|
|
531
|
+
this.overlayElement = this.document.createElement('div');
|
|
532
|
+
this.overlayElement.classList.add('ax-uploader-overlay-state', '-ax-z-1');
|
|
533
|
+
this.overlayElement.id = 'ax-uploader-overlay-state';
|
|
534
|
+
// Create embedded view from template (will be inserted at directive anchor)
|
|
535
|
+
this.viewContainerRef.clear();
|
|
536
|
+
this.templateViewRef = this.viewContainerRef.createEmbeddedView(template);
|
|
537
|
+
this.templateViewRef.detectChanges();
|
|
538
|
+
// Get root nodes and move them to overlay element
|
|
539
|
+
const rootNodes = this.templateViewRef.rootNodes;
|
|
540
|
+
for (const node of rootNodes) {
|
|
541
|
+
// Remove from current location if it has a parent
|
|
542
|
+
if (node.parentNode) {
|
|
543
|
+
node.parentNode.removeChild(node);
|
|
544
|
+
}
|
|
545
|
+
this.overlayElement.appendChild(node);
|
|
546
|
+
}
|
|
547
|
+
// Detach the view since we've moved the nodes
|
|
548
|
+
this.templateViewRef.detach();
|
|
549
|
+
this.element.appendChild(this.overlayElement);
|
|
550
|
+
this.cdr.detectChanges();
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
// Use default overlay
|
|
554
|
+
this.overlayElement = this.document.createElement('div');
|
|
555
|
+
this.overlayElement.classList.add('ax-uploader-overlay-state', '-ax-z-1');
|
|
556
|
+
this.overlayElement.id = 'ax-uploader-overlay-state';
|
|
557
|
+
const icon = this.document.createElement('span');
|
|
558
|
+
icon.classList.add('ax-icon', 'ax-icon-upload');
|
|
559
|
+
const text = this.document.createElement('span');
|
|
560
|
+
text.innerText = await this.translateService.translateAsync('@acorex:uploader.zone.text');
|
|
561
|
+
this.overlayElement.appendChild(icon);
|
|
562
|
+
this.overlayElement.appendChild(text);
|
|
563
|
+
this.element.appendChild(this.overlayElement);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* Cancels any pending exit animation and cleans up handlers.
|
|
568
|
+
* @private
|
|
569
|
+
*/
|
|
570
|
+
cancelExitAnimation() {
|
|
571
|
+
if (isPlatformBrowser(this.platformID)) {
|
|
572
|
+
const overlay = this.document.getElementById('ax-uploader-overlay-state') || this.overlayElement;
|
|
573
|
+
if (overlay && this.animationEndHandler) {
|
|
574
|
+
// Remove exiting class to cancel animation
|
|
575
|
+
overlay.classList.remove('ax-uploader-overlay-exiting');
|
|
576
|
+
// Clean up the handler
|
|
577
|
+
overlay.removeEventListener('animationend', this.animationEndHandler);
|
|
578
|
+
this.animationEndHandler = null;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Removes the visual overlay for drag and drop feedback.
|
|
584
|
+
* @private
|
|
585
|
+
*/
|
|
586
|
+
removeZone(event) {
|
|
587
|
+
if (this.disableDragDrop())
|
|
588
|
+
return;
|
|
589
|
+
this.dragDepth.update((depth) => Math.max(0, depth - 1)); // Decrement depth counter, ensure non-negative
|
|
590
|
+
// Only remove overlay when we've truly left the drop zone (depth reaches 0)
|
|
591
|
+
if (this.dragDepth() <= 0) {
|
|
592
|
+
this.dragDepth.set(0); // Ensure it's exactly 0
|
|
593
|
+
this.dragOverEmitted.set(false); // Reset flag when leaving
|
|
594
|
+
if (event) {
|
|
595
|
+
this.dragLeave.emit(event);
|
|
596
|
+
}
|
|
597
|
+
if (isPlatformBrowser(this.platformID)) {
|
|
598
|
+
const overlay = this.document.getElementById('ax-uploader-overlay-state') || this.overlayElement;
|
|
599
|
+
if (overlay) {
|
|
600
|
+
// Clean up any existing handler first
|
|
601
|
+
if (this.animationEndHandler) {
|
|
602
|
+
overlay.removeEventListener('animationend', this.animationEndHandler);
|
|
603
|
+
}
|
|
604
|
+
// Add exiting class to trigger CSS animation
|
|
605
|
+
overlay.classList.add('ax-uploader-overlay-exiting');
|
|
606
|
+
// Wait for CSS animation to complete
|
|
607
|
+
this.animationEndHandler = (e) => {
|
|
608
|
+
if (e.target === overlay && e.animationName === 'fadeOutScale') {
|
|
609
|
+
overlay.removeEventListener('animationend', this.animationEndHandler);
|
|
610
|
+
this.animationEndHandler = null;
|
|
611
|
+
if (overlay.parentNode === this.element) {
|
|
612
|
+
this.element.removeChild(overlay);
|
|
613
|
+
}
|
|
614
|
+
if (this.overlayElement && this.overlayElement.parentNode) {
|
|
615
|
+
this.overlayElement.remove();
|
|
616
|
+
}
|
|
617
|
+
// Clear the overlay element reference
|
|
618
|
+
this.overlayElement = null;
|
|
619
|
+
// Remove custom template view if it exists
|
|
620
|
+
if (this.templateViewRef) {
|
|
621
|
+
this.viewContainerRef.clear();
|
|
622
|
+
this.templateViewRef = null;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
overlay.addEventListener('animationend', this.animationEndHandler);
|
|
627
|
+
}
|
|
628
|
+
else {
|
|
629
|
+
// If overlay doesn't exist, just clean up template view
|
|
630
|
+
if (this.templateViewRef) {
|
|
631
|
+
this.viewContainerRef.clear();
|
|
632
|
+
this.templateViewRef = null;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Opens the file browser dialog and processes selected files.
|
|
640
|
+
* @returns Promise that resolves when files are processed
|
|
641
|
+
*/
|
|
642
|
+
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),
|
|
656
|
+
});
|
|
657
|
+
// Also emit the old onChanged event for backward compatibility
|
|
658
|
+
this.onChanged.emit({
|
|
659
|
+
component: this,
|
|
660
|
+
requests: files,
|
|
661
|
+
isUserInteraction: true,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXUploaderZoneDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
666
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "20.3.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 }, 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
|
+
}
|
|
668
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXUploaderZoneDirective, decorators: [{
|
|
669
|
+
type: Directive,
|
|
670
|
+
args: [{
|
|
671
|
+
selector: '[axUploaderZone]',
|
|
672
|
+
providers: [AXUnsubscriber],
|
|
673
|
+
host: {
|
|
674
|
+
class: 'ax-drop-zone',
|
|
675
|
+
},
|
|
676
|
+
}]
|
|
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"] }] } });
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* A directive that provides browse functionality for file uploads.
|
|
681
|
+
* When applied to an element, clicking it will trigger the file browser dialog.
|
|
682
|
+
* @category Directives
|
|
683
|
+
*/
|
|
684
|
+
class AXUploaderBrowseDirective {
|
|
685
|
+
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
|
+
*/
|
|
700
|
+
this.platformID = inject(PLATFORM_ID);
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* Initializes the directive by adding click event listener and data attribute.
|
|
704
|
+
*/
|
|
705
|
+
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
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* Cleans up the directive by removing event listeners.
|
|
720
|
+
*/
|
|
721
|
+
ngOnDestroy() {
|
|
722
|
+
if (isPlatformBrowser(this.platformID) && this.elementRef.nativeElement) {
|
|
723
|
+
this.elementRef.nativeElement.removeEventListener('click', this.handleClick.bind(this));
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* Handles the click event to trigger file browser.
|
|
728
|
+
* @private
|
|
729
|
+
*/
|
|
730
|
+
async handleClick() {
|
|
731
|
+
await this.uploaderZone.browser();
|
|
732
|
+
}
|
|
733
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXUploaderBrowseDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
|
|
734
|
+
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "20.3.9", type: AXUploaderBrowseDirective, isStandalone: true, selector: "[axUploaderBrowseHandle]", ngImport: i0 }); }
|
|
735
|
+
}
|
|
736
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.9", ngImport: i0, type: AXUploaderBrowseDirective, decorators: [{
|
|
737
|
+
type: Directive,
|
|
738
|
+
args: [{ selector: '[axUploaderBrowseHandle]' }]
|
|
739
|
+
}] });
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Generated bundle index. Do not edit.
|
|
743
|
+
*/
|
|
744
|
+
|
|
745
|
+
export { AXUploadRequest, AXUploaderBrowseDirective, AXUploaderService, AXUploaderZoneDirective };
|
|
746
|
+
//# sourceMappingURL=acorex-cdk-uploader.mjs.map
|