import { AlchemyHTMLElement } from "alchemy_admin/components/alchemy_html_element" import { FileUpload } from "alchemy_admin/components/uploader/file_upload" import { formatFileSize } from "alchemy_admin/utils/format" import { translate } from "alchemy_admin/i18n" export class Progress extends AlchemyHTMLElement { #visible = false /** * @param {FileUpload[]} fileUploads */ constructor(fileUploads = []) { super() this.buttonLabel = translate("Cancel all uploads") this.fileUploads = fileUploads this.fileCount = fileUploads.length this.className = "in-progress" this.visible = true this.handleFileChange = () => this._updateView() } /** * append file progress - components for each file */ afterRender() { this.actionButton = this.querySelector("button") this.actionButton.addEventListener("click", () => { if (this.finished) { this.onComplete(this.status) } else { this.cancel() } }) this.fileUploads.forEach((fileUpload) => { this.querySelector(".single-uploads").append(fileUpload) }) } /** * cancel requests in all remaining uploads */ cancel() { this._activeUploads().forEach((upload) => { upload.cancel() }) this._setupCloseButton() } /** * update view and register change event */ connected() { this._updateView() this.addEventListener("Alchemy.FileUpload.Change", this.handleFileChange) } /** * deregister file upload change - event */ disconnected() { this.removeEventListener("Alchemy.FileUpload.Change", this.handleFileChange) } /** * a complete hook to allow the uploader to react and trigger an event * it would be possible to trigger the event here, but the dispatching would happen * in the scope of that component and can't be cached o uploader - component level */ onComplete(_status) {} render() { return `
` } /** * get all active upload components * @returns {FileUpload[]} * @private */ _activeUploads() { return this.fileUploads.filter((upload) => upload.active) } /** * replace cancel button to be the close button * @private */ _setupCloseButton() { this.buttonLabel = translate("Close") this.actionButton.ariaLabel = this.buttonLabel this.actionButton.parentElement.content = this.buttonLabel // update tooltip content } /** * @param {string} field * @returns {number} * @private */ _sumFileProgresses(field) { return this._activeUploads().reduce( (accumulator, upload) => upload[field] + accumulator, 0 ) } /** * don't render the whole element new, because it would prevent selecting buttons * @private */ _updateView() { const status = this.status this.className = status // update progress bar this.progressElement.value = this.totalProgress this.progressElement.toggleAttribute( "indeterminate", status === "upload-finished" ) // show progress in file size and percentage this.querySelector(`.overall-progress-value > span`).textContent = this.overallProgressValue this.querySelector(`.overall-upload-value`).textContent = this.overallUploadSize if (this.finished) { this._setupCloseButton() this.onComplete(status) } else { this.visible = true } } /** * @returns {boolean} */ get finished() { return this._activeUploads().every((entry) => entry.finished) } /** * @returns {string} */ get overallUploadSize() { const uploadedFileCount = this._activeUploads().filter( (fileProgress) => fileProgress.value >= 100 ).length const overallProgressValue = `${ this.totalProgress }% (${uploadedFileCount} / ${this._activeUploads().length})` return `${formatFileSize( this._sumFileProgresses("progressEventLoaded") )} / ${formatFileSize(this._sumFileProgresses("progressEventTotal"))}` } /** * @returns {string} */ get overallProgressValue() { const uploadedFileCount = this._activeUploads().filter( (fileProgress) => fileProgress.value >= 100 ).length return `${this.totalProgress}% (${uploadedFileCount} / ${ this._activeUploads().length })` } /** * @returns {HTMLProgressElement|undefined} */ get progressElement() { return this.querySelector("sl-progress-bar") } /** * get status of file progresses and accumulate the overall status * @returns {string} */ get status() { const uploadsStatuses = this._activeUploads().map( (upload) => upload.className ) // mark as failed, if any upload failed if (uploadsStatuses.includes("failed")) { return "failed" } // no active upload means that every upload was canceled if (uploadsStatuses.length === 0) { return "canceled" } // all uploads are successful or upload-finished or in-progress if (uploadsStatuses.every((entry) => entry === uploadsStatuses[0])) { return uploadsStatuses[0] } return "in-progress" } /** * @returns {number} */ get totalProgress() { const totalSize = this._activeUploads().reduce( (accumulator, upload) => accumulator + upload.file.size, 0 ) let totalProgress = Math.ceil( this._activeUploads().reduce((accumulator, upload) => { const weight = upload.file.size / totalSize return upload.value * weight + accumulator }, 0) ) // prevent rounding errors if (totalProgress > 100) { totalProgress = 100 } return totalProgress } /** * @returns {boolean} */ get visible() { return this.#visible } /** * @param {boolean} visible */ set visible(visible) { this.classList.toggle("visible", visible) this.#visible = visible } } customElements.define("alchemy-upload-progress", Progress)