victorwesterlund.com/public/assets/js/modules/Modals.mjs
Victor Westerlund 5c07d93535
Version 9.0 (#17)
Release of version 9.0.0
2021-09-20 11:37:36 +02:00

191 lines
No EOL
4.7 KiB
JavaScript

// Victor Westerlund - www.victorwesterlund.com
import { default as Interaction, destroy } from "./UI.mjs";
import { Button } from "./Components.mjs";
// Boilerplate for creating element overlays
class Modal extends Interaction {
constructor(extendedInteractions = {}) {
const element = document.createElement("div");
let interactions = {
close: () => {
this.close();
},
openPage: (event) => {
let modal = undefined;
switch(event.target.dataset.type) {
case "card":
modal = new Card({});
break;
case "dialog":
default:
modal = new Dialog({});
break;
}
modal.openPage(event.target.dataset.value);
}
};
// Combine template and incoming interactions into one object
interactions = Object.assign(interactions,extendedInteractions);
super(interactions,element);
this.transition = 300;
this.element = this.applyTemplate(element);
this.element.close = () => this.close(); // Bind modal close to element prototype
document.body.insertAdjacentElement("afterbegin",this.element);
}
// Fetch page html from "assets/pages"
async getPage(page) {
const url = `assets/pages/${page}`;
const response = await fetch(url);
if(!response.ok) {
const report = {
"self": "Modal.getPage()",
"self_page": page,
"resp_status": response.status,
"resp_statusText": response.statusText,
"resp_url": response.url,
"rqst_ua": navigator.userAgent
};
throw new Error(JSON.stringify(report,null,2));
}
return response.text();
}
insertHTML(element) {
this.inner.insertAdjacentHTML("afterbegin",element);
}
insertElement(element) {
this.inner.insertAdjacentElement("afterbegin",element);
}
// Apply a modal template to the provided element
applyTemplate(element) {
// The inner div will contain modal content
this.inner = document.createElement("div");
this.inner.classList.add("inner");
element.appendChild(this.inner);
element.classList.add("modal");
// PointerEvents on the outer div will close the modal
element.addEventListener("click",event => {
if(event.target == element) {
this.close();
}
});
return element;
}
error(message) {
const oops = document.createElement("p");
const infoButton = document.createElement("p");
oops.classList.add("error");
oops.innerText = "🤯\nSomething went wrong";
infoButton.innerText = "more info..";
infoButton.addEventListener("click",() => {
const details = new Dialog();
details.insertHTML(`<h1>📄 Error report</h1><pre>${message}</pre>`);
details.open();
this.close();
});
this.insertElement(infoButton);
this.insertElement(oops);
}
// Open page from "assets/pages"
openPage(page) {
// Show a spinner while fetching
const spinner = document.createElement("div");
spinner.classList = "logo spinner";
this.element.setAttribute("data-page",page);
this.insertElement(spinner);
this.open();
// Fetch the requested page
this.getPage(page).then(html => {
this.insertHTML(html);
this.bindAll(this.inner);
})
.catch(error => {
const tryAgain = new Button({
text: "try again",
type: "solid"
});
tryAgain.element.addEventListener("click",() => {
// Clear and recreate modal structure
destroy(this.inner);
this.applyTemplate(this.element);
this.init();
this.insertElement(spinner);
// Attempt to fetch the requested url again (with soft rate-limiting)
setTimeout(() => {
this.openPage(page);
destroy(spinner);
},500);
});
this.insertElement(tryAgain.element);
this.error(error);
})
.finally(() => destroy(spinner));
}
open() {
setTimeout(() => this.element.classList.add("active"),this.transition / 2);
}
// Close the modal and remove it from the DOM
close() {
this.element.classList.remove("active");
setTimeout(() => destroy(this.element),this.transition + 1); // Wait for transition
}
}
export class Dialog extends Modal {
constructor(interactions = {}) {
super(interactions);
this.init();
}
init() {
this.element.classList.add("dialog");
this.element.classList.add("center");
const closeButton = new Button({
text: "close",
action: "close",
type: "phantom"
});
this.bind(closeButton.element);
this.inner.appendChild(closeButton.element);
}
}
// Overlay with a slide-in animation from the bottom of the viewport
export class Card extends Modal {
constructor(interactions = {}) {
super(interactions);
this.init();
}
init() {
this.element.classList.add("card");
this.element.classList.add("center");
const closeButton = new Button({
text: "close",
action: "close",
type: "phantom"
});
this.bind(closeButton.element);
this.inner.appendChild(closeButton.element);
}
}