Skip to content

Commit

Permalink
Add cancel all - button
Browse files Browse the repository at this point in the history
Add an action button to cancel all uploads. It also add the possibility to close the progress component, after the upload, if the oncomplete - method did not set the visibility.

Also a minor round glitch was removed (the progress could show 101%).
  • Loading branch information
sascha-karnatz committed Dec 15, 2023
1 parent ca5bdfb commit e4ceee1
Show file tree
Hide file tree
Showing 10 changed files with 356 additions and 85 deletions.
10 changes: 8 additions & 2 deletions app/assets/stylesheets/alchemy/upload.scss
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ alchemy-upload-progress {
display: grid;
font-size: var(--font-size_medium);
gap: var(--spacing-4);
grid-template-columns: calc(100% - var(--pogress_value-width)) var(
--pogress_value-width
grid-template-columns: calc(100% - var(--pogress_value-width)) calc(
var(--pogress_value-width) - var(--spacing-2)
);
height: auto;
left: 0;
Expand All @@ -69,6 +69,12 @@ alchemy-upload-progress {
opacity: 1;
}

.overall-progress-value {
align-items: center;
display: flex;
justify-content: space-between;
}

.value-text {
color: white;
font-size: var(--font-size_large);
Expand Down
35 changes: 22 additions & 13 deletions app/javascript/alchemy_admin/components/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,19 +81,7 @@ export class Uploader extends AlchemyHTMLElement {
return fileUpload
})

// create progress bar
this.uploadProgress = new Progress(fileUploads)
this.uploadProgress.onComplete = (status) => {
if (status === "successful") {
// wait three seconds to see the result of the progressbar
setTimeout(() => (this.uploadProgress.visible = false), 1500)
setTimeout(() => this.dispatchCustomEvent("Upload.Complete"), 2000)
} else {
this.dispatchCustomEvent("Upload.Failure")
}
}

document.body.append(this.uploadProgress)
this._createProgress(fileUploads)
}

/**
Expand All @@ -112,6 +100,27 @@ export class Uploader extends AlchemyHTMLElement {
request.send(formData)
}

/**
* create (and maybe remove the old) progress bar - component
* @param {FileUpload[]} fileUploads
* @private
*/
_createProgress(fileUploads) {
if (this.uploadProgress) {
this.uploadProgress.cancel()
document.body.removeChild(this.uploadProgress)
}
this.uploadProgress = new Progress(fileUploads)
this.uploadProgress.onComplete = (status) => {
if (status === "successful" || status === "canceled") {
this.uploadProgress.visible = false
}
this.dispatchCustomEvent(`upload.${status}`)
}

document.body.append(this.uploadProgress)
}

/**
* @returns {HTMLInputElement}
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,11 @@ export class FileUpload extends AlchemyHTMLElement {
* cancel the upload
*/
cancel() {
this.status = "canceled"
this.request?.abort()
this.dispatchCustomEvent("FileUpload.Change")
if (!this.finished) {
this.status = "canceled"
this.request?.abort()
this.dispatchCustomEvent("FileUpload.Change")
}
}

/**
Expand Down
202 changes: 149 additions & 53 deletions app/javascript/alchemy_admin/components/uploader/progress.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AlchemyHTMLElement } from "../alchemy_html_element"
import { FileUpload } from "./file_upload"
import { formatFileSize } from "../../utils/format"
import { translate } from "../../i18n"

export class Progress extends AlchemyHTMLElement {
#visible = false
Expand All @@ -10,21 +11,77 @@ export class Progress extends AlchemyHTMLElement {
*/
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)
this.visible = false
} 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._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 `
<sl-progress-bar value="0"></sl-progress-bar>
<div class="overall-progress-value value-text"></div>
<div class="overall-progress-value">
<span class="value-text"></span>
<sl-tooltip content="${this.buttonLabel}">
<button class="icon_button" aria-label="${this.buttonLabel}">
<i class="icon ri-close-line ri-fw"></i>
</button>
</sl-tooltip>
</div>
<div class="single-uploads" style="--progress-columns: ${
this.fileCount > 3 ? 3 : this.fileCount
}"></div>
Expand All @@ -33,72 +90,100 @@ export class Progress extends AlchemyHTMLElement {
}

/**
* append file progress - components for each file
* get all active upload components
* @returns {FileUpload[]}
* @private
*/
afterRender() {
this.fileUploads.forEach((fileUpload) => {
this.querySelector(".single-uploads").append(fileUpload)
})
_activeUploads() {
return this.fileUploads.filter((upload) => upload.active)
}

/**
* don't render the whole element new, because it would prevent selecting buttons
* replace cancel button to be the close button
* @private
*/
_updateView() {
const totalSize = this._activeUploads().reduce(
(accumulator, upload) => accumulator + upload.file.size,
_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
)
const totalProgress = Math.ceil(
this._activeUploads().reduce((accumulator, upload) => {
const weight = upload.file.size / totalSize
return upload.value * weight + accumulator
}, 0)
)
const uploadedFileCount = this._activeUploads().filter(
(fileProgress) => fileProgress.value >= 100
).length
const overallProgressValue = `${totalProgress}% (${uploadedFileCount} / ${
this._activeUploads().length
})`
const overallUploadSize = `${formatFileSize(
this._sumFileProgresses("progressEventLoaded")
)} / ${formatFileSize(this._sumFileProgresses("progressEventTotal"))}`
}

/**
* don't render the whole element new, because it would prevent selecting buttons
* @private
*/
_updateView() {
const status = this.status

this.progressElement.value = totalProgress
// update progress bar
this.progressElement.value = this.totalProgress
this.progressElement.toggleAttribute(
"indeterminate",
status === "upload-finished"
)
this.querySelector(`.overall-progress-value`).textContent =
overallProgressValue
this.querySelector(`.overall-upload-value`).textContent = overallUploadSize

// 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)
}

this.className = status
this.visible = true
}

/**
* 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) {}

/**
* @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}
*/
Expand All @@ -115,10 +200,16 @@ export class Progress extends AlchemyHTMLElement {
(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]
Expand All @@ -127,6 +218,27 @@ export class Progress extends AlchemyHTMLElement {
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}
*/
Expand All @@ -141,22 +253,6 @@ export class Progress extends AlchemyHTMLElement {
this.classList.toggle("visible", visible)
this.#visible = visible
}

/**
* @param {string} field
* @returns {number}
* @private
*/
_sumFileProgresses(field) {
return this._activeUploads().reduce(
(accumulator, upload) => upload[field] + accumulator,
0
)
}

_activeUploads() {
return this.fileUploads.filter((upload) => upload.active)
}
}

customElements.define("alchemy-upload-progress", Progress)
2 changes: 2 additions & 0 deletions app/javascript/alchemy_admin/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ export const en = {
"Maximum number of files exceeded": "Maximum number of files exceeded",
"Uploaded bytes exceed file size": "Uploaded bytes exceed file size",
"Abort upload": "Abort upload",
"Cancel all uploads": "Cancel all uploads",
Close: "Close",
formats: {
datetime: "Y-m-d H:i",
date: "Y-m-d",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
</alchemy-uploader>

<script type='text/javascript'>
document.getElementById("<%= file_upload_id %>").addEventListener("Alchemy.Upload.Complete", (event) => {
document.getElementById("<%= file_upload_id %>").addEventListener("Alchemy.upload.successful", (event) => {
Turbo.visit('<%= redirect_url.html_safe %>');
})
</script>
Loading

0 comments on commit e4ceee1

Please sign in to comment.