UNPKG

17.2 kBJavaScriptView Raw
1import { EventEmitter, Directive, ElementRef, Input, Output, HostListener, NgModule } from '@angular/core';
2import { Subject, Observable } from 'rxjs';
3import { mergeMap, finalize } from 'rxjs/operators';
4
5var UploadStatus;
6(function (UploadStatus) {
7 UploadStatus[UploadStatus["Queue"] = 0] = "Queue";
8 UploadStatus[UploadStatus["Uploading"] = 1] = "Uploading";
9 UploadStatus[UploadStatus["Done"] = 2] = "Done";
10 UploadStatus[UploadStatus["Cancelled"] = 3] = "Cancelled";
11})(UploadStatus || (UploadStatus = {}));
12
13function humanizeBytes(bytes) {
14 if (bytes === 0) {
15 return '0 Byte';
16 }
17 const k = 1024;
18 const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB'];
19 const i = Math.floor(Math.log(bytes) / Math.log(k));
20 return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
21}
22class NgUploaderService {
23 constructor(concurrency = Number.POSITIVE_INFINITY, contentTypes = ['*'], maxUploads = Number.POSITIVE_INFINITY, maxFileSize = Number.POSITIVE_INFINITY) {
24 this.queue = [];
25 this.serviceEvents = new EventEmitter();
26 this.uploadScheduler = new Subject();
27 this.subs = [];
28 this.contentTypes = contentTypes;
29 this.maxUploads = maxUploads;
30 this.maxFileSize = maxFileSize;
31 this.uploadScheduler
32 .pipe(mergeMap(upload => this.startUpload(upload), concurrency))
33 .subscribe(uploadOutput => this.serviceEvents.emit(uploadOutput));
34 }
35 handleFiles(incomingFiles) {
36 const allowedIncomingFiles = [].reduce.call(incomingFiles, (acc, checkFile, i) => {
37 const futureQueueLength = acc.length + this.queue.length + 1;
38 if (this.isContentTypeAllowed(checkFile.type) &&
39 futureQueueLength <= this.maxUploads &&
40 this.isFileSizeAllowed(checkFile.size)) {
41 acc = acc.concat(checkFile);
42 }
43 else {
44 const rejectedFile = this.makeUploadFile(checkFile, i);
45 this.serviceEvents.emit({ type: 'rejected', file: rejectedFile });
46 }
47 return acc;
48 }, []);
49 this.queue.push(...[].map.call(allowedIncomingFiles, (file, i) => {
50 const uploadFile = this.makeUploadFile(file, i);
51 this.serviceEvents.emit({ type: 'addedToQueue', file: uploadFile });
52 return uploadFile;
53 }));
54 this.serviceEvents.emit({ type: 'allAddedToQueue' });
55 }
56 initInputEvents(input) {
57 return input.subscribe((event) => {
58 switch (event.type) {
59 case 'uploadFile':
60 const uploadFileIndex = this.queue.findIndex(file => file === event.file);
61 if (uploadFileIndex !== -1 && event.file) {
62 this.uploadScheduler.next({ file: this.queue[uploadFileIndex], event: event });
63 }
64 break;
65 case 'uploadAll':
66 const files = this.queue.filter(file => file.progress.status === UploadStatus.Queue);
67 files.forEach(file => this.uploadScheduler.next({ file: file, event: event }));
68 break;
69 case 'cancel':
70 const id = event.id || null;
71 if (!id) {
72 return;
73 }
74 const subs = this.subs.filter(sub => sub.id === id);
75 subs.forEach(sub => {
76 if (sub.sub) {
77 sub.sub.unsubscribe();
78 const fileIndex = this.queue.findIndex(file => file.id === id);
79 if (fileIndex !== -1) {
80 this.queue[fileIndex].progress.status = UploadStatus.Cancelled;
81 this.serviceEvents.emit({ type: 'cancelled', file: this.queue[fileIndex] });
82 }
83 }
84 });
85 break;
86 case 'cancelAll':
87 this.subs.forEach(sub => {
88 if (sub.sub) {
89 sub.sub.unsubscribe();
90 }
91 const file = this.queue.find(uploadFile => uploadFile.id === sub.id);
92 if (file) {
93 file.progress.status = UploadStatus.Cancelled;
94 this.serviceEvents.emit({ type: 'cancelled', file: file });
95 }
96 });
97 break;
98 case 'remove':
99 if (!event.id) {
100 return;
101 }
102 const i = this.queue.findIndex(file => file.id === event.id);
103 if (i !== -1) {
104 const file = this.queue[i];
105 this.queue.splice(i, 1);
106 this.serviceEvents.emit({ type: 'removed', file: file });
107 }
108 break;
109 case 'removeAll':
110 if (this.queue.length) {
111 this.queue = [];
112 this.serviceEvents.emit({ type: 'removedAll' });
113 }
114 break;
115 }
116 });
117 }
118 startUpload(upload) {
119 return new Observable(observer => {
120 const sub = this.uploadFile(upload.file, upload.event)
121 .pipe(finalize(() => {
122 if (!observer.closed) {
123 observer.complete();
124 }
125 }))
126 .subscribe(output => {
127 observer.next(output);
128 }, err => {
129 observer.error(err);
130 observer.complete();
131 }, () => {
132 observer.complete();
133 });
134 this.subs.push({ id: upload.file.id, sub: sub });
135 });
136 }
137 uploadFile(file, event) {
138 return new Observable(observer => {
139 const url = event.url || '';
140 const method = event.method || 'POST';
141 const data = event.data || {};
142 const headers = event.headers || {};
143 const xhr = new XMLHttpRequest();
144 const time = new Date().getTime();
145 let progressStartTime = (file.progress.data && file.progress.data.startTime) || time;
146 let speed = 0;
147 let eta = null;
148 xhr.upload.addEventListener('progress', (e) => {
149 if (e.lengthComputable) {
150 const percentage = Math.round((e.loaded * 100) / e.total);
151 const diff = new Date().getTime() - time;
152 speed = Math.round((e.loaded / diff) * 1000);
153 progressStartTime = (file.progress.data && file.progress.data.startTime) || new Date().getTime();
154 eta = Math.ceil((e.total - e.loaded) / speed);
155 file.progress = {
156 status: UploadStatus.Uploading,
157 data: {
158 percentage: percentage,
159 speed: speed,
160 speedHuman: `${humanizeBytes(speed)}/s`,
161 startTime: progressStartTime,
162 endTime: null,
163 eta: eta,
164 etaHuman: this.secondsToHuman(eta)
165 }
166 };
167 observer.next({ type: 'uploading', file: file });
168 }
169 }, false);
170 xhr.upload.addEventListener('error', (e) => {
171 observer.error(e);
172 observer.complete();
173 });
174 xhr.onreadystatechange = () => {
175 if (xhr.readyState === XMLHttpRequest.DONE) {
176 const speedAverage = Math.round((file.size / (new Date().getTime() - progressStartTime)) * 1000);
177 file.progress = {
178 status: UploadStatus.Done,
179 data: {
180 percentage: 100,
181 speed: speedAverage,
182 speedHuman: `${humanizeBytes(speedAverage)}/s`,
183 startTime: progressStartTime,
184 endTime: new Date().getTime(),
185 eta: eta,
186 etaHuman: this.secondsToHuman(eta || 0)
187 }
188 };
189 file.responseStatus = xhr.status;
190 try {
191 file.response = JSON.parse(xhr.response);
192 }
193 catch (e) {
194 file.response = xhr.response;
195 }
196 file.responseHeaders = this.parseResponseHeaders(xhr.getAllResponseHeaders());
197 observer.next({ type: 'done', file: file });
198 observer.complete();
199 }
200 };
201 xhr.open(method, url, true);
202 xhr.withCredentials = event.withCredentials ? true : false;
203 try {
204 const uploadFile = file.nativeFile;
205 const uploadIndex = this.queue.findIndex(outFile => outFile.nativeFile === uploadFile);
206 if (this.queue[uploadIndex].progress.status === UploadStatus.Cancelled) {
207 observer.complete();
208 }
209 Object.keys(headers).forEach(key => xhr.setRequestHeader(key, headers[key]));
210 let bodyToSend;
211 if (event.includeWebKitFormBoundary !== false) {
212 Object.keys(data).forEach(key => file.form.append(key, data[key]));
213 file.form.append(event.fieldName || 'file', uploadFile, uploadFile.name);
214 bodyToSend = file.form;
215 }
216 else {
217 bodyToSend = uploadFile;
218 }
219 this.serviceEvents.emit({ type: 'start', file: file });
220 xhr.send(bodyToSend);
221 }
222 catch (e) {
223 observer.complete();
224 }
225 return () => {
226 xhr.abort();
227 };
228 });
229 }
230 secondsToHuman(sec) {
231 return new Date(sec * 1000).toISOString().substr(11, 8);
232 }
233 generateId() {
234 return Math.random().toString(36).substring(7);
235 }
236 setContentTypes(contentTypes) {
237 if (typeof contentTypes !== 'undefined' && contentTypes instanceof Array) {
238 if (contentTypes.find((type) => type === '*') !== undefined) {
239 this.contentTypes = ['*'];
240 }
241 else {
242 this.contentTypes = contentTypes;
243 }
244 return;
245 }
246 this.contentTypes = ['*'];
247 }
248 allContentTypesAllowed() {
249 return this.contentTypes.find((type) => type === '*') !== undefined;
250 }
251 isContentTypeAllowed(mimetype) {
252 if (this.allContentTypesAllowed()) {
253 return true;
254 }
255 return this.contentTypes.find((type) => type === mimetype) !== undefined;
256 }
257 isFileSizeAllowed(fileSize) {
258 if (!this.maxFileSize) {
259 return true;
260 }
261 return fileSize <= this.maxFileSize;
262 }
263 makeUploadFile(file, index) {
264 return {
265 fileIndex: index,
266 id: this.generateId(),
267 name: file.name,
268 size: file.size,
269 type: file.type,
270 form: new FormData(),
271 progress: {
272 status: UploadStatus.Queue,
273 data: {
274 percentage: 0,
275 speed: 0,
276 speedHuman: `${humanizeBytes(0)}/s`,
277 startTime: null,
278 endTime: null,
279 eta: null,
280 etaHuman: null
281 }
282 },
283 lastModifiedDate: new Date(file.lastModified),
284 sub: undefined,
285 nativeFile: file
286 };
287 }
288 parseResponseHeaders(httpHeaders) {
289 if (!httpHeaders) {
290 return;
291 }
292 return httpHeaders
293 .split('\n')
294 .map((x) => x.split(/: */, 2))
295 .filter((x) => x[0])
296 .reduce((acc, x) => {
297 acc[x[0]] = x[1];
298 return acc;
299 }, {});
300 }
301}
302
303class NgFileDropDirective {
304 constructor(elementRef) {
305 this.elementRef = elementRef;
306 this.stopEvent = (e) => {
307 e.stopPropagation();
308 e.preventDefault();
309 };
310 this.uploadOutput = new EventEmitter();
311 }
312 ngOnInit() {
313 this._sub = [];
314 const concurrency = this.options && this.options.concurrency || Number.POSITIVE_INFINITY;
315 const allowedContentTypes = this.options && this.options.allowedContentTypes || ['*'];
316 const maxUploads = this.options && this.options.maxUploads || Number.POSITIVE_INFINITY;
317 const maxFileSize = this.options && this.options.maxFileSize || Number.POSITIVE_INFINITY;
318 this.upload = new NgUploaderService(concurrency, allowedContentTypes, maxUploads, maxFileSize);
319 this.el = this.elementRef.nativeElement;
320 this._sub.push(this.upload.serviceEvents.subscribe((event) => {
321 this.uploadOutput.emit(event);
322 }));
323 if (this.uploadInput instanceof EventEmitter) {
324 this._sub.push(this.upload.initInputEvents(this.uploadInput));
325 }
326 this.el.addEventListener('drop', this.stopEvent, false);
327 this.el.addEventListener('dragenter', this.stopEvent, false);
328 this.el.addEventListener('dragover', this.stopEvent, false);
329 }
330 ngOnDestroy() {
331 this._sub.forEach(sub => sub.unsubscribe());
332 }
333 onDrop(e) {
334 e.stopPropagation();
335 e.preventDefault();
336 const event = { type: 'drop' };
337 this.uploadOutput.emit(event);
338 this.upload.handleFiles(e.dataTransfer.files);
339 }
340 onDragOver(e) {
341 if (!e) {
342 return;
343 }
344 const event = { type: 'dragOver' };
345 this.uploadOutput.emit(event);
346 }
347 onDragLeave(e) {
348 if (!e) {
349 return;
350 }
351 const event = { type: 'dragOut' };
352 this.uploadOutput.emit(event);
353 }
354}
355NgFileDropDirective.decorators = [
356 { type: Directive, args: [{
357 selector: '[ngFileDrop]'
358 },] }
359];
360NgFileDropDirective.ctorParameters = () => [
361 { type: ElementRef }
362];
363NgFileDropDirective.propDecorators = {
364 options: [{ type: Input }],
365 uploadInput: [{ type: Input }],
366 uploadOutput: [{ type: Output }],
367 onDrop: [{ type: HostListener, args: ['drop', ['$event'],] }],
368 onDragOver: [{ type: HostListener, args: ['dragover', ['$event'],] }],
369 onDragLeave: [{ type: HostListener, args: ['dragleave', ['$event'],] }]
370};
371
372class NgFileSelectDirective {
373 constructor(elementRef) {
374 this.elementRef = elementRef;
375 this.fileListener = () => {
376 if (this.el.files) {
377 this.upload.handleFiles(this.el.files);
378 }
379 };
380 this.uploadOutput = new EventEmitter();
381 }
382 ngOnInit() {
383 this._sub = [];
384 const concurrency = this.options && this.options.concurrency || Number.POSITIVE_INFINITY;
385 const allowedContentTypes = this.options && this.options.allowedContentTypes || ['*'];
386 const maxUploads = this.options && this.options.maxUploads || Number.POSITIVE_INFINITY;
387 const maxFileSize = this.options && this.options.maxFileSize || Number.POSITIVE_INFINITY;
388 this.upload = new NgUploaderService(concurrency, allowedContentTypes, maxUploads, maxFileSize);
389 this.el = this.elementRef.nativeElement;
390 this.el.addEventListener('change', this.fileListener, false);
391 this._sub.push(this.upload.serviceEvents.subscribe((event) => {
392 this.uploadOutput.emit(event);
393 }));
394 if (this.uploadInput instanceof EventEmitter) {
395 this._sub.push(this.upload.initInputEvents(this.uploadInput));
396 }
397 }
398 ngOnDestroy() {
399 if (this.el) {
400 this.el.removeEventListener('change', this.fileListener, false);
401 this._sub.forEach(sub => sub.unsubscribe());
402 }
403 }
404}
405NgFileSelectDirective.decorators = [
406 { type: Directive, args: [{
407 selector: '[ngFileSelect]'
408 },] }
409];
410NgFileSelectDirective.ctorParameters = () => [
411 { type: ElementRef }
412];
413NgFileSelectDirective.propDecorators = {
414 options: [{ type: Input }],
415 uploadInput: [{ type: Input }],
416 uploadOutput: [{ type: Output }]
417};
418
419class NgxUploaderModule {
420}
421NgxUploaderModule.decorators = [
422 { type: NgModule, args: [{
423 declarations: [NgFileDropDirective, NgFileSelectDirective],
424 exports: [NgFileDropDirective, NgFileSelectDirective]
425 },] }
426];
427
428/*
429 * Public API Surface of ngx-uploader
430 */
431
432/**
433 * Generated bundle index. Do not edit.
434 */
435
436export { NgFileDropDirective, NgFileSelectDirective, NgUploaderService, NgxUploaderModule, UploadStatus, humanizeBytes };
437//# sourceMappingURL=ngx-uploader.js.map