refactor: modal system (#5162)

* add animated modal class

* convert one popup to new system

* also grabbing backdrop classes

* rename

* padd modal as parameter

* rename to wrapper

* unnecessary constant

* rename parameter
remove first hashtag if present

* combine types

* rename popup to modal

* reduce options object nesting
This commit is contained in:
Jack 2024-03-01 00:29:08 +01:00 committed by GitHub
parent 9cbca109ea
commit 1d7fde25a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 294 additions and 59 deletions

View file

@ -363,12 +363,12 @@
</div>
</div>
<div id="settingsImportWrapper" class="popupWrapper hidden">
<div id="settingsImport" action="">
<dialog id="settingsImportPopup" class="modalWrapper hidden">
<form class="modal" data-mode="">
<input type="text" title="import settings" />
<div class="button">import settings</div>
</div>
</div>
<button>import settings</button>
</form>
</dialog>
<div id="customThemeShareWrapper" class="popupWrapper hidden">
<div id="customThemeShare" action="">

View file

@ -1,4 +1,6 @@
.popupWrapper {
.popupWrapper,
.backdrop,
.modalWrapper {
width: 100%;
height: 100%;
max-width: 100%;
@ -11,9 +13,11 @@
z-index: 1000;
display: grid;
justify-content: center;
justify-items: center;
align-items: center;
padding: 2rem 0;
padding: 2rem;
border: none;
grid-template-columns: 100%;
&:focus-visible {
outline: none;
}
@ -22,6 +26,17 @@
}
}
.modal {
background: var(--bg-color);
border-radius: var(--roundness);
padding: 2rem;
display: grid;
gap: 1rem;
width: 100%;
max-width: 300px;
color: var(--text-color);
}
#customTextPopupWrapper {
#customTextPopup {
background: var(--bg-color);

View file

@ -0,0 +1,222 @@
import { isPopupVisible } from "../utils/misc";
import * as Skeleton from "./skeleton";
type ShowHideOptions = {
animationMode?: "none" | "both" | "modalOnly";
animationDurationMs?: number;
customAnimation?: {
wrapper?: {
from: Record<string, string>;
to: Record<string, string>;
easing?: string;
};
modal?: {
from: Record<string, string>;
to: Record<string, string>;
easing?: string;
};
};
beforeAnimation?: (modal: HTMLElement) => Promise<void>;
afterAnimation?: (modal: HTMLElement) => Promise<void>;
};
export class AnimatedModal {
private wrapperEl: HTMLDialogElement;
private modalEl: HTMLElement;
private wrapperId: string;
private open = false;
constructor(wrapperId: string) {
if (wrapperId.startsWith("#")) {
wrapperId = wrapperId.slice(1);
}
const dialogElement = document.getElementById(wrapperId);
const modalElement = document.querySelector(
`#${wrapperId} > .modal`
) as HTMLElement;
if (dialogElement === null) {
throw new Error(`Dialog element with id ${wrapperId} not found`);
}
if (!(dialogElement instanceof HTMLDialogElement)) {
throw new Error("Animated dialog must be an HTMLDialogElement");
}
if (dialogElement === null) {
throw new Error(`Dialog element with id ${wrapperId} not found`);
}
if (modalElement === null) {
throw new Error(
`Div element inside #${wrapperId} with class 'modal' not found`
);
}
this.wrapperId = wrapperId;
this.wrapperEl = dialogElement;
this.modalEl = modalElement;
this.wrapperEl.addEventListener("keydown", (e) => {
if (e.key === "Escape" && isPopupVisible(this.wrapperId)) {
void this.hide();
}
});
this.wrapperEl.addEventListener("mousedown", (e) => {
if (e.target === this.wrapperEl) {
void this.hide();
}
});
Skeleton.save(this.wrapperId);
}
getWrapper(): HTMLDialogElement {
return this.wrapperEl;
}
getModal(): HTMLElement {
return this.modalEl;
}
isOpen(): boolean {
return this.open;
}
async show(options?: ShowHideOptions): Promise<void> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
Skeleton.append(this.wrapperId);
if (isPopupVisible(this.wrapperId)) return resolve();
this.open = true;
this.wrapperEl.showModal();
await options?.beforeAnimation?.(this.modalEl);
const animationMode = options?.animationMode ?? "both";
const animationDuration = options?.animationDurationMs ?? 125;
$(this.modalEl).stop(true, false);
$(this.wrapperEl).stop(true, false);
if (animationMode === "both" || animationMode === "none") {
if (options?.customAnimation?.modal?.from) {
$(this.modalEl).css(options.customAnimation.modal.from);
$(this.modalEl).animate(
options.customAnimation.modal.to,
animationMode === "none" ? 0 : animationDuration,
options.customAnimation.modal.easing ?? "swing"
);
} else {
$(this.modalEl).css("opacity", "1");
}
if (options?.customAnimation?.wrapper?.from) {
$(this.wrapperEl).css(options.customAnimation.wrapper.from);
}
$(this.wrapperEl)
.removeClass("hidden")
.css("opacity", "0")
.animate(
options?.customAnimation?.wrapper?.to ?? { opacity: 1 },
animationMode === "none" ? 0 : animationDuration,
options?.customAnimation?.wrapper?.easing ?? "swing",
async () => {
this.wrapperEl.focus();
await options?.afterAnimation?.(this.modalEl);
resolve();
}
);
} else if (animationMode === "modalOnly") {
$(this.wrapperEl).removeClass("hidden").css("opacity", "1");
if (options?.customAnimation?.modal?.from) {
$(this.modalEl).css(options.customAnimation.modal.from);
} else {
$(this.modalEl).css("opacity", "0");
}
$(this.modalEl).animate(
options?.customAnimation?.modal?.to ?? { opacity: 1 },
animationDuration,
options?.customAnimation?.modal?.easing ?? "swing",
async () => {
this.wrapperEl.focus();
await options?.afterAnimation?.(this.modalEl);
resolve();
}
);
}
});
}
async hide(options?: ShowHideOptions): Promise<void> {
// eslint-disable-next-line no-async-promise-executor
return new Promise(async (resolve) => {
if (!isPopupVisible(this.wrapperId)) return resolve();
await options?.beforeAnimation?.(this.modalEl);
const animationMode = options?.animationMode ?? "both";
const animationDuration = options?.animationDurationMs ?? 125;
$(this.modalEl).stop(true, false);
$(this.wrapperEl).stop(true, false);
if (animationMode === "both" || animationMode === "none") {
if (options?.customAnimation?.modal?.from) {
$(this.modalEl).css(options.customAnimation.modal.from);
$(this.modalEl).animate(
options.customAnimation.modal.to,
animationMode === "none" ? 0 : animationDuration,
options.customAnimation.modal.easing ?? "swing"
);
} else {
$(this.modalEl).css("opacity", "0");
}
if (options?.customAnimation?.wrapper?.from) {
$(this.wrapperEl).css(options.customAnimation.wrapper.from);
}
$(this.wrapperEl)
.css("opacity", "1")
.animate(
options?.customAnimation?.wrapper?.to ?? { opacity: 0 },
animationMode === "none" ? 0 : animationDuration,
options?.customAnimation?.wrapper?.easing ?? "swing",
async () => {
this.wrapperEl.close();
this.wrapperEl.classList.add("hidden");
Skeleton.remove(this.wrapperId);
this.open = false;
await options?.afterAnimation?.(this.modalEl);
resolve();
}
);
} else if (animationMode === "modalOnly") {
$(this.wrapperEl).removeClass("hidden").css("opacity", "1");
if (options?.customAnimation?.modal?.from) {
$(this.modalEl).css(options.customAnimation.modal.from);
} else {
$(this.modalEl).css("opacity", "1");
}
$(this.modalEl).animate(
options?.customAnimation?.modal?.to ?? { opacity: 0 },
animationDuration,
options?.customAnimation?.modal?.easing ?? "swing",
async () => {
this.wrapperEl.close();
$(this.wrapperEl).addClass("hidden").css("opacity", "0");
Skeleton.remove(this.wrapperId);
this.open = false;
await options?.afterAnimation?.(this.modalEl);
resolve();
}
);
}
});
}
}

View file

@ -1,65 +1,61 @@
import * as UpdateConfig from "../config";
import * as Notifications from "../elements/notifications";
import { isPopupVisible } from "../utils/misc";
import * as Skeleton from "./skeleton";
import { AnimatedModal } from "./animated-modal";
const wrapperId = "settingsImportWrapper";
type State = {
mode: "import" | "export";
value: string;
};
export function show(mode: string, config?: string): void {
Skeleton.append(wrapperId);
const state: State = {
mode: "import",
value: "",
};
if (!isPopupVisible(wrapperId)) {
$("#settingsImportWrapper").attr("mode", mode);
export function show(mode: "import" | "export", config?: string): void {
state.mode = mode;
state.value = config ?? "";
if (mode === "export") {
$("#settingsImportWrapper .button").addClass("hidden");
$("#settingsImportWrapper input").val(config ?? "");
} else if (mode === "import") {
$("#settingsImportWrapper .button").removeClass("hidden");
}
$("#settingsImportWrapper")
.stop(true, true)
.css("opacity", 0)
.removeClass("hidden")
.animate({ opacity: 1 }, 125, () => {
$("#settingsImportWrapper input").trigger("focus");
$("#settingsImportWrapper input").trigger("select");
$("#settingsImportWrapper input").trigger("focus");
});
}
}
async function hide(): Promise<void> {
if (isPopupVisible(wrapperId)) {
if ($("#settingsImportWrapper input").val() !== "") {
try {
await UpdateConfig.apply(
JSON.parse($("#settingsImportWrapper input").val() as string)
);
} catch (e) {
Notifications.add("Failed to import settings: " + e, -1);
void modal.show({
beforeAnimation: async (modal) => {
(modal.querySelector("input") as HTMLInputElement).value = state.value;
if (state.mode === "export") {
modal.querySelector("button")?.classList.add("hidden");
modal.querySelector("input")?.setAttribute("readonly", "true");
} else if (state.mode === "import") {
modal.querySelector("button")?.classList.remove("hidden");
modal.querySelector("input")?.removeAttribute("readonly");
}
UpdateConfig.saveFullConfigToLocalStorage();
}
$("#settingsImportWrapper")
.stop(true, true)
.css("opacity", 1)
.animate({ opacity: 0 }, 125, () => {
$("#settingsImportWrapper").addClass("hidden");
Skeleton.remove(wrapperId);
});
}
},
afterAnimation: async (modal) => {
const inputEl = modal.querySelector("input");
if (state.mode === "import") {
inputEl?.focus();
} else if (state.mode === "export") {
inputEl?.select();
}
},
});
}
$("#settingsImportWrapper .button").on("click", () => {
void hide();
$("#settingsImportPopup input").on("input", (e) => {
state.value = (e.target as HTMLInputElement).value;
});
$("#settingsImportWrapper").on("click", (e) => {
if ($(e.target).attr("id") === "settingsImportWrapper") {
void hide();
$("#settingsImportPopup form").on("submit", async (e) => {
e.preventDefault();
if (state.mode !== "import") return;
if (state.value === "") {
void modal.hide();
return;
}
try {
await UpdateConfig.apply(JSON.parse(state.value));
} catch (e) {
Notifications.add("Failed to import settings: " + e, -1);
}
UpdateConfig.saveFullConfigToLocalStorage();
void modal.hide();
});
Skeleton.save(wrapperId);
const modal = new AnimatedModal("settingsImportPopup");

View file

@ -1355,7 +1355,9 @@ export function isPopupVisible(popupId: string): boolean {
}
export function isAnyPopupVisible(): boolean {
const popups = document.querySelectorAll("#popups .popupWrapper");
const popups = document.querySelectorAll(
"#popups .popupWrapper, #popups .backdrop, #popups .modalWrapper"
);
let popupVisible = false;
for (const popup of popups) {
if (isPopupVisible(popup.id)) {