-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,10 @@ | ||
#flash-messages { | ||
position: absolute; | ||
top: 12rem; | ||
right: 2rem; | ||
display: flex; | ||
flex-direction: column-reverse; | ||
gap: 2rem; | ||
background-color: transparent; | ||
|
||
@extend .toast-container; | ||
pointer-events: none; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,3 +13,8 @@ | |
display: inline-block; | ||
opacity: 1; | ||
} | ||
|
||
.toast .spinner-border { | ||
display: inline-block; | ||
opacity: 1; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
import showFlashMessage from "./flash_message"; | ||
|
||
export default function fileDownload(el) { | ||
el.querySelectorAll("a[download]").forEach((anchor) => { | ||
anchor.addEventListener("click", async (e) => { | ||
e.preventDefault(); | ||
|
||
const flashMessage = showFlashMessage({ | ||
isLoading: true, | ||
text: "Preparing download...", | ||
isDismissible: false, | ||
toastOptions: { | ||
autohide: false, | ||
}, | ||
}); | ||
|
||
try { | ||
const response = await fetch(anchor.href); | ||
if (!response.ok) { | ||
throw new Error( | ||
"An unexpected error occurred while downloading the file. Please try again.", | ||
); | ||
} | ||
|
||
const filename = extractFileName(response); | ||
const blob = await response.blob(); | ||
|
||
const dummyAnchor = document.createElement("a"); | ||
dummyAnchor.href = URL.createObjectURL(blob); | ||
dummyAnchor.setAttribute("class", "link-primary text-nowrap"); | ||
dummyAnchor.setAttribute("download", filename); | ||
dummyAnchor.textContent = filename; | ||
|
||
flashMessage?.setLevel("success"); | ||
flashMessage?.setIsLoading(false); | ||
flashMessage?.setText("Download ready: " + dummyAnchor.outerHTML); | ||
flashMessage?.setIsDismissible(true); | ||
|
||
// Trigger download (save-as window or auto-download, depending on browser settings) | ||
dummyAnchor.click(); | ||
} catch (error) { | ||
if (flashMessage) { | ||
flashMessage.hide(); | ||
} | ||
|
||
showFlashMessage({ | ||
level: "error", | ||
text: error.message, | ||
}); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
function extractFileName(response: Response) { | ||
const FILENAME_REGEX = /filename\*?=(UTF-8'')?/; | ||
const contentDispositionHeader = response.headers.get("content-disposition"); | ||
|
||
if (contentDispositionHeader) { | ||
const fileNamePart = contentDispositionHeader | ||
.split(";") | ||
.find((n) => n.match(FILENAME_REGEX)); | ||
|
||
if (fileNamePart) { | ||
const fileName = fileNamePart.replace(FILENAME_REGEX, "").trim(); | ||
return decodeURIComponent(fileName); | ||
} | ||
} | ||
|
||
return ""; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { Toast, type ToastOptions } from "bootstrap.native"; | ||
|
||
const levelMap = { | ||
success: "success", | ||
info: "primary", | ||
warning: "warning", | ||
error: "error", | ||
} as const; | ||
|
||
type Level = keyof typeof levelMap; | ||
|
||
type FlashMessageOptions = { | ||
level?: Level; | ||
isLoading?: boolean; | ||
title?: string; | ||
text: string; | ||
isDismissible?: boolean; | ||
toastOptions?: Partial<ToastOptions>; | ||
}; | ||
|
||
export default function showFlashMessage({ | ||
level, | ||
isLoading = false, | ||
title, | ||
text, | ||
isDismissible = true, | ||
toastOptions, | ||
}: FlashMessageOptions) { | ||
let flashMessages = document.querySelector("#flash-messages"); | ||
if (flashMessages) { | ||
/* | ||
* Expects somewhere in the document .. | ||
* | ||
* <template id="template-flash-message"></template> | ||
* | ||
* .. a template that encapsulates the toast | ||
* */ | ||
const templateFlashMessage = document.querySelector<HTMLTemplateElement>( | ||
"template#template-flash-message", | ||
); | ||
|
||
if (templateFlashMessage) { | ||
const toastEl = ( | ||
templateFlashMessage.content.cloneNode(true) as HTMLElement | ||
).querySelector<HTMLElement>(".toast"); | ||
|
||
if (toastEl) { | ||
const flashMessage = new FlashMessage(toastEl); | ||
|
||
flashMessage.setLevel(level); | ||
flashMessage.setIsLoading(isLoading); | ||
flashMessage.setTitle(title); | ||
flashMessage.setText(text); | ||
flashMessage.setIsDismissible(isDismissible); | ||
|
||
flashMessages.appendChild(toastEl); | ||
|
||
flashMessage.show(toastOptions); | ||
|
||
return flashMessage; | ||
} | ||
} | ||
} | ||
} | ||
|
||
class FlashMessage { | ||
#toastEl: HTMLElement; | ||
#toast: Toast | null; | ||
|
||
constructor(readonly toastEl: HTMLElement) { | ||
this.#toastEl = toastEl; | ||
} | ||
|
||
setLevel(level: Level | undefined) { | ||
this.#toastEl.querySelectorAll(".toast-body i.if").forEach((el) => { | ||
el.classList.add("d-none"); | ||
}); | ||
|
||
if (level && Object.keys(levelMap).includes(level)) { | ||
this.#toastEl | ||
.querySelector(`.if--${levelMap[level]}`) | ||
?.classList.remove("d-none"); | ||
} | ||
} | ||
|
||
setIsLoading(isLoading: boolean) { | ||
this.#toastEl | ||
.querySelector(".spinner-border") | ||
?.classList.toggle("d-none", !isLoading); | ||
} | ||
|
||
setTitle(title: string | undefined) { | ||
const titleEl = this.#toastEl.querySelector(".alert-title"); | ||
if (titleEl) { | ||
if (title) { | ||
titleEl.classList.remove("d-none"); | ||
titleEl.textContent = title; | ||
} else { | ||
titleEl.classList.add("d-none"); | ||
} | ||
} | ||
} | ||
|
||
setText(text: string) { | ||
const textEl = this.#toastEl.querySelector(".toast-text"); | ||
if (textEl) { | ||
textEl.innerHTML = text; | ||
} | ||
} | ||
|
||
setIsDismissible(isDismissible: boolean) { | ||
const btnClose = this.#toastEl.querySelector(".btn-close"); | ||
if (btnClose) { | ||
btnClose.classList.toggle("d-none", !isDismissible); | ||
} | ||
} | ||
|
||
setAutohide(autohide: boolean, delay = 5000) { | ||
if (this.#toast) { | ||
this.#toast.options.autohide = autohide; | ||
this.#toast.options.delay = delay; | ||
this.#toast.show(); | ||
} | ||
} | ||
|
||
show(toastOptions: Partial<ToastOptions> = {}) { | ||
this.#toast = | ||
Toast.getInstance(this.#toastEl) ?? | ||
new Toast(this.#toastEl, toastOptions); | ||
|
||
this.#toast.show(); | ||
|
||
this.#toastEl.addEventListener("hidden.bs.toast", () => { | ||
this.#toastEl.remove(); | ||
}); | ||
} | ||
|
||
hide() { | ||
if (this.#toast) { | ||
this.#toast.hide(); | ||
} | ||
} | ||
} |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
This file was deleted.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
This file was deleted.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.