@progress/kendo-angular-upload
Version:
Kendo UI Angular Upload Component
1,586 lines (1,567 loc) • 201 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import * as i1 from '@angular/common/http';
import { HttpHeaders, HttpRequest, HttpEventType, HttpResponse } from '@angular/common/http';
import * as i0 from '@angular/core';
import { EventEmitter, Injectable, Directive, ElementRef, ContentChild, ViewChild, Input, HostBinding, Output, Component, HostListener, ViewChildren, Inject, forwardRef, isDevMode, NgModule } from '@angular/core';
import { guid, Keys, isControlRequired, isChanged, isDocumentAvailable, KendoInput, ResizeBatchService } from '@progress/kendo-angular-common';
import { fileAudioIcon, fileVideoIcon, fileImageIcon, fileTxtIcon, filePresentationIcon, fileDataIcon, fileProgrammingIcon, filePdfIcon, fileConfigIcon, fileZipIcon, fileDiscImageIcon, arrowRotateCwSmallIcon, playSmIcon, pauseSmIcon, cancelIcon, xIcon, copyIcon, fileIcon, checkIcon, exclamationCircleIcon, uploadIcon } from '@progress/kendo-svg-icons';
import { NgControl, NG_VALUE_ACCESSOR } from '@angular/forms';
import * as i1$1 from '@progress/kendo-angular-l10n';
import { ComponentMessages, LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { fromEvent, merge } from 'rxjs';
import { filter } from 'rxjs/operators';
import { validatePackage } from '@progress/kendo-licensing';
import { trigger, state, style, transition, animate } from '@angular/animations';
import { ButtonComponent } from '@progress/kendo-angular-buttons';
import { NgIf, NgFor, NgClass, NgTemplateOutlet } from '@angular/common';
import { IconWrapperComponent, IconsService } from '@progress/kendo-angular-icons';
import { ProgressBarComponent } from '@progress/kendo-angular-progressbar';
import { PopupService } from '@progress/kendo-angular-popup';
/**
* Lists the possible states of a file.
*/
var FileState;
(function (FileState) {
/**
* The file upload process has failed.
*/
FileState[FileState["Failed"] = 0] = "Failed";
/**
* An initially selected fake file without a set state.
*/
FileState[FileState["Initial"] = 1] = "Initial";
/**
* The file is selected.
*/
FileState[FileState["Selected"] = 2] = "Selected";
/**
* The file is successfully uploaded.
*/
FileState[FileState["Uploaded"] = 3] = "Uploaded";
/**
* The file is in the process of uploading.
*/
FileState[FileState["Uploading"] = 4] = "Uploading";
/**
* The file upload process has been paused.
*/
FileState[FileState["Paused"] = 5] = "Paused";
})(FileState || (FileState = {}));
/**
* @hidden
*/
class FileMap {
_files;
constructor() {
this._files = {};
}
add(file) {
const uid = file.uid;
if (this.has(uid)) {
if (file.validationErrors && file.validationErrors.length > 0) {
this._files[uid].unshift(file);
}
else {
this._files[uid].push(file);
}
}
else {
this._files[uid] = [file];
}
}
remove(uid) {
if (this.has(uid)) {
this._files[uid] = null;
delete this._files[uid];
}
}
clear() {
const allFiles = this._files;
for (const uid in allFiles) {
if (allFiles.hasOwnProperty(uid)) {
for (const file of allFiles[uid]) {
if (file.httpSubscription) {
file.httpSubscription.unsubscribe();
}
}
allFiles[uid] = null;
delete allFiles[uid];
}
}
}
has(uid) {
return uid in this._files;
}
get(uid) {
return this._files[uid];
}
setFilesState(files, state) {
for (const file of files) {
this.setFilesStateByUid(file.uid, state);
}
}
setFilesStateByUid(uid, state) {
this.get(uid).forEach((f) => {
f.state = state;
});
}
get count() {
return Object.getOwnPropertyNames(this._files).length;
}
get files() {
const initial = this._files;
const transformed = [];
for (const uid in initial) {
if (initial.hasOwnProperty(uid)) {
transformed.push(initial[uid]);
}
}
return transformed;
}
get filesFlat() {
const initial = this._files;
const transformed = [];
for (const uid in initial) {
if (initial.hasOwnProperty(uid)) {
const current = initial[uid];
current.forEach((file) => {
transformed.push(file);
});
}
}
return transformed;
}
get filesToUpload() {
const files = this._files;
const notUploaded = [];
for (const uid in files) {
if (files.hasOwnProperty(uid)) {
const currentFiles = files[uid];
let currentFilesValid = true;
for (const file of currentFiles) {
if (file.state !== FileState.Selected || (file.validationErrors && file.validationErrors.length > 0)) {
currentFilesValid = false;
}
}
if (currentFilesValid) {
notUploaded.push(currentFiles);
}
}
}
return notUploaded;
}
get firstFileToUpload() {
const files = this._files;
for (const uid in files) {
if (files.hasOwnProperty(uid)) {
const currentFiles = files[uid];
let currentFilesValid = true;
for (const file of currentFiles) {
if (file.state !== FileState.Selected || (file.validationErrors && file.validationErrors.length > 0)) {
currentFilesValid = false;
}
}
if (currentFilesValid) {
return currentFiles;
}
}
}
return null;
}
getFilesWithState(state) {
return this.filesFlat.filter(file => file.state === state);
}
hasFileWithState(fileStates) {
const files = this._files;
for (const uid in files) {
if (files.hasOwnProperty(uid)) {
const currentFiles = files[uid];
for (const file of currentFiles) {
if (fileStates.indexOf(file.state) >= 0) {
return true;
}
}
}
}
return false;
}
}
/**
* Arguments for the `cancel` event. The `cancel` event fires when
* the user cancels the process of uploading a file or a batch of files.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <p>Click the <span class='k-icon k-font-icon k-i-cancel'></span> icon during upload to trigger the event</p>
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (cancel)="cancelEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* cancelEventHandler(e: CancelEvent) {
* console.log('Canceling file upload', e.files);
* }
* }
* ```
*/
class CancelEvent {
/**
* The list of the files that were going to be uploaded.
*/
files;
/**
* @hidden
* Constructs the event arguments for the `cancel` event.
* @param files - The list of the files that were going to be uploaded.
*/
constructor(files) {
this.files = files;
}
}
/**
* @hidden
*/
class PreventableEvent {
prevented = false;
/**
* Prevents the default action for a specified event.
* In this way, the source component suppresses the built-in behavior that follows the event.
*/
preventDefault() {
this.prevented = true;
}
/**
* If the event is prevented by any of its subscribers, returns `true`.
*
* @returns `true` if the default action was prevented. Otherwise, returns `false`.
*/
isDefaultPrevented() {
return this.prevented;
}
}
/**
* Arguments for the `clear` event. The `clear` event fires when
* the **Clear** button is clicked. At this point, the selected files are about to be cleared.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [autoUpload]="false"
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (clear)="clearEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* clearEventHandler(e: ClearEvent) {
* console.log('Clearing the file upload');
* }
* }
* ```
*/
class ClearEvent extends PreventableEvent {
/**
* @hidden
* Constructs the event arguments for the `clear` event.
*/
constructor() { super(); }
}
/**
* Arguments for the `error` event. The `error` event fires when
* an `upload` or `remove` operation fails.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (error)="errorEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* errorEventHandler(e: ErrorEvent) {
* console.log('An error occurred');
* }
* }
* ```
*/
class ErrorEvent {
/**
* The list of the files that failed to be uploaded or removed.
*/
files;
/**
* The operation type (`upload` or `remove`).
*/
operation;
/**
* The response object returned by the server.
*/
response;
/**
* @hidden
* Constructs the event arguments for the `error` event.
*
* @param files - The list of the files that failed to be uploaded or removed.
* @param operation - The operation type (`upload` or `remove`).
* @param response - The response object returned by the server.
*/
constructor(files, operation, response) {
this.files = files;
this.operation = operation;
this.response = response;
}
}
/**
* Arguments for the `pause` event. The `pause` event fires when the user
* pauses a file that is currently uploading.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [chunkable]="true"
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (pause)="pauseEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* pauseEventHandler(ev: PauseEvent) {
* console.log('File paused');
* }
* }
* ```
*
*/
class PauseEvent {
/**
* The file that is going to be paused.
*/
file;
/**
* @hidden
* Constructs the event arguments for the `pause` event.
* @param file - The file that is going to be paused.
*/
constructor(file) {
this.file = file;
}
}
/**
* Arguments for the `remove` event. The `remove` event fires when an uploaded
* or selected file is about to be removed. If you cancel the event, the removal is prevented.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (remove)="removeEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* removeEventHandler(e: RemoveEvent) {
* console.log('Removing a file');
* }
* }
* ```
*/
class RemoveEvent extends PreventableEvent {
/**
* An optional object that is sent to the `remove` handler in the form of a key/value pair.
*/
data;
/**
* The list of the files that will be removed.
*/
files;
/**
* The headers of the request.
*/
headers;
/**
* @hidden
* Constructs the event arguments for the `remove` event.
* @param files - The list of the files that will be removed.
* @param headers - The headers of the request.
*/
constructor(files, headers) {
super();
this.files = files;
this.headers = headers;
}
}
/**
* Arguments for the `resume` event. The `resume` event fires when the user
* resumes the upload of a file that has been previously paused.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [chunkable]="true"
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (resume)="resumeEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* resumeEventHandler(ev: ResumeEvent) {
* console.log('File resumed');
* }
* }
* ```
*
*/
class ResumeEvent {
/**
* The file that is going to be resumed.
*/
file;
/**
* @hidden
* Constructs the event arguments for the `resume` event.
* @param file - The file that is going to be resumed.
*/
constructor(file) {
this.file = file;
}
}
/**
* Arguments for the `select` event. The `select` event fires when the user
* selects a file or multiple files for upload. If you cancel the event, the selection is prevented.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (select)="selectEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* selectEventHandler(e: SelectEvent) {
* console.log('File selected');
* }
* }
* ```
*/
class SelectEvent extends PreventableEvent {
/**
* The list of the selected files.
*/
files;
/**
* @hidden
* Constructs the event arguments for the `select` event.
* @param files - The list of the selected files.
*/
constructor(files) {
super();
this.files = files;
}
}
/**
* Arguments for the `success` event. The `success` event fires when
* the selected files are successfully uploaded or removed.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (success)="successEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* successEventHandler(e: SuccessEvent) {
* console.log('The ' + e.operation + ' was successful!');
* }
* }
* ```
*/
class SuccessEvent extends PreventableEvent {
/**
* The list of the files that were uploaded or removed.
*/
files;
/**
* The operation type (`upload` or `remove`).
*/
operation;
/**
* The response object returned by the server.
*/
response;
/**
* @hidden
* Constructs the event arguments for the `success` event.
* @param files - The list of the files that were uploaded or removed.
* @param operation - The operation type (`upload` or `remove`).
* @param response - The response object returned by the server.
*/
constructor(files, operation, response) {
super();
this.files = files;
this.operation = operation;
this.response = response;
}
}
/**
* Arguments for the `upload` event. The `upload` event fires when one or more files are about
* to be uploaded. If you cancel the event, the upload is prevented. You can add headers to the request.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (upload)="uploadEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* uploadEventHandler(e: UploadEvent) {
* e.headers = e.headers.append('X-Foo', 'Bar');
* }
* }
* ```
*/
class UploadEvent extends PreventableEvent {
/**
* The optional object that is sent to the `upload` handler in the form of key/value pair.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (upload)="uploadEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* uploadEventHandler(e: UploadEvent) {
* e.data = {
* description: 'File description'
* };
* }
* }
* ```
*/
data;
/**
* The list of the files that will be uploaded.
*/
files;
/**
* The headers of the request.
*/
headers;
/**
* @hidden
* Constructs the event arguments for the `upload` event.
* @param files - The list of the files that will be uploaded.
* @param headers - The headers of the request.
*/
constructor(files, headers) {
super();
this.files = files;
this.headers = headers;
}
}
/**
* Arguments for the `uploadprogress` event. The `uploadprogress` event
* fires when the files are in the process of uploading.
*
* ```ts-no-run
* @Component({
* selector: 'my-upload',
* template: `
* <kendo-upload
* [saveUrl]="uploadSaveUrl"
* [removeUrl]="uploadRemoveUrl"
* (uploadProgress)="uploadProgressEventHandler($event)">
* </kendo-upload>
* `
* })
* export class UploadComponent {
* uploadSaveUrl = 'saveUrl'; // should represent an actual API endpoint
* uploadRemoveUrl = 'removeUrl'; // should represent an actual API endpoint
*
* uploadProgressEventHandler(e: UploadProgressEvent) {
* console.log(e.files[0].name + ' is ' + e.percentComplete + ' uploaded');
* }
* }
* ```
*/
class UploadProgressEvent {
/**
* The list of files that are being uploaded.
*/
files;
/**
* The portion that has been uploaded.
*/
percentComplete;
/**
* @hidden
* Constructs the event arguments for the `uploadprogress` event.
* @param files - The list of files that are being uploaded.
* @param percentComplete - The portion that has been uploaded.
*/
constructor(files, percentComplete) {
this.files = files;
this.percentComplete = percentComplete;
}
}
/**
* @hidden
*/
const fileGroupMap = {
audio: [
".aif", ".iff", ".m3u", ".m4a", ".mid", ".mp3", ".mpa", ".wav", ".wma", ".ogg", ".wav", ".wma", ".wpl"
],
video: [
".3g2", ".3gp", ".avi", ".asf", ".flv", ".m4u", ".rm", ".h264", ".m4v", ".mkv", ".mov", ".mp4", ".mpg", ".rm", ".swf", ".vob", ".wmv"
],
image: [
".ai", ".dds", ".heic", ".jpe", "jfif", ".jif", ".jp2", ".jps", ".eps", ".bmp", ".gif", ".jpeg", ".jpg", ".png", ".ps", ".psd", ".svg", ".svgz", ".tif", ".tiff"
],
txt: [
".doc", ".docx", ".log", ".pages", ".tex", ".wpd", ".wps", ".odt", ".rtf", ".text", ".txt", ".wks"
],
presentation: [
".key", ".odp", ".pps", ".ppt", ".pptx"
],
data: [
".xlr", ".xls", ".xlsx"
],
programming: [
".tmp", ".bak", ".msi", ".cab", ".cpl", ".cur", ".dll", ".dmp", ".drv", ".icns", ".ico", ".link", ".sys", ".cfg",
".ini", ".asp", ".aspx", ".cer", ".csr", ".css", ".dcr", ".htm", ".html", ".js", ".php", ".rss", ".xhtml"
],
pdf: [
".pdf"
],
config: [
".apk", ".app", ".bat", ".cgi", ".com", ".exe", ".gadget", ".jar", ".wsf"
],
zip: [
".7z", ".cbr", ".gz", ".sitx", ".arj", ".deb", ".pkg", ".rar", ".rpm", ".tar.gz", ".z", ".zip", ".zipx"
],
discImage: [
".dmg", ".iso", ".toast", ".vcd", ".bin", ".cue", ".mdf"
]
};
/**
* @hidden
*/
const fileSVGGroupMap = {
audio: fileAudioIcon,
video: fileVideoIcon,
image: fileImageIcon,
txt: fileTxtIcon,
presentation: filePresentationIcon,
data: fileDataIcon,
programming: fileProgrammingIcon,
pdf: filePdfIcon,
config: fileConfigIcon,
zip: fileZipIcon,
discImage: fileDiscImageIcon
};
/* eslint-disable no-bitwise */
/**
* @hidden
*/
const getTotalFilesSizeMessage = (files) => {
let totalSize = 0;
let i;
if (typeof files[0].size === "number") {
for (i = 0; i < files.length; i++) {
if (files[i].size) {
totalSize += files[i].size;
}
}
}
else {
return "";
}
totalSize /= 1024;
if (totalSize < 1024) {
return totalSize.toFixed(2) + " KB";
}
else {
return (totalSize / 1024).toFixed(2) + " MB";
}
};
const stripPath = (name) => {
const slashIndex = name.lastIndexOf("\\");
return (slashIndex !== -1) ? name.substr(slashIndex + 1) : name;
};
const getFileExtension = (fileName) => {
const rFileExtension = /\.([^\.]+)$/;
const matches = fileName.match(rFileExtension);
return matches ? matches[0] : "";
};
/**
* @hidden
*/
const validateInitialFileInfo = (file) => {
if (file instanceof Object && file.hasOwnProperty("name")) {
return true;
}
return false;
};
/**
* @hidden
*/
const validateInitialFileSelectFile = (file) => {
if (file instanceof File || validateInitialFileInfo(file)) {
return true;
}
return false;
};
/**
* @hidden
*/
const getInitialFileInfo = (fakeFile) => {
fakeFile.extension = fakeFile.extension || getFileExtension(fakeFile.name);
fakeFile.name = fakeFile.name; // eslint-disable-line no-self-assign
fakeFile.size = fakeFile.size || 0;
if (!fakeFile.hasOwnProperty("state")) {
fakeFile.state = FileState.Initial;
}
if (!fakeFile.hasOwnProperty("uid")) {
fakeFile.uid = guid();
}
return fakeFile;
};
/**
* @hidden
*/
const convertFileToFileInfo = (file) => {
const fileInfo = getFileInfo(file);
fileInfo.uid = guid();
// Used to differentiate initial FileInfo objects and actual Files
fileInfo.state = FileState.Selected;
return fileInfo;
};
const getFileInfo = (rawFile) => {
const fileName = rawFile.name;
const fileSize = rawFile.size;
return {
extension: getFileExtension(fileName),
name: fileName,
rawFile: rawFile,
size: fileSize,
state: FileState.Selected
};
};
/**
* @hidden
*/
const getAllFileInfo = (rawFiles) => {
const allFileInfo = new Array();
let i;
for (i = 0; i < rawFiles.length; i++) {
allFileInfo.push(getFileInfo(rawFiles[i]));
}
return allFileInfo;
};
/**
* @hidden
*/
const fileHasValidationErrors = (file) => {
if (file.validationErrors && file.validationErrors.length > 0) {
return true;
}
return false;
};
/**
* @hidden
*/
const filesHaveValidationErrors = (files) => {
for (const file of files) {
if (fileHasValidationErrors(file)) {
return true;
}
}
return false;
};
/**
* @hidden
*/
const inputFiles = (input) => {
if (input.files) {
return getAllFileInfo(input.files);
}
else {
//Required for testing
const fileNames = input.value.split("|").map((file, index) => {
const fileName = file.trim();
return {
extension: getFileExtension(fileName),
name: stripPath(fileName),
rawFile: null,
size: (index + 1) * 1000,
state: FileState.Selected
};
});
return fileNames;
}
};
/**
* @hidden
*/
const assignGuidToFiles = (files, isUnique) => {
const uid = guid();
return files.map((file) => {
file.uid = isUnique ? guid() : uid;
return file;
});
};
/**
* @hidden
*/
const supportsFormData = () => {
return typeof (FormData) !== "undefined";
};
/**
* @hidden
*/
const userAgent = () => {
return navigator.userAgent;
};
const focusableRegex = /^(?:a|input|select|textarea|button|object)$/i;
/**
* @hidden
*/
const IGNORE_TARGET_CLASSES = 'k-icon k-select k-input k-multiselect-wrap';
/**
* @hidden
*/
const UPLOAD_CLASSES = 'k-upload-button k-clear-selected k-upload-selected k-upload-action k-file';
const isVisible = (element) => {
const rect = element.getBoundingClientRect();
return !!(rect.width && rect.height) && window.getComputedStyle(element).visibility !== 'hidden';
};
const toClassList = (classNames) => String(classNames).trim().split(' ');
/**
* @hidden
*/
const hasClasses = (element, classNames) => {
const namesList = toClassList(classNames);
return Boolean(toClassList(element.className).find((className) => namesList.indexOf(className) >= 0));
};
/**
* @hidden
*/
const isFocusable = (element, checkVisibility = true) => {
if (element.tagName) {
const tagName = element.tagName.toLowerCase();
const tabIndex = element.getAttribute('tabIndex');
const validTabIndex = tabIndex !== null && !isNaN(tabIndex) && tabIndex > -1;
let focusable = false;
if (focusableRegex.test(tagName)) {
focusable = !element.disabled;
}
else {
focusable = validTabIndex;
}
return focusable && (!checkVisibility || isVisible(element));
}
return false;
};
/**
* @hidden
*/
const getFileGroupCssClass = (fileExtension) => {
const initial = 'file';
for (const group in fileGroupMap) {
if (fileGroupMap[group].indexOf(fileExtension) >= 0) {
if (group === 'discImage') {
return `${initial}-disc-image`;
}
return `${initial}-${group}`;
}
}
return initial;
};
/**
* @hidden
*/
const isPresent = (value) => value !== null && value !== undefined;
/**
* @hidden
*/
class ChunkMap {
_files;
constructor() {
this._files = {};
}
add(uid, totalChunks) {
const initialChunkInfo = {
index: 0,
position: 0,
retries: 0,
totalChunks: totalChunks
};
this._files[uid] = initialChunkInfo;
return initialChunkInfo;
}
remove(uid) {
if (this.has(uid)) {
this._files[uid] = null;
delete this._files[uid];
}
}
has(uid) {
return uid in this._files;
}
get(uid) {
return this._files[uid];
}
}
/**
* @hidden
*/
class UploadService {
http;
cancelEvent = new EventEmitter();
clearEvent = new EventEmitter();
completeEvent = new EventEmitter();
errorEvent = new EventEmitter();
pauseEvent = new EventEmitter();
removeEvent = new EventEmitter();
resumeEvent = new EventEmitter();
selectEvent = new EventEmitter();
successEvent = new EventEmitter();
uploadEvent = new EventEmitter();
uploadProgressEvent = new EventEmitter();
/**
* Required for the `ControlValueAccessor` integration
*/
changeEvent = new EventEmitter();
/**
* Default async settings
*/
async = {
autoUpload: true,
batch: false,
chunk: false,
concurrent: true,
removeField: "fileNames",
removeHeaders: new HttpHeaders(),
removeMethod: "POST",
removeUrl: "",
responseType: "json",
saveField: "files",
saveHeaders: new HttpHeaders(),
saveMethod: "POST",
saveUrl: "",
withCredentials: true
};
/**
* Default chunk settings
*/
chunk = {
autoRetryAfter: 100,
size: 1024 * 1024,
maxAutoRetries: 1,
resumable: true
};
component = 'Upload';
chunkMap = new ChunkMap();
fileList = new FileMap();
constructor(http) {
this.http = http;
}
get files() {
return this.fileList;
}
setChunkSettings(settings) {
if (settings !== false) {
this.async.chunk = true;
if (typeof settings === "object") {
this.chunk = Object.assign({}, this.chunk, settings);
}
}
}
onChange() {
const files = this.fileList.filesFlat.filter((file) => {
return file.state === FileState.Initial ||
file.state === FileState.Uploaded;
});
this.changeEvent.emit(files.length > 0 ? files : null);
}
addFiles(files) {
const selectEventArgs = new SelectEvent(files);
this.selectEvent.emit(selectEventArgs);
if (!selectEventArgs.isDefaultPrevented()) {
for (const file of files) {
this.fileList.add(file);
}
if (this.async.autoUpload) {
this.uploadFiles();
}
}
if (this.component === 'FileSelect') {
const flatFiles = this.fileList.filesFlat;
this.changeEvent.emit(flatFiles.length > 0 ? flatFiles : null);
}
}
addInitialFiles(initialFiles) {
this.fileList.clear();
initialFiles.forEach((file) => {
const fakeFile = getInitialFileInfo(file);
this.fileList.add(fakeFile);
});
}
addInitialFileSelectFiles(initialFiles) {
this.fileList.clear();
initialFiles.forEach((file) => {
if (file instanceof File) {
this.fileList.add(convertFileToFileInfo(file));
}
else {
this.fileList.add(getInitialFileInfo(file));
}
});
}
resumeFile(uid) {
const fileToResume = this.fileList.get(uid);
this.resumeEvent.emit(new ResumeEvent(fileToResume[0]));
this.fileList.setFilesStateByUid(uid, FileState.Uploading);
this._uploadFiles([fileToResume]);
}
pauseFile(uid) {
const pausedFile = this.fileList.get(uid)[0];
this.pauseEvent.emit(new PauseEvent(pausedFile));
this.fileList.setFilesStateByUid(uid, FileState.Paused);
}
removeFiles(uid) {
const removedFiles = this.fileList.get(uid);
// Clone the Headers so that the default ones are not overridden
const removeEventArgs = new RemoveEvent(removedFiles, this.cloneRequestHeaders(this.async.removeHeaders));
this.removeEvent.emit(removeEventArgs);
if (!removeEventArgs.isDefaultPrevented()) {
if (this.component === 'Upload' &&
(removedFiles[0].state === FileState.Uploaded ||
removedFiles[0].state === FileState.Initial)) {
this.performRemove(removedFiles, removeEventArgs);
}
else {
this.fileList.remove(uid);
if (this.component === 'FileSelect') {
const flatFiles = this.fileList.filesFlat;
this.changeEvent.emit(flatFiles.length > 0 ? flatFiles : null);
}
}
}
}
cancelFiles(uid) {
const canceledFiles = this.fileList.get(uid);
const cancelEventArgs = new CancelEvent(canceledFiles);
this.cancelEvent.emit(cancelEventArgs);
for (const file of canceledFiles) {
if (file.httpSubscription) {
file.httpSubscription.unsubscribe();
}
}
this.fileList.remove(uid);
this.checkAllComplete();
}
clearFiles() {
const clearEventArgs = new ClearEvent();
this.clearEvent.emit(clearEventArgs);
if (!clearEventArgs.isDefaultPrevented()) {
const triggerChange = this.fileList.hasFileWithState([
FileState.Initial,
FileState.Uploaded
]);
this.fileList.clear();
if (triggerChange) {
this.onChange();
}
}
}
uploadFiles() {
let filesToUpload = [];
if (this.async.concurrent) {
filesToUpload = this.fileList.filesToUpload;
}
if (!this.async.concurrent && !this.fileList.hasFileWithState([FileState.Uploading])) {
filesToUpload = this.fileList.firstFileToUpload ? [this.fileList.firstFileToUpload] : [];
}
if (filesToUpload && filesToUpload.length > 0) {
this._uploadFiles(filesToUpload);
}
}
retryFiles(uid) {
const filesToRetry = [this.fileList.get(uid)];
if (filesToRetry) {
this._uploadFiles(filesToRetry);
}
}
_uploadFiles(allFiles) {
for (const filesToUpload of allFiles) {
if (filesToUpload[0].state === FileState.Paused) {
return;
}
// Clone the Headers so that the default ones are not overridden
const uploadEventArgs = new UploadEvent(filesToUpload, this.cloneRequestHeaders(this.async.saveHeaders));
this.uploadEvent.emit(uploadEventArgs);
if (!uploadEventArgs.isDefaultPrevented()) {
this.fileList.setFilesState(filesToUpload, FileState.Uploading);
const httpSubcription = this.performUpload(filesToUpload, uploadEventArgs);
filesToUpload.forEach((file) => {
file.httpSubscription = httpSubcription;
});
}
else {
this.fileList.remove(filesToUpload[0].uid);
this.checkAllComplete();
}
}
}
performRemove(files, removeEventArgs) {
const async = this.async;
const fileNames = files.map((file) => {
return file.name;
});
const formData = this.populateRemoveFormData(fileNames, removeEventArgs.data);
const options = this.populateRequestOptions(removeEventArgs.headers, false);
const removeRequest = new HttpRequest(async.removeMethod, async.removeUrl, formData, options);
this.http.request(removeRequest)
.subscribe(success => {
this.onSuccess(success, files, "remove");
}, error => {
this.onError(error, files, "remove");
});
}
performUpload(files, uploadEventArgs) {
const async = this.async;
const formData = this.populateUploadFormData(files, uploadEventArgs.data);
const options = this.populateRequestOptions(uploadEventArgs.headers);
const uploadRequest = new HttpRequest(async.saveMethod, async.saveUrl, formData, options);
const httpSubscription = this.http.request(uploadRequest)
.subscribe(event => {
if (event.type === HttpEventType.UploadProgress && !this.async.chunk) {
this.onProgress(event, files);
}
else if (event instanceof HttpResponse) {
this.onSuccess(event, files, "upload");
this.checkAllComplete();
}
}, error => {
this.onError(error, files, "upload");
this.checkAllComplete();
});
return httpSubscription;
}
onSuccess(successResponse, files, operation) {
if (operation === "upload" && this.async.chunk) {
this.onChunkProgress(files);
if (this.isChunkUploadComplete(files[0].uid)) {
this.removeChunkInfo(files[0].uid);
}
else {
this.updateChunkInfo(files[0].uid);
this._uploadFiles([files]);
return;
}
}
const successArgs = new SuccessEvent(files, operation, successResponse);
this.successEvent.emit(successArgs);
if (operation === "upload") {
this.fileList.setFilesState(files, successArgs.isDefaultPrevented() ? FileState.Failed : FileState.Uploaded);
}
else {
if (!successArgs.isDefaultPrevented()) {
this.fileList.remove(files[0].uid);
}
}
if (!successArgs.isDefaultPrevented()) {
this.onChange();
}
}
onError(errorResponse, files, operation) {
if (operation === "upload" && this.async.chunk) {
const maxRetries = this.chunk.maxAutoRetries;
const chunkInfo = this.chunkMap.get(files[0].uid);
if (chunkInfo.retries < maxRetries) {
chunkInfo.retries += 1;
setTimeout(() => {
this.retryFiles(files[0].uid);
}, this.chunk.autoRetryAfter);
return;
}
}
const errorArgs = new ErrorEvent(files, operation, errorResponse);
this.errorEvent.emit(errorArgs);
if (operation === "upload") {
this.fileList.setFilesState(files, FileState.Failed);
}
}
onProgress(event, files) {
const percentComplete = Math.round(100 * event.loaded / event.total);
const progressArgs = new UploadProgressEvent(files, percentComplete < 100 ? percentComplete : 100);
this.uploadProgressEvent.emit(progressArgs);
}
onChunkProgress(files) {
const chunkInfo = this.chunkMap.get(files[0].uid);
let percentComplete = 0;
if (chunkInfo) {
if (chunkInfo.index === chunkInfo.totalChunks - 1) {
percentComplete = 100;
}
else {
percentComplete = Math.round(((chunkInfo.index + 1) / chunkInfo.totalChunks) * 100);
}
}
const progressArgs = new UploadProgressEvent(files, percentComplete < 100 ? percentComplete : 100);
this.uploadProgressEvent.emit(progressArgs);
}
checkAllComplete() {
if (!this.fileList.hasFileWithState([
FileState.Uploading,
FileState.Paused
]) && this.areAllSelectedFilesHandled()) {
this.completeEvent.emit();
}
else if (this.shouldUploadNextFile()) {
this.uploadFiles();
}
}
shouldUploadNextFile() {
return !this.async.concurrent &&
this.fileList.hasFileWithState([FileState.Selected]) &&
!this.fileList.hasFileWithState([FileState.Uploading]);
}
areAllSelectedFilesHandled() {
const validSelectedFiles = this.fileList.getFilesWithState(FileState.Selected).filter(file => !file.validationErrors);
return validSelectedFiles.length === 0;
}
cloneRequestHeaders(headers) {
const cloned = {};
if (headers) {
headers.keys().forEach((key) => {
if (key !== 'constructor' && key !== '__proto__' && key !== 'prototype')
cloned[key] = headers.get(key);
});
}
return new HttpHeaders(cloned);
}
populateRequestOptions(headers, reportProgress = true) {
return {
headers: headers,
reportProgress: reportProgress,
responseType: this.async.responseType,
withCredentials: this.async.withCredentials
};
}
populateUploadFormData(files, clientData) {
const saveField = this.async.saveField;
const data = new FormData();
this.populateClientFormData(data, clientData);
if (this.async.chunk) {
data.append(saveField, this.getNextChunk(files[0]));
data.append("metadata", this.getChunkMetadata(files[0]));
}
else {
for (const file of files) {
data.append(saveField, file.rawFile);
}
}
return data;
}
populateRemoveFormData(fileNames, clientData) {
const data = new FormData();
this.populateClientFormData(data, clientData);
for (const fileName of fileNames) {
data.append(this.async.removeField, fileName);
}
return data;
}
populateClientFormData(data, clientData) {
for (const key in clientData) {
if (clientData.hasOwnProperty(key)) {
data.append(key, clientData[key]);
}
}
}
/* Chunking Helper Methods Section */
getNextChunk(file) {
const info = this.getChunkInfo(file);
const newPosition = info.position + this.chunk.size;
return file.rawFile.slice(info.position, newPosition);
}
getChunkInfo(file) {
let chunkInfo = this.chunkMap.get(file.uid);
if (!chunkInfo) {
const totalChunks = file.size > 0 ? Math.ceil(file.size / this.chunk.size) : 1;
chunkInfo = this.chunkMap.add(file.uid, totalChunks);
}
return chunkInfo;
}
updateChunkInfo(uid) {
const chunkInfo = this.chunkMap.get(uid);
if (chunkInfo.index < chunkInfo.totalChunks - 1) {
chunkInfo.index += 1;
chunkInfo.position += this.chunk.size;
chunkInfo.retries = 0;
}
}
removeChunkInfo(uid) {
this.chunkMap.remove(uid);
}
getChunkMetadata(file) {
const chunkInfo = this.chunkMap.get(file.uid);
const chunkMetadata = {
chunkIndex: chunkInfo.index,
contentType: file.rawFile.type,
fileName: file.name,
fileSize: file.size,
fileUid: file.uid,
totalChunks: chunkInfo.totalChunks
};
return JSON.stringify(chunkMetadata);
}
isChunkUploadComplete(uid) {
const chunkInfo = this.chunkMap.get(uid);
if (chunkInfo) {
return chunkInfo.index + 1 === chunkInfo.totalChunks;
}
return false;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: UploadService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: i1.HttpClient }]; } });
/**
* @hidden
*/
class NavigationService {
uploadService;
zone;
onActionButtonFocus = new EventEmitter();
onFileAction = new EventEmitter();
onFileFocus = new EventEmitter();
onTabOut = new EventEmitter();
onWrapperFocus = new EventEmitter();
onSelectButtonFocus = new EventEmitter();
actionButtonsVisible = false;
fileListVisible = false;
focused = false;
keyBindings;
focusedFileIndex = 0;
_focusedIndex = -1;
constructor(uploadService, zone) {
this.uploadService = uploadService;
this.zone = zone;
}
action(event) {
const key = event.keyCode;
return this.keyBindings[key];
}
process(event, component) {
const handler = this.action(event);
if (handler) {
handler(event, component);
}
}
computeKeys() {
this.keyBindings = {
[Keys.Space]: () => this.handleSpace(),
[Keys.Enter]: () => this.handleEnter(),
[Keys.Escape]: () => this.handleEscape(),
[Keys.Delete]: () => this.handleDelete(),
[Keys.Tab]: (event, component) => this.handleTab(event, component),
[Keys.ArrowUp]: (event) => this.handleUpDown(event, -1),
[Keys.ArrowDown]: (event) => this.handleUpDown(event, 1)
};
}
focusSelectButton() {
this.focused = true;
this._focusedIndex = -1;
this.onSelectButtonFocus.emit();
}
handleEnter() {
if (this.lastIndex >= 0 && this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) {
this.zone.run(() => this.onFileAction.emit(Keys.Enter));
}
}
handleSpace() {
if (this.lastIndex >= 0 && this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) {
this.zone.run(() => this.onFileAction.emit(Keys.Space));
}
}
handleDelete() {
if (this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) {
this.zone.run(() => this.onFileAction.emit(Keys.Delete));
}
}
handleEscape() {
if (this.focusedIndex >= 0 && this.focusedIndex <= this.lastFileIndex) {
this.zone.run(() => this.onFileAction.emit(Keys.Escape));
}
}
handleTab(event, component) {
const shifted = event.shiftKey;
/* Select Files button is focused */
if (this.focusedIndex === -1 && this.fileListVisible && !shifted) {
this.focusedIndex = this.focusedFileIndex;
event.preventDefault();
this.onFileFocus.emit(this.focusedFileIndex);
return;
}
/* File in the list is focused */
if (this.focusedIndex > -1 && this.focusedIndex <= this.lastFileIndex) {
if (shifted) {
this.focusedIndex = -1;
}
else if (component !== 'fileselect' && this.actionButtonsVisible) {
this.focusedIndex = this.lastFileIndex + 1;
return;
}
}
/* Clear button is focused */
if (this.focusedIndex === this.lastFileIndex + 1) {
this.focusedIndex = shifted ? this.focusedFileIndex : this.lastIndex;
if (shifted) {
event.preventDefault();
this.onFileFocus.emit(this.focusedFileIndex);
}
return;
}
/* Upload button is focused */
if (this.focusedIndex === this.lastIndex && this.actionButtonsVisible && shifted) {
this.focusedIndex -= 1;
return;
}
this.onTabOut.emit();
}
handleUpDown(event, direction) {
const focusOnFileList = this.focusedIndex > -1 && this.uploadService.files.count >= 0;
const nextFocusableIndexInBoundaries = direction > 0 ? this.focusedFileIndex < this.lastFileIndex : this.focusedFileIndex > 0;
const focusNextFile = focusOnFileList && nextFocusableIndexInBoundaries;
if (focusNextFile) {
event.preventDefault();
this.zone.run(() => {
this.focusedIndex += direction;
this.focusedFileIndex += direction;
});
}
}
get focusedIndex() {
return this._focusedIndex;
}
set focusedIndex(index) {
if (!this.focused) {
this.onWrapperFocus.emit();
}
this._focusedIndex = index;
this.focused = true;
if (this._focusedIndex >= 0 && this._focusedIndex <= this.lastFileIndex) {
this.onFileFocus.emit(index);
}
}
get lastFileIndex() {
return this.actionButtonsVisible ? this.lastIndex - 2 : this.lastIndex;
}
get lastIndex() {
const fileCount = this.uploadService.files.count;
return this.actionButtonsVisible ? fileCount + 1 : fileCount - 1;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, deps: [{ token: UploadService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: NavigationService, decorators: [{
type: Injectable
}], ctorParameters: function () { return [{ type: UploadService }, { type: i0.NgZone }]; } });
const components = {};
/**
* @hidden
*/
class DropZoneService {
addComponent(component, zoneId) {
if (this.has(zoneId)) {
components[zoneId].push(component);
}
else {
components[zoneId] = [component];
}
}
clearComponent(component, zoneId) {
if (this.has(zoneId)) {
const componentIdx = components[zoneId].indexOf(component);
components[zoneId].splice(componentIdx, 1);
}
}
getComponents(zoneId) {
return components[zoneId];