-
Notifications
You must be signed in to change notification settings - Fork 563
Expand file tree
/
Copy pathuploader.component.ts
More file actions
330 lines (298 loc) · 11.1 KB
/
Copy pathuploader.component.ts
File metadata and controls
330 lines (298 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
import { CommonModule } from '@angular/common';
import { HttpXsrfTokenExtractor } from '@angular/common/http';
import {
AfterViewInit,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnInit,
Output,
ViewEncapsulation,
} from '@angular/core';
import { CookieService } from '@dspace/core/cookies/cookie.service';
import { DragService } from '@dspace/core/drag.service';
import {
XSRF_COOKIE,
XSRF_REQUEST_HEADER,
XSRF_RESPONSE_HEADER,
} from '@dspace/core/xsrf/xsrf.constants';
import {
hasValue,
isNotEmpty,
isUndefined,
} from '@dspace/shared/utils/empty.util';
import { TranslateModule } from '@ngx-translate/core';
import uniqueId from 'lodash/uniqueId';
import {
FileItem,
FileUploader,
FileUploadModule,
} from 'ng2-file-upload';
import { of } from 'rxjs';
import { BtnDisabledDirective } from '../../btn-disabled.directive';
import { LiveRegionService } from '../../live-region/live-region.service';
import { UploaderCompleteEvent } from './uploader-complete-event.model';
import { UploaderError } from './uploader-error.model';
import { UploaderOptions } from './uploader-options.model';
import { UploaderProperties } from './uploader-properties.model';
@Component({
selector: 'ds-uploader',
templateUrl: 'uploader.component.html',
styleUrls: ['uploader.component.scss'],
changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated,
imports: [
BtnDisabledDirective,
CommonModule,
FileUploadModule,
TranslateModule,
],
})
export class UploaderComponent implements OnInit, AfterViewInit {
/**
* Header key to impersonate a user
*/
private readonly ON_BEHALF_HEADER = 'X-On-Behalf-Of';
/**
* The message to show when drag files on the drop zone
*/
@Input() dropMsg: string;
/**
* The message to show when drag files on the window document
*/
@Input() dropOverDocumentMsg: string;
/**
* The message to show when drag files on the window document
*/
@Input() enableDragOverDocument: boolean;
/**
* The function to call before an upload
*/
@Input() onBeforeUpload: () => void;
/**
* Configuration for the ng2-file-upload component.
*/
@Input() uploadFilesOptions: UploaderOptions;
/**
* Extra properties to be passed with the form-data of the upload
*/
@Input() uploadProperties: UploaderProperties;
/**
* The aria label to describe what kind of files need to be uploaded
*/
@Input() ariaLabel: string;
/**
* Component that defines the area in which `dragOver` events are processed
*/
@Input() dragoverContainer = 'ds-app';
/**
* The function to call when upload is completed
*/
@Output() onCompleteItem: EventEmitter<any> = new EventEmitter<any>();
/**
* The function to call when upload is completed, carrying the parsed response together with the
* client-side file name. Emitted alongside {@link onCompleteItem} so existing consumers are unaffected.
*/
@Output() onCompleteItemWithFile: EventEmitter<UploaderCompleteEvent> = new EventEmitter<UploaderCompleteEvent>();
/**
* The function to call on error occurred
*/
@Output() onUploadError: EventEmitter<UploaderError> = new EventEmitter<UploaderError>();
/**
* The function to call when a file is selected
*/
@Output() onFileSelected: EventEmitter<any> = new EventEmitter<any>();
public uploader: FileUploader;
public uploaderId: string;
public isOverBaseDropZone = of(false);
public isOverDocumentDropZone = of(false);
/**
* Set of progress values that have been announced to screen readers
*/
private announcedProgress: Set<number> = new Set();
/**
* The uuid of the last progress message announced to screen readers
* @private
*/
private lastProgressMessageUuid: string;
@HostListener('window:dragover', ['$event'])
onDragOver(event: any) {
if (hasValue(this.dragoverContainer)) {
if (!event.target.closest(this.dragoverContainer)) {
return;
}
}
if (this.enableDragOverDocument && this.dragService.isAllowedDragOverPage()) {
// Only show drop area when dragging files or event is manually triggered
const hasFiles = event.dataTransfer?.types ? Array.from(event.dataTransfer.types).includes('Files') : true;
if (!hasFiles) {
return;
}
// Show drop area on the page
event.preventDefault();
if ((event.target as any).tagName !== 'HTML') {
this.isOverDocumentDropZone = of(true);
}
}
}
constructor(
private cdr: ChangeDetectorRef,
private dragService: DragService,
private tokenExtractor: HttpXsrfTokenExtractor,
private cookieService: CookieService,
private liveRegionService: LiveRegionService,
) {
}
/**
* Method provided by Angular. Invoked after the constructor.
*/
ngOnInit(): void {
this.uploaderId = 'ds-drag-and-drop-uploader' + uniqueId();
this.checkConfig(this.uploadFilesOptions);
this.uploader = new FileUploader({
url: this.uploadFilesOptions.url,
authToken: this.uploadFilesOptions.authToken,
disableMultipart: this.uploadFilesOptions.disableMultipart,
itemAlias: this.uploadFilesOptions.itemAlias,
removeAfterUpload: true,
autoUpload: this.uploadFilesOptions.autoUpload,
method: this.uploadFilesOptions.method,
queueLimit: this.uploadFilesOptions.maxFileNumber,
});
if (isUndefined(this.enableDragOverDocument)) {
this.enableDragOverDocument = false;
}
if (isUndefined(this.dropMsg)) {
this.dropMsg = 'uploader.drag-message';
}
if (isUndefined(this.dropOverDocumentMsg)) {
this.dropOverDocumentMsg = 'uploader.drag-message';
}
}
ngAfterViewInit(): void {
this.uploader.onAfterAddingAll = ((items) => {
this.onFileSelected.emit(items);
});
if (isUndefined(this.onBeforeUpload)) {
this.onBeforeUpload = () => {return;};
}
this.uploader.onBeforeUploadItem = (item) => {
if (item.url !== this.uploader.options.url) {
item.url = this.uploader.options.url;
}
// Ensure the current XSRF token is included in every upload request (token may change between items uploaded)
// Ensure the behalf header is set if impersonating
this.uploader.options.headers = [
{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() },
];
if (hasValue(this.uploadFilesOptions.impersonatingID)) {
this.uploader.options.headers.push({ name: this.ON_BEHALF_HEADER, value: this.uploadFilesOptions.impersonatingID });
}
this.onBeforeUpload();
this.isOverDocumentDropZone = of(false);
};
if (hasValue(this.uploadProperties)) {
this.uploader.onBuildItemForm = (item, form) => {
form.append('properties', JSON.stringify(this.uploadProperties));
};
}
this.uploader.onCompleteItem = (item: any, response: any, status: any, headers: any) => {
// Check for a changed XSRF token in response & save new token if found (to both cookie & header for next request)
// NOTE: this is only necessary because ng2-file-upload doesn't use an Http service and therefore never
// triggers our xsrf.interceptor.ts. See this bug: https://github.com/valor-software/ng2-file-upload/issues/950
const token = headers[XSRF_RESPONSE_HEADER.toLowerCase()];
if (isNotEmpty(token)) {
this.saveXsrfToken(token);
this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }];
}
if (isNotEmpty(response)) {
const responsePath = JSON.parse(response);
this.onCompleteItem.emit(responsePath);
const fileName = item?.file?.name;
this.onCompleteItemWithFile.emit(isNotEmpty(fileName) ? { response: responsePath, fileName } : { response: responsePath });
}
};
this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => {
// Check for a changed XSRF token in response & save new token if found (to both cookie & header for next request)
// NOTE: this is only necessary because ng2-file-upload doesn't use an Http service and therefore never
// triggers our xsrf.interceptor.ts. See this bug: https://github.com/valor-software/ng2-file-upload/issues/950
const token = headers[XSRF_RESPONSE_HEADER.toLowerCase()];
if (isNotEmpty(token)) {
this.saveXsrfToken(token);
this.uploader.options.headers = [{ name: XSRF_REQUEST_HEADER, value: this.tokenExtractor.getToken() }];
}
this.onUploadError.emit({ item: item, response: response, status: status, headers: headers });
this.uploader.cancelAll();
};
this.uploader.onProgressAll = () => this.onProgress();
// Live region service setup
this.liveRegionService.setMessageTimeOutMs(1500);
this.liveRegionService.clear();
this.uploader.onProgressItem = (fileItem: FileItem, progress: any) => {
this.announceProgress(progress);
this.onProgress();
};
}
/**
* Announce the progress of the upload to screen readers
* @param progress
*/
private announceProgress(progress: any) {
if (!this.announcedProgress.has(progress)) {
this.announcedProgress.add(progress);
const message = progress + '%';
if (this.lastProgressMessageUuid) {
this.liveRegionService.clearMessageByUUID(this.lastProgressMessageUuid);
}
this.lastProgressMessageUuid = this.liveRegionService.addMessage(message);
}
}
/**
* Called when files are dragged on the base drop area.
*/
public fileOverBase(isOver: boolean): void {
this.isOverBaseDropZone = of(isOver);
}
/**
* Called when files are dragged on the window document drop area.
*/
public fileOverDocument(isOver: boolean) {
if (!isOver) {
this.isOverDocumentDropZone = of(isOver);
}
}
private onProgress() {
this.cdr.detectChanges();
}
/**
* Ensure options passed contains the required properties.
*
* @param fileUploadOptions
* The upload-files options object.
*/
private checkConfig(fileUploadOptions: any) {
const required = ['url', 'authToken', 'disableMultipart', 'itemAlias'];
const missing = required.filter((prop) => {
return !((prop in fileUploadOptions) && fileUploadOptions[prop] !== '');
});
if (0 < missing.length) {
throw new Error('UploadFiles: Argument is missing the following required properties: ' + missing.join(', '));
}
}
/**
* Save XSRF token found in response. This is a temporary copy of the method in xsrf.interceptor.ts
* It can be removed once ng2-file-upload supports interceptors (see https://github.com/valor-software/ng2-file-upload/issues/950),
* or we switch to a new upload library (see https://github.com/DSpace/dspace-angular/issues/820)
* @param token token found
*/
private saveXsrfToken(token: string) {
// Save token value as a *new* value of our client-side XSRF-TOKEN cookie.
// This is the cookie that is parsed by Angular's tokenExtractor(),
// which we will send back in the X-XSRF-TOKEN header per Angular best practices.
this.cookieService.remove(XSRF_COOKIE);
this.cookieService.set(XSRF_COOKIE, token);
}
}