Current Path : /var/www/www-root/data/www/www.monolith-realty.ru/bitrix/js/ui/uploader/core/src/ |
Current File : /var/www/www-root/data/www/www.monolith-realty.ru/bitrix/js/ui/uploader/core/src/uploader-file.js |
// eslint-disable-next-line max-classes-per-file import { Type } from 'main.core'; import { BaseEvent, EventEmitter } from 'main.core.events'; import { FileStatus } from './enums/file-status'; import { FileOrigin } from './enums/file-origin'; import { FileEvent } from './enums/file-event'; import UploaderError from './uploader-error'; import AbstractUploadController from './backend/abstract-upload-controller'; import AbstractLoadController from './backend/abstract-load-controller'; import AbstractRemoveController from './backend/abstract-remove-controller'; import createUniqueId from './helpers/create-unique-id'; import createFileFromBlob from './helpers/create-file-from-blob'; import isDataUri from './helpers/is-data-uri'; import createBlobFromDataUri from './helpers/create-blob-from-data-uri'; import isResizableImage from './helpers/is-resizable-image'; import formatFileSize from './helpers/format-file-size'; import type { UploaderFileOptions } from './types/uploader-file-options'; import type { UploaderFileInfo } from './types/uploader-file-info'; import type { RemoveFileOptions } from './types/remove-file-options'; export default class UploaderFile extends EventEmitter { #id: string = null; #file: File = null; #serverFileId: number | string = null; #name: string = null; #size: number = 0; #type: string = ''; #width: ?number = null; #height: ?number = null; #treatImageAsFile: boolean = false; #clientPreview: ?Blob = null; #clientPreviewUrl: ?string = null; #clientPreviewWidth: ?number = null; #clientPreviewHeight: ?number = null; #serverPreviewUrl: ?string = null; #serverPreviewWidth: ?number = null; #serverPreviewHeight: ?number = null; #downloadUrl: ?string = null; #status: FileStatus = FileStatus.INIT; #origin: FileOrigin = FileOrigin.CLIENT; #errors: UploaderError[] = []; #progress: number = 0; #customData: Object<string, any> = Object.create(null); #uploadController: AbstractUploadController = null; #loadController: AbstractLoadController = null; #removeController: AbstractRemoveController = null; #forceServerLoad: boolean = false; #uploadCallbacks: CallbackCollection = new CallbackCollection(this); constructor(source: File | Blob | string | number | UploaderFileOptions, fileOptions: UploaderFileOptions = {}) { super(); this.setEventNamespace('BX.UI.Uploader.File'); const options: UploaderFileOptions = Type.isPlainObject(fileOptions) ? fileOptions : {}; if (Type.isFile(source)) { this.#file = source; this.update(options); } else if (Type.isBlob(source)) { this.#file = createFileFromBlob(source, options.name || source.name); this.update(options); } else if (isDataUri(source)) { const blob: Blob = createBlobFromDataUri(source); this.#file = createFileFromBlob(blob, options.name); this.update(options); } else if (Type.isNumber(source) || Type.isStringFilled(source)) { this.#origin = FileOrigin.SERVER; this.#serverFileId = source; this.update(options); } else if ( Type.isPlainObject(source) && (Type.isNumber(source.serverFileId) || Type.isStringFilled(source.serverFileId)) ) { this.#origin = FileOrigin.SERVER; this.update(source); } this.#id = Type.isStringFilled(options.id) ? options.id : createUniqueId(); if (this.#origin === FileOrigin.SERVER) { this.#forceServerLoad = options.preload === true || (Type.isPlainObject(source) && source.preload === true); } this.subscribeFromOptions({ [FileEvent.ADD]: (): void => { this.#setStatus(FileStatus.ADDED); }, }); this.subscribeFromOptions(options.events); } load(): void { if (!this.canLoad()) { return; } this.#setStatus(FileStatus.LOADING); this.emit(FileEvent.LOAD_START); this.#loadController.load(this); } shouldForceServerLoad(): boolean { return this.#forceServerLoad; } upload(callbacks: { onComplete: Function, onError: Function } = {}): void { this.#uploadCallbacks.subscribe(callbacks); if (this.isComplete() && this.isUploadable()) { this.#uploadCallbacks.emit('onComplete'); return; } if (this.isUploadFailed()) { this.#uploadCallbacks.emit('onError', { error: this.getError() }); return; } if (!this.canUpload()) { this.#uploadCallbacks.emit('onError', { error: new UploaderError('FILE_UPLOAD_NOT_ALLOWED') }); return; } const event: BaseEvent<{ file: UploaderFile }> = new BaseEvent({ data: { file: this } }); this.emit(FileEvent.BEFORE_UPLOAD, event); if (event.isDefaultPrevented()) { return; } const prepareEvent: BaseEvent = new BaseEvent({ data: { file: this } }); this.emitAsync(FileEvent.PREPARE_FILE_ASYNC, prepareEvent) .then((): void => { this.#setStatus(FileStatus.UPLOADING); this.emit(FileEvent.UPLOAD_START); this.#uploadController.upload(this); }) .catch((prepareError) => { const error = this.addError(prepareError); this.#setStatus(FileStatus.UPLOAD_FAILED); this.emit(FileEvent.UPLOAD_ERROR, { error }); }) ; } remove(options?: RemoveFileOptions): void { if (this.getStatus() === FileStatus.INIT) { return; } this.#setStatus(FileStatus.INIT); this.emit(FileEvent.REMOVE_COMPLETE); this.abort(); // this.#setStatus(FileStatus.REMOVING); // this.#removeController.remove(this); const removeFromServer: boolean = !options || options.removeFromServer !== false; if (removeFromServer && this.#removeController !== null && this.getOrigin() === FileOrigin.CLIENT) { this.#removeController.remove(this); } this.#uploadController = null; this.#loadController = null; this.#removeController = null; } // stop(): void // { // if (this.isUploading()) // { // this.abort(); // this.setStatus(FileStatus.PENDING); // } // } // // resume(): void // { // // } // retry(): void // { // // TODO // } abort(): void { if (this.isLoading()) { this.#setStatus(FileStatus.LOAD_FAILED); const error: UploaderError = new UploaderError('FILE_LOAD_ABORTED'); this.emit(FileEvent.LOAD_ERROR, { error }); } else if (this.isUploading()) { this.#setStatus(FileStatus.UPLOAD_FAILED); const error: UploaderError = new UploaderError('FILE_UPLOAD_ABORTED'); this.emit('onUploadError', { error }); this.#uploadCallbacks.emit('onError', { error }); } if (this.#loadController) { this.#loadController.abort(); } if (this.#uploadController) { this.#uploadController.abort(); } } getUploadController(): ?AbstractUploadController { return this.#uploadController; } setUploadController(controller: ?AbstractUploadController): void { if (!(controller instanceof AbstractUploadController) && !Type.isNull(controller)) { return; } const changed = this.#uploadController !== controller; this.#uploadController = controller; if (this.#uploadController && changed) { this.#uploadController.subscribeOnce('onError', (event: BaseEvent): void => { const error: UploaderError = this.addError(event.getData().error); this.#setStatus(FileStatus.UPLOAD_FAILED); this.emit(FileEvent.UPLOAD_ERROR, { error }); this.#uploadCallbacks.emit('onError', { error }); }); this.#uploadController.subscribe('onProgress', (event: BaseEvent): void => { const { progress } = event.getData(); this.setProgress(progress); this.emit(FileEvent.UPLOAD_PROGRESS, { progress }); }); this.#uploadController.subscribeOnce('onUpload', (event: BaseEvent): void => { this.#setStatus(FileStatus.COMPLETE); this.update(event.getData().fileInfo); this.emit(FileEvent.UPLOAD_COMPLETE); this.#uploadCallbacks.emit('onComplete'); }); } if (changed) { this.emit(FileEvent.UPLOAD_CONTROLLER_INIT, { controller }); } } setLoadController(controller: AbstractLoadController): void { if (!(controller instanceof AbstractLoadController)) { return; } const changed = this.#loadController !== controller; this.#loadController = controller; if (this.#loadController && changed) { this.#loadController.subscribeOnce('onError', (event: BaseEvent): void => { const error: UploaderError = this.addError(event.getData().error); this.#setStatus(FileStatus.LOAD_FAILED); this.emit(FileEvent.LOAD_ERROR, { error }); }); this.#loadController.subscribe('onProgress', (event: BaseEvent): void => { const { progress } = event.getData(); this.emit(FileEvent.LOAD_PROGRESS, { progress }); }); this.#loadController.subscribeOnce('onLoad', (event: BaseEvent): void => { if (this.getOrigin() === FileOrigin.CLIENT) { const validationEvent: BaseEvent = new BaseEvent({ data: { file: this } }); this.emitAsync(FileEvent.VALIDATE_FILE_ASYNC, validationEvent) .then((): void => { if (this.isUploadable()) { this.#setStatus(FileStatus.PENDING); this.emit(FileEvent.LOAD_COMPLETE); } else { const preparationEvent: BaseEvent = new BaseEvent({ data: { file: this } }); this.emitAsync(FileEvent.PREPARE_FILE_ASYNC, preparationEvent) .then((): void => { this.#setStatus(FileStatus.COMPLETE); this.emit(FileEvent.LOAD_COMPLETE); }) .catch((preparationError) => { const error = this.addError(preparationError); this.#setStatus(FileStatus.LOAD_FAILED); this.emit(FileEvent.LOAD_ERROR, { error }); }) ; } }) .catch((validationError) => { const error = this.addError(validationError); this.#setStatus(FileStatus.LOAD_FAILED); this.emit(FileEvent.LOAD_ERROR, { error }); }) ; } else { this.update(event.getData().fileInfo); if (this.isUploadable()) { this.#setStatus(FileStatus.PENDING); } else { this.#setStatus(FileStatus.COMPLETE); } this.emit(FileEvent.LOAD_COMPLETE); } }); } if (changed) { this.emit(FileEvent.LOAD_CONTROLLER_INIT, { controller }); } } setRemoveController(controller: ?AbstractRemoveController): void { if (!(controller instanceof AbstractRemoveController) && !Type.isNull(controller)) { return; } const changed = this.#removeController !== controller; this.#removeController = controller; if (this.#removeController && changed) { this.#removeController.subscribeOnce('onError', (event: BaseEvent) => { // const error = this.addError(event.getData().error); // this.emit(FileEvent.REMOVE_ERROR, { error }); }); this.#removeController.subscribeOnce('onRemove', (event: BaseEvent) => { // this.#setStatus(FileStatus.INIT); // this.emit(FileEvent.REMOVE_COMPLETE); }); } if (changed) { this.emit(FileEvent.REMOVE_CONTROLLER_INIT, { controller }); } } isReadyToUpload(): boolean { return this.getStatus() === FileStatus.PENDING; } isUploadable(): boolean { return this.#uploadController !== null; } isLoadable(): boolean { return this.#loadController !== null; } isRemoveable(): boolean { return this.#removeController !== null; } canUpload(): boolean { return this.isReadyToUpload() && this.isUploadable(); } canLoad(): boolean { return this.getStatus() === FileStatus.ADDED && this.isLoadable(); } isUploading(): boolean { return this.getStatus() === FileStatus.UPLOADING; } isLoading(): boolean { return this.getStatus() === FileStatus.LOADING; } isComplete(): boolean { return this.getStatus() === FileStatus.COMPLETE; } isFailed(): boolean { return this.getStatus() === FileStatus.LOAD_FAILED || this.getStatus() === FileStatus.UPLOAD_FAILED; } isLoadFailed(): boolean { return this.getStatus() === FileStatus.LOAD_FAILED; } isUploadFailed(): boolean { return this.getStatus() === FileStatus.UPLOAD_FAILED; } getBinary(): ?File { return this.#file; } setFile(file: File | Blob): void { if (Type.isFile(file)) { this.#file = file; } else if (Type.isBlob(file)) { this.#file = createFileFromBlob(file, this.getName()); } } update(options: UploaderFileOptions): void { if (Type.isPlainObject(options)) { this.setName(options.name); this.setType(options.type); this.setSize(options.size); this.setServerFileId(options.serverFileId); this.setWidth(options.width); this.setHeight(options.height); this.setTreatImageAsFile(options.treatImageAsFile); this.setClientPreview(options.clientPreview, options.clientPreviewWidth, options.clientPreviewHeight); this.setServerPreview(options.serverPreviewUrl, options.serverPreviewWidth, options.serverPreviewHeight); this.setDownloadUrl(options.downloadUrl); this.setCustomData(options.customData); this.setLoadController(options.loadController); this.setUploadController(options.uploadController); this.setRemoveController(options.removeController); } } getName(): string { return this.#name === null ? (this.getBinary() ? this.getBinary().name : '') : this.#name; } setName(name: string | null): void { if (Type.isStringFilled(name) || Type.isNull(name)) { this.#name = name; this.emit(FileEvent.STATE_CHANGE, { property: 'name', value: name }); } } getExtension(): string { const name: string = this.getName(); const position: number = name.lastIndexOf('.'); return position >= 0 ? name.slice(Math.max(0, position + 1)).toLowerCase() : ''; } getType(): string { return this.getBinary() ? this.getBinary().type : this.#type; } setType(type: string): string { if (Type.isStringFilled(type)) { this.#type = type; this.emit(FileEvent.STATE_CHANGE, { property: 'type', value: type }); } } getSize(): number { return this.getBinary() ? this.getBinary().size : this.#size; } getSizeFormatted(): string { return formatFileSize(this.getSize()); } setSize(size: number): void { if (Type.isNumber(size) && size >= 0) { this.#size = size; this.emit(FileEvent.STATE_CHANGE, { property: 'size', value: size }); } } getId(): string { return this.#id; } getServerFileId(): number | string | null { return this.#serverFileId; } /** * @deprecated * use getServerFileId */ getServerId(): number | string | null { return this.getServerFileId(); } setServerFileId(id: number | string): void { if (Type.isNumber(id) || Type.isStringFilled(id)) { this.#serverFileId = id; this.emit(FileEvent.STATE_CHANGE, { property: 'serverFileId', value: id }); } } getStatus(): FileStatus { return this.#status; } #setStatus(status: FileStatus): void { this.#status = status; this.emit(FileEvent.STATE_CHANGE, { property: 'status', value: status }); this.emit(FileEvent.STATUS_CHANGE); } getOrigin(): FileOrigin { return this.#origin; } getDownloadUrl(): ?string { return this.#downloadUrl; } setDownloadUrl(url: string): void { if (Type.isStringFilled(url)) { this.#downloadUrl = url; this.emit(FileEvent.STATE_CHANGE, { property: 'downloadUrl', value: url }); } } getWidth(): ?number { return this.#width; } setWidth(width: number) { if (Type.isNumber(width)) { this.#width = width; this.emit(FileEvent.STATE_CHANGE, { property: 'width', value: width }); } } getHeight(): ?number { return this.#height; } setHeight(height: ?number) { if (Type.isNumber(height)) { this.#height = height; this.emit(FileEvent.STATE_CHANGE, { property: 'height', value: height }); } } setTreatImageAsFile(flag: boolean): void { if (Type.isBoolean(flag)) { this.#treatImageAsFile = flag; this.emit(FileEvent.STATE_CHANGE, { property: 'treatImageAsFile', value: flag }); } } shouldTreatImageAsFile(): boolean { return this.#treatImageAsFile; } getPreviewUrl(): ?string { return this.getClientPreview() ? this.getClientPreviewUrl() : this.getServerPreviewUrl(); } getPreviewWidth(): ?number { return this.getClientPreview() ? this.getClientPreviewWidth() : this.getServerPreviewWidth(); } getPreviewHeight(): ?number { return this.getClientPreview() ? this.getClientPreviewHeight() : this.getServerPreviewHeight(); } getClientPreview(): ?Blob { return this.#clientPreview; } setClientPreview(file: ?Blob, width: number = null, height: number = null): void { if (Type.isBlob(file) || Type.isNull(file)) { this.revokeClientPreviewUrl(); const url = Type.isNull(file) ? null : URL.createObjectURL(file); this.#clientPreview = file; this.#clientPreviewUrl = url; this.#clientPreviewWidth = width; this.#clientPreviewHeight = height; this.emit(FileEvent.STATE_CHANGE, { property: 'clientPreviewUrl', value: url }); this.emit(FileEvent.STATE_CHANGE, { property: 'clientPreviewWidth', value: width }); this.emit(FileEvent.STATE_CHANGE, { property: 'clientPreviewHeight', value: height }); } } getClientPreviewUrl(): ?string { return this.#clientPreviewUrl; } revokeClientPreviewUrl(): void { if (this.#clientPreviewUrl !== null) { URL.revokeObjectURL(this.#clientPreviewUrl); this.#clientPreviewUrl = null; this.emit(FileEvent.STATE_CHANGE, { property: 'clientPreviewUrl', value: null }); } } getClientPreviewWidth(): ?number { return this.#clientPreviewWidth; } getClientPreviewHeight(): ?number { return this.#clientPreviewHeight; } getServerPreviewUrl(): ?string { return this.#serverPreviewUrl; } setServerPreview(url: ?string, width: number = null, height: number = null): ?string { if (Type.isStringFilled(url) || Type.isNull(url)) { this.#serverPreviewUrl = url; this.#serverPreviewWidth = width; this.#serverPreviewHeight = height; this.emit(FileEvent.STATE_CHANGE, { property: 'serverPreviewUrl', value: url }); this.emit(FileEvent.STATE_CHANGE, { property: 'serverPreviewWidth', value: width }); this.emit(FileEvent.STATE_CHANGE, { property: 'serverPreviewHeight', value: height }); } } getServerPreviewWidth(): ?number { return this.#serverPreviewWidth; } getServerPreviewHeight(): ?number { return this.#serverPreviewHeight; } isImage(): boolean { if (this.shouldTreatImageAsFile()) { return false; } // return isResizableImage(this.getName(), this.getType()); return this.getWidth() > 0 && this.getHeight() > 0 && isResizableImage(this.getName(), this.getType()); } getProgress(): number { return this.#progress; } setProgress(progress: ?number): void { if (Type.isNumber(progress)) { this.#progress = progress; this.emit(FileEvent.STATE_CHANGE, { property: 'progress', value: progress }); } } addError(error: Error | UploaderError): UploaderError { const uploaderError: UploaderError = error instanceof Error ? UploaderError.createFromError(error) : error; this.#errors.push(uploaderError); this.emit(FileEvent.STATE_CHANGE); return uploaderError; } getError(): ?UploaderError { return this.#errors[0] || null; } getErrors(): UploaderError[] { return this.#errors; } getState(): UploaderFileInfo { return JSON.parse(JSON.stringify(this)); } setCustomData(property: ?string | { [key: string]: any }, value?: any): void { if (Type.isNull(property)) { this.#customData = Object.create(null); this.emit(FileEvent.STATE_CHANGE, { property: 'customData', value: null }); } else if (Type.isPlainObject(property)) { Object.entries(property).forEach((item) => { const [currentKey, currentValue] = item; this.setCustomData(currentKey, currentValue); }); } else if (Type.isString(property)) { if (Type.isNull(value)) { delete this.#customData[property]; this.emit(FileEvent.STATE_CHANGE, { property: 'customData', customProperty: property, value: null }); } else if (!Type.isUndefined(value)) { this.#customData[property] = value; this.emit(FileEvent.STATE_CHANGE, { property: 'customData', customProperty: property, value }); } } } getCustomData(property?: string): any { if (Type.isUndefined(property)) { return this.#customData; } if (Type.isStringFilled(property)) { return this.#customData[property]; } return undefined; } toJSON(): UploaderFileInfo { return { id: this.getId(), serverFileId: this.getServerFileId(), serverId: this.getServerFileId(), // compatibility status: this.getStatus(), name: this.getName(), size: this.getSize(), sizeFormatted: this.getSizeFormatted(), type: this.getType(), extension: this.getExtension(), origin: this.getOrigin(), isImage: this.isImage(), failed: this.isFailed(), width: this.getWidth(), height: this.getHeight(), progress: this.getProgress(), error: this.getError(), errors: this.getErrors(), previewUrl: this.getPreviewUrl(), previewWidth: this.getPreviewWidth(), previewHeight: this.getPreviewHeight(), clientPreviewUrl: this.getClientPreviewUrl(), clientPreviewWidth: this.getClientPreviewWidth(), clientPreviewHeight: this.getClientPreviewHeight(), serverPreviewUrl: this.getServerPreviewUrl(), serverPreviewWidth: this.getServerPreviewWidth(), serverPreviewHeight: this.getServerPreviewHeight(), downloadUrl: this.getDownloadUrl(), customData: this.getCustomData(), }; } } class CallbackCollection { #emitter: EventEmitter = null; constructor(file: UploaderFile) { this.#emitter = new EventEmitter(file, 'BX.UI.Uploader.File.UploadCallbacks'); } subscribe(callbacks: { onComplete: Function, onError: Function } = {}): void { const handlers = Type.isPlainObject(callbacks) ? callbacks : {}; if (Type.isFunction(handlers.onComplete)) { this.getEmitter().subscribeOnce('onComplete', handlers.onComplete); } if (Type.isFunction(handlers.onError)) { this.getEmitter().subscribeOnce('onError', handlers.onError); } } emit(eventName: string, event: BaseEvent | {[key: string]: any}): void { if (this.#emitter) { this.#emitter.emit(eventName, event); this.#emitter.unsubscribeAll(); } } getEmitter(): EventEmitter { if (Type.isNull(this.#emitter)) { this.#emitter = new EventEmitter(this, 'BX.UI.Uploader.File.UploadCallbacks'); } return this.#emitter; } }