fix: modal lifecycle issues, multiple modals, new directory creation and discard changes behavior (#5773)

This commit is contained in:
Ariel Leyva
2026-02-21 12:12:10 -05:00
committed by GitHub
parent 3169a14a4d
commit 200d501547
12 changed files with 109 additions and 82 deletions

View File

@@ -42,7 +42,6 @@
"videojs-hotkeys": "^0.2.28",
"videojs-mobile-ui": "^1.1.1",
"vue": "^3.5.17",
"vue-final-modal": "^4.5.5",
"vue-i18n": "^11.1.10",
"vue-lazyload": "^3.0.0",
"vue-reader": "^1.2.17",

View File

@@ -83,9 +83,6 @@ importers:
vue:
specifier: ^3.5.17
version: 3.5.28(typescript@5.9.3)
vue-final-modal:
specifier: ^4.5.5
version: 4.5.5(@vueuse/core@14.2.1(vue@3.5.28(typescript@5.9.3)))(@vueuse/integrations@14.2.1(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.28(typescript@5.9.3)))(focus-trap@8.0.0)(vue@3.5.28(typescript@5.9.3))
vue-i18n:
specifier: ^11.1.10
version: 11.2.8(vue@3.5.28(typescript@5.9.3))
@@ -2640,14 +2637,6 @@ packages:
peerDependencies:
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
vue-final-modal@4.5.5:
resolution: {integrity: sha512-A6xgsXqE6eLw9e6Tq/W6pxDBmimPuSuvq20WL9TOZpZy7itPdGeNn8e1P15PCGqP2yHM3q2gJIchPY9ZJd8YsA==}
peerDependencies:
'@vueuse/core': '>=10.0.0'
'@vueuse/integrations': '>=10.0.0'
focus-trap: '>=7.2.0'
vue: '>=3.2.0'
vue-i18n@11.2.8:
resolution: {integrity: sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==}
engines: {node: '>= 16'}
@@ -4562,6 +4551,7 @@ snapshots:
focus-trap@8.0.0:
dependencies:
tabbable: 6.4.0
optional: true
fraction.js@5.3.4: {}
@@ -5053,7 +5043,8 @@ snapshots:
systemjs@6.15.1: {}
tabbable@6.4.0: {}
tabbable@6.4.0:
optional: true
tar-mini@0.2.0: {}
@@ -5228,13 +5219,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-final-modal@4.5.5(@vueuse/core@14.2.1(vue@3.5.28(typescript@5.9.3)))(@vueuse/integrations@14.2.1(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.28(typescript@5.9.3)))(focus-trap@8.0.0)(vue@3.5.28(typescript@5.9.3)):
dependencies:
'@vueuse/core': 14.2.1(vue@3.5.28(typescript@5.9.3))
'@vueuse/integrations': 14.2.1(focus-trap@8.0.0)(jwt-decode@4.0.0)(vue@3.5.28(typescript@5.9.3))
focus-trap: 8.0.0
vue: 3.5.28(typescript@5.9.3)
vue-i18n@11.2.8(vue@3.5.28(typescript@5.9.3)):
dependencies:
'@intlify/core-base': 11.2.8

View File

@@ -1,21 +1,61 @@
<template>
<VueFinalModal
class="vfm-modal"
overlay-transition="vfm-fade"
content-transition="vfm-fade"
@closed="layoutStore.closeHovers"
:focus-trap="{
initialFocus: '#focus-prompt',
fallbackFocus: 'div.vfm__content',
}"
>
<slot />
</VueFinalModal>
<div id="modal-background" @click="backgroundClick">
<div ref="modalContainer">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { VueFinalModal } from "vue-final-modal";
import { useLayoutStore } from "@/stores/layout";
import { onMounted, ref } from "vue";
const layoutStore = useLayoutStore();
const emit = defineEmits(["closed"]);
const modalContainer = ref(null);
onMounted(() => {
const element = document.querySelector("#focus-prompt") as HTMLElement | null;
if (element) {
element.focus();
} else if (modalContainer.value) {
(modalContainer.value as HTMLElement).focus();
}
});
const backgroundClick = (event: Event) => {
const target = event.target as HTMLElement;
if (target.id == "modal-background") {
emit("closed");
}
};
window.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
event.stopImmediatePropagation();
emit("closed");
}
});
</script>
<style scoped>
#modal-background {
position: fixed;
inset: 0;
background-color: #00000096;
display: flex;
justify-content: center;
align-items: center;
z-index: 10000;
animation: ease-in 150ms opacity-enter;
}
@keyframes opacity-enter {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
</style>

View File

@@ -35,12 +35,17 @@ const props = defineProps({
type: Boolean,
default: false,
},
path: {
type: String,
default: null,
},
});
const container = ref<HTMLElement | null>(null);
const path = computed(() => {
let basePath = fileStore.isFiles ? route.path : url.removeLastDir(route.path);
const routePath = props.path || route.path;
let basePath = fileStore.isFiles ? routePath : url.removeLastDir(routePath);
if (!basePath.endsWith("/")) {
basePath += "/";
}

View File

@@ -168,7 +168,13 @@ export default {
this.showHover({
prompt: "newDir",
action: null,
confirm: null,
confirm: (url) => {
const paths = url.split("/");
this.items.push({
name: paths[paths.length - 2],
url: url,
});
},
props: {
redirect: false,
base: this.current === this.$route.path ? null : this.current,

View File

@@ -14,7 +14,7 @@
v-model.trim="name"
tabindex="1"
/>
<CreateFilePath :name="name" :is-dir="true" />
<CreateFilePath :name="name" :is-dir="true" :path="base" />
</div>
<div class="card-action">
@@ -41,7 +41,7 @@
</template>
<script setup lang="ts">
import { inject, ref } from "vue";
import { computed, inject, ref } from "vue";
import { useFileStore } from "@/stores/file";
import { useLayoutStore } from "@/stores/layout";
@@ -53,17 +53,13 @@ import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
const $showError = inject<IToastError>("$showError")!;
const props = defineProps({
base: String,
redirect: {
type: Boolean,
default: true,
},
});
const fileStore = useFileStore();
const layoutStore = useLayoutStore();
const base = computed(() => {
return layoutStore.currentPrompt?.props?.base;
});
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
@@ -76,7 +72,7 @@ const submit = async (event: Event) => {
// Build the path of the new directory.
let uri: string;
if (props.base) uri = props.base;
if (base.value) uri = base.value;
else if (fileStore.isFiles) uri = route.path + "/";
else uri = "/";
@@ -89,12 +85,15 @@ const submit = async (event: Event) => {
try {
await api.post(uri);
if (props.redirect) {
if (layoutStore.currentPrompt?.props?.redirect) {
router.push({ path: uri });
} else if (!props.base) {
} else if (!base.value) {
const res = await api.fetch(url.removeLastDir(uri) + "/");
fileStore.updateRequest(res);
}
if (layoutStore.currentPrompt?.confirm) {
layoutStore.currentPrompt?.confirm(uri);
}
} catch (e) {
if (e instanceof Error) {
$showError(e);

View File

@@ -1,10 +1,13 @@
<template>
<ModalsContainer />
<base-modal v-if="modal != null" :prompt="currentPromptName" @closed="close">
<keep-alive>
<component :is="modal" />
</keep-alive>
</base-modal>
</template>
<script setup lang="ts">
import { watch } from "vue";
import { ModalsContainer, useModal } from "vue-final-modal";
import { computed } from "vue";
import { storeToRefs } from "pinia";
import { useLayoutStore } from "@/stores/layout";
@@ -49,27 +52,15 @@ const components = new Map<string, any>([
["discardEditorChanges", DiscardEditorChanges],
]);
watch(currentPromptName, (newValue) => {
const modal = components.get(newValue!);
if (!modal) return;
const modal = computed(() => {
const modal = components.get(currentPromptName.value!);
if (!modal) null;
const { open, close } = useModal({
component: BaseModal,
slots: {
default: modal,
},
return modal;
});
layoutStore.setCloseOnPrompt(close, newValue!);
open();
});
window.addEventListener("keydown", (event) => {
const close = () => {
if (!layoutStore.currentPrompt) return;
if (event.key === "Escape") {
event.stopImmediatePropagation();
layoutStore.closeHovers();
}
});
};
</script>

View File

@@ -182,10 +182,11 @@ html[dir="rtl"] .breadcrumbs a {
background: var(--textSecondary) !important;
}
.vfm-modal {
z-index: 9999999 !important;
}
body > div[style*="z-index: 9990"] {
z-index: 10000 !important;
}
#modal-background .button:focus {
outline: 1px solid #2195f32d;
outline-offset: 1px;
}

View File

@@ -1,6 +1,5 @@
@import "normalize.css/normalize.css";
@import "vue-toastification/dist/index.css";
@import "vue-final-modal/style.css";
@import "./_variables.css";
@import "./_buttons.css";
@import "./_inputs.css";

View File

@@ -2,7 +2,6 @@ import { disableExternal } from "@/utils/constants";
import { createApp } from "vue";
import VueNumberInput from "@chenfengyuan/vue-number-input";
import VueLazyload from "vue-lazyload";
import { createVfm } from "vue-final-modal";
import Toast, { POSITION, useToast } from "vue-toastification";
import type {
ToastOptions,
@@ -27,7 +26,6 @@ dayjs.extend(relativeTime);
dayjs.extend(duration);
const pinia = createPinia(router);
const vfm = createVfm();
const app = createApp(App);
@@ -39,7 +37,6 @@ app.use(Toast, {
newestOnTop: true,
} satisfies PluginOptions);
app.use(vfm);
app.use(i18n);
app.use(pinia);
app.use(router);

View File

@@ -76,7 +76,7 @@ export const useLayoutStore = defineStore("layout", {
});
},
closeHovers() {
this.prompts.shift()?.close?.();
this.prompts.pop()?.close?.();
},
// easily reset state using `$reset`
clearLayout() {

View File

@@ -121,7 +121,7 @@ const isMarkdownFile =
fileStore.req?.name.endsWith(".markdown");
const katexOptions = {
output: "mathml" as const,
throwOnError: false
throwOnError: false,
};
marked.use(markedKatex(katexOptions));
@@ -233,6 +233,11 @@ const initEditor = (fileContent: string) => {
editor.value.setFontSize(fontSize.value);
editor.value.focus();
const selection = editor.value?.getSelection();
selection.on("changeSelection", function () {
isSelectionEmpty.value = selection.isEmpty();
});
};
const keyEvent = (event: KeyboardEvent) => {
@@ -296,6 +301,7 @@ const close = () => {
prompt: "discardEditorChanges",
confirm: (event: Event) => {
event.preventDefault();
editor.value?.session.getUndoManager().reset();
finishClose();
},
saveAction: async () => {