@progress/kendo-angular-upload
Version:
Kendo UI Angular Upload Component
439 lines (428 loc) • 19 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 { Component, HostBinding, forwardRef, Renderer2, ViewChild, ElementRef, NgZone, ChangeDetectorRef, Injector, Input, Output, EventEmitter } from "@angular/core";
import { NG_VALUE_ACCESSOR } from '@angular/forms';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { KendoInput, Keys, isDocumentAvailable } from '@progress/kendo-angular-common';
import { fromEvent, merge } from 'rxjs';
import { filter } from 'rxjs/operators';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from './package-metadata';
import { UploadService } from './upload.service';
import { NavigationService } from './navigation.service';
import { UPLOAD_CLASSES, hasClasses, isFocusable, IGNORE_TARGET_CLASSES, validateInitialFileSelectFile } from './common/util';
import { FileState } from './types/file-state';
import { DropZoneService } from './dropzone.service';
import { UploadFileSelectBase } from "./common/base";
import { FileListComponent } from "./rendering/file-list.component";
import { NgIf } from "@angular/common";
import { FileSelectDirective } from "./file-select.directive";
import { ButtonComponent } from "@progress/kendo-angular-buttons";
import { DropZoneInternalDirective } from "./dropzone-internal.directive";
import { LocalizedMessagesDirective } from "./localization/localized-messages.directive";
import * as i0 from "@angular/core";
import * as i1 from "./upload.service";
import * as i2 from "@progress/kendo-angular-l10n";
import * as i3 from "./navigation.service";
import * as i4 from "./dropzone.service";
/**
* @hidden
*/
export const FILESELECT_VALUE_ACCESSOR = {
multi: true,
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => FileSelectComponent)
};
let idx = 0;
export class FileSelectComponent extends UploadFileSelectBase {
uploadService;
localization;
navigation;
dropZoneService;
ngZone;
renderer;
cdr;
injector;
fileSelectInput;
get dir() {
return this.direction;
}
/**
* Sets the `name` attribute of the `input` element of the FileSelect.
*/
set name(name) {
this.uploadService.async.saveField = name;
}
get name() {
return this.uploadService.async.saveField;
}
/**
* Fires when the value of the component has changed as a result of a successful `select` or `remove` operation.
*/
valueChange = new EventEmitter();
/**
* @hidden
*/
_restrictions = {
allowedExtensions: [],
maxFileSize: 0,
minFileSize: 0
};
direction;
wrapper;
fileListId;
documentClick; // eslint-disable-line @typescript-eslint/ban-types
blurSubscription;
wrapperFocusSubscription;
selectButtonFocusSubscription;
localizationChangeSubscription;
subs;
constructor(uploadService, localization, navigation, dropZoneService, ngZone, renderer, cdr, wrapper, injector) {
super(uploadService, navigation, cdr, injector, ngZone);
this.uploadService = uploadService;
this.localization = localization;
this.navigation = navigation;
this.dropZoneService = dropZoneService;
this.ngZone = ngZone;
this.renderer = renderer;
this.cdr = cdr;
this.injector = injector;
validatePackage(packageMetadata);
this.wrapper = wrapper.nativeElement;
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.navigation.computeKeys();
this.localizationChangeSubscription = localization.changes.subscribe(({ rtl }) => {
this.direction = rtl ? 'rtl' : 'ltr';
});
this.subscribeBlur();
this.subscribeFocus();
this.attachEventHandlers();
this.setDefaultSettings();
}
ngOnInit() {
const { buttonId, fileListId } = this.getIds();
this.focusableId = buttonId;
this.fileListId = fileListId;
if (this.zoneId) {
this.dropZoneService.addComponent(this, this.zoneId);
}
this.subs.add(this.renderer.listen(this.fileSelectInput.nativeElement, 'mouseenter', () => {
this.renderer.addClass(this.fileSelectButton.nativeElement, 'k-hover');
}));
this.subs.add(this.renderer.listen(this.fileSelectInput.nativeElement, 'mouseleave', () => {
this.renderer.removeClass(this.fileSelectButton.nativeElement, 'k-hover');
}));
this.ngZone.runOutsideAngular(() => {
this.subs.add(this.renderer.listen(this.wrapper, 'keydown', event => this.handleKeydown(event)));
});
}
/**
* @hidden
*/
textFor(key) {
return this.localization.get(key);
}
ngOnDestroy() {
this.fileList.clear();
if (this.blurSubscription) {
this.blurSubscription.unsubscribe();
}
if (this.wrapperFocusSubscription) {
this.wrapperFocusSubscription.unsubscribe();
}
if (this.selectButtonFocusSubscription) {
this.selectButtonFocusSubscription.unsubscribe();
}
if (this.localizationChangeSubscription) {
this.localizationChangeSubscription.unsubscribe();
}
if (this.subs) {
this.subs.unsubscribe();
}
}
/**
* Removes specific file from the file list.
*/
removeFileByUid(uid) {
this.uploadService.removeFiles(uid);
}
/**
* Visually clears all files from the UI.
*/
clearFiles() {
this.uploadService.clearFiles();
}
/**
* @hidden
* Used to determine if the component is empty.
*/
isEmpty() {
return false;
}
/**
* @hidden
* Used by the external dropzone to add files to the FileSelect
*/
addFiles(files) {
this.uploadService.addFiles(files);
}
/**
* @hidden
*/
get selectButtonTabIndex() {
return this.disabled ? undefined : this.tabindex;
}
/**
* @hidden
*/
getIds() {
const id = ++idx;
const buttonId = `k-fileselect-button-${id}`;
const fileListId = `k-fileselect-file-list-${id}`;
return { buttonId, fileListId };
}
/**
* @hidden
*/
writeValue(newValue) {
super.writeValue(newValue, validateInitialFileSelectFile, 'addInitialFileSelectFiles');
}
subscribeBlur() {
if (!isDocumentAvailable()) {
return;
}
this.ngZone.runOutsideAngular(() => {
this.documentClick = fromEvent(document, 'click').pipe(filter((event) => {
return !(this.wrapper !== event.target && this.wrapper.contains(event.target));
}));
this.blurSubscription = merge(this.documentClick, this.navigation.onTabOut).subscribe(() => {
if (this.navigation.focused) {
this.ngZone.run(() => {
this.navigation.focused = false;
this.onTouchedCallback();
this.onBlur.emit();
});
}
});
});
}
subscribeFocus() {
this.wrapperFocusSubscription = this.navigation.onWrapperFocus.subscribe(() => {
this.onFocus.emit();
});
this.selectButtonFocusSubscription = this.navigation.onSelectButtonFocus.subscribe(() => {
this.fileSelectButton.nativeElement.focus();
});
}
handleKeydown(event) {
if (this.disabled) {
return;
}
if (event.target === this.fileSelectButton.nativeElement && (event.keyCode === Keys.Enter || event.keyCode === Keys.Space)) {
event.preventDefault();
this.fileSelectInput.nativeElement.click();
return;
}
if (hasClasses(event.target, UPLOAD_CLASSES) ||
(!isFocusable(event.target) && !hasClasses(event.target, IGNORE_TARGET_CLASSES))) {
this.navigation.process(event, 'fileselect');
}
}
attachEventHandlers() {
this.subs = this.uploadService.changeEvent.subscribe((files) => {
let model = [];
if (files !== null) {
files.forEach((file) => {
if (file.state === FileState.Initial) {
model.push(file);
}
if (file.state === FileState.Selected && file.rawFile && !file.validationErrors) {
model.push(file.rawFile);
}
});
}
if (model.length === 0) {
model = null;
}
this.onChangeCallback(model);
this.valueChange.emit(model);
});
this.subs.add(this.uploadService.removeEvent.subscribe((args) => {
this.remove.emit(args);
}));
this.subs.add(this.uploadService.selectEvent.subscribe((args) => {
this.select.emit(args);
}));
}
setDefaultSettings() {
this.uploadService.async.autoUpload = false;
this.uploadService.component = 'FileSelect';
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FileSelectComponent, deps: [{ token: i1.UploadService }, { token: i2.LocalizationService }, { token: i3.NavigationService }, { token: i4.DropZoneService }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i0.ElementRef }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: FileSelectComponent, isStandalone: true, selector: "kendo-fileselect", inputs: { name: "name" }, outputs: { valueChange: "valueChange" }, host: { properties: { "attr.dir": "this.dir" } }, providers: [
LocalizationService,
NavigationService,
UploadService,
DropZoneService,
FILESELECT_VALUE_ACCESSOR,
{
provide: L10N_PREFIX,
useValue: 'kendo.fileselect'
},
{
provide: KendoInput,
useExisting: forwardRef(() => FileSelectComponent)
}
], viewQueries: [{ propertyName: "fileSelectInput", first: true, predicate: ["fileSelectInput"], descendants: true, static: true }], exportAs: ["kendoFileSelect"], usesInheritance: true, ngImport: i0, template: `
<ng-container kendoFileSelectLocalizedMessages
i18n-dropFilesHere="kendo.fileselect.dropFilesHere|The drop zone hint"
dropFilesHere="Drop files here to select"
i18n-invalidFileExtension="kendo.fileselect.invalidFileExtension|The text for the invalid allowed extensions restriction message"
invalidFileExtension="File type not allowed."
i18n-invalidMaxFileSize="kendo.fileselect.invalidMaxFileSize|The text for the invalid max file size restriction message"
invalidMaxFileSize="File size too large."
i18n-invalidMinFileSize="kendo.fileselect.invalidMinFileSize|The text for the invalid min file size restriction message"
invalidMinFileSize="File size too small."
i18n-remove="kendo.fileselect.remove|The text for the Remove button"
remove="Remove"
i18n-select="kendo.fileselect.select|The text for the Select button"
select="Select files..."
>
</ng-container>
<div kendoFileSelectInternalDropZone
[restrictions]="restrictions"
[multiple]="multiple"
[disabled]="disabled">
<div class="k-upload-button-wrap">
<button
kendoButton
#fileSelectButton
class="k-upload-button"
type="button"
role="button"
(click)="fileSelectInput.click()"
(focus)="onFileSelectButtonFocus()"
[id]="focusableId"
[attr.aria-label]="textFor('select')"
[attr.tabindex]="tabindex"
[attr.aria-expanded]="hasFileList"
[attr.aria-controls]="hasFileList ? fileListId : undefined"
>
{{textFor('select')}}
</button>
<input kendoFileSelect #fileSelectInput
[dir]="direction"
[accept]="accept"
[restrictions]="restrictions"
[multiple]="multiple"
[disabled]="disabled"
[required]="isControlRequired"
/>
</div>
<div class="k-dropzone-hint">{{textFor('dropFilesHere')}}</div>
</div>
<ul kendo-upload-file-list
class="k-upload-files k-reset"
*ngIf="hasFileList"
[disabled]="disabled"
[fileList]="fileList.files"
[fileTemplate]="fileTemplate"
[fileInfoTemplate]="fileInfoTemplate"
[id]="fileListId">
</ul>
`, isInline: true, dependencies: [{ kind: "directive", type: LocalizedMessagesDirective, selector: "\n [kendoUploadLocalizedMessages],\n [kendoFileSelectLocalizedMessages],\n [kendoUploadDropZoneLocalizedMessages]\n " }, { kind: "directive", type: DropZoneInternalDirective, selector: "\n [kendoUploadInternalDropZone],\n [kendoFileSelectInternalDropZone]\n ", inputs: ["disabled", "multiple", "restrictions"] }, { kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "directive", type: FileSelectDirective, selector: "[kendoFileSelect]", inputs: ["dir", "disabled", "multiple", "restrictions", "accept", "required"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: FileListComponent, selector: "[kendo-upload-file-list]", inputs: ["disabled", "fileList", "fileTemplate", "fileInfoTemplate"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FileSelectComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoFileSelect',
providers: [
LocalizationService,
NavigationService,
UploadService,
DropZoneService,
FILESELECT_VALUE_ACCESSOR,
{
provide: L10N_PREFIX,
useValue: 'kendo.fileselect'
},
{
provide: KendoInput,
useExisting: forwardRef(() => FileSelectComponent)
}
],
selector: 'kendo-fileselect',
template: `
<ng-container kendoFileSelectLocalizedMessages
i18n-dropFilesHere="kendo.fileselect.dropFilesHere|The drop zone hint"
dropFilesHere="Drop files here to select"
i18n-invalidFileExtension="kendo.fileselect.invalidFileExtension|The text for the invalid allowed extensions restriction message"
invalidFileExtension="File type not allowed."
i18n-invalidMaxFileSize="kendo.fileselect.invalidMaxFileSize|The text for the invalid max file size restriction message"
invalidMaxFileSize="File size too large."
i18n-invalidMinFileSize="kendo.fileselect.invalidMinFileSize|The text for the invalid min file size restriction message"
invalidMinFileSize="File size too small."
i18n-remove="kendo.fileselect.remove|The text for the Remove button"
remove="Remove"
i18n-select="kendo.fileselect.select|The text for the Select button"
select="Select files..."
>
</ng-container>
<div kendoFileSelectInternalDropZone
[restrictions]="restrictions"
[multiple]="multiple"
[disabled]="disabled">
<div class="k-upload-button-wrap">
<button
kendoButton
#fileSelectButton
class="k-upload-button"
type="button"
role="button"
(click)="fileSelectInput.click()"
(focus)="onFileSelectButtonFocus()"
[id]="focusableId"
[attr.aria-label]="textFor('select')"
[attr.tabindex]="tabindex"
[attr.aria-expanded]="hasFileList"
[attr.aria-controls]="hasFileList ? fileListId : undefined"
>
{{textFor('select')}}
</button>
<input kendoFileSelect #fileSelectInput
[dir]="direction"
[accept]="accept"
[restrictions]="restrictions"
[multiple]="multiple"
[disabled]="disabled"
[required]="isControlRequired"
/>
</div>
<div class="k-dropzone-hint">{{textFor('dropFilesHere')}}</div>
</div>
<ul kendo-upload-file-list
class="k-upload-files k-reset"
*ngIf="hasFileList"
[disabled]="disabled"
[fileList]="fileList.files"
[fileTemplate]="fileTemplate"
[fileInfoTemplate]="fileInfoTemplate"
[id]="fileListId">
</ul>
`,
standalone: true,
imports: [LocalizedMessagesDirective, DropZoneInternalDirective, ButtonComponent, FileSelectDirective, NgIf, FileListComponent]
}]
}], ctorParameters: function () { return [{ type: i1.UploadService }, { type: i2.LocalizationService }, { type: i3.NavigationService }, { type: i4.DropZoneService }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }, { type: i0.ElementRef }, { type: i0.Injector }]; }, propDecorators: { fileSelectInput: [{
type: ViewChild,
args: ['fileSelectInput', { static: true }]
}], dir: [{
type: HostBinding,
args: ['attr.dir']
}], name: [{
type: Input
}], valueChange: [{
type: Output
}] } });