fix: modal lifecycle issues, multiple modals, new directory creation and discard changes behavior (#5773)
This commit is contained in:
@@ -42,7 +42,6 @@
|
|||||||
"videojs-hotkeys": "^0.2.28",
|
"videojs-hotkeys": "^0.2.28",
|
||||||
"videojs-mobile-ui": "^1.1.1",
|
"videojs-mobile-ui": "^1.1.1",
|
||||||
"vue": "^3.5.17",
|
"vue": "^3.5.17",
|
||||||
"vue-final-modal": "^4.5.5",
|
|
||||||
"vue-i18n": "^11.1.10",
|
"vue-i18n": "^11.1.10",
|
||||||
"vue-lazyload": "^3.0.0",
|
"vue-lazyload": "^3.0.0",
|
||||||
"vue-reader": "^1.2.17",
|
"vue-reader": "^1.2.17",
|
||||||
|
|||||||
22
frontend/pnpm-lock.yaml
generated
22
frontend/pnpm-lock.yaml
generated
@@ -83,9 +83,6 @@ importers:
|
|||||||
vue:
|
vue:
|
||||||
specifier: ^3.5.17
|
specifier: ^3.5.17
|
||||||
version: 3.5.28(typescript@5.9.3)
|
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:
|
vue-i18n:
|
||||||
specifier: ^11.1.10
|
specifier: ^11.1.10
|
||||||
version: 11.2.8(vue@3.5.28(typescript@5.9.3))
|
version: 11.2.8(vue@3.5.28(typescript@5.9.3))
|
||||||
@@ -2640,14 +2637,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: ^8.57.0 || ^9.0.0 || ^10.0.0
|
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:
|
vue-i18n@11.2.8:
|
||||||
resolution: {integrity: sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==}
|
resolution: {integrity: sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==}
|
||||||
engines: {node: '>= 16'}
|
engines: {node: '>= 16'}
|
||||||
@@ -4562,6 +4551,7 @@ snapshots:
|
|||||||
focus-trap@8.0.0:
|
focus-trap@8.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
tabbable: 6.4.0
|
tabbable: 6.4.0
|
||||||
|
optional: true
|
||||||
|
|
||||||
fraction.js@5.3.4: {}
|
fraction.js@5.3.4: {}
|
||||||
|
|
||||||
@@ -5053,7 +5043,8 @@ snapshots:
|
|||||||
|
|
||||||
systemjs@6.15.1: {}
|
systemjs@6.15.1: {}
|
||||||
|
|
||||||
tabbable@6.4.0: {}
|
tabbable@6.4.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tar-mini@0.2.0: {}
|
tar-mini@0.2.0: {}
|
||||||
|
|
||||||
@@ -5228,13 +5219,6 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- 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)):
|
vue-i18n@11.2.8(vue@3.5.28(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@intlify/core-base': 11.2.8
|
'@intlify/core-base': 11.2.8
|
||||||
|
|||||||
@@ -1,21 +1,61 @@
|
|||||||
<template>
|
<template>
|
||||||
<VueFinalModal
|
<div id="modal-background" @click="backgroundClick">
|
||||||
class="vfm-modal"
|
<div ref="modalContainer">
|
||||||
overlay-transition="vfm-fade"
|
<slot></slot>
|
||||||
content-transition="vfm-fade"
|
</div>
|
||||||
@closed="layoutStore.closeHovers"
|
</div>
|
||||||
:focus-trap="{
|
|
||||||
initialFocus: '#focus-prompt',
|
|
||||||
fallbackFocus: 'div.vfm__content',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<slot />
|
|
||||||
</VueFinalModal>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { VueFinalModal } from "vue-final-modal";
|
import { onMounted, ref } from "vue";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
|
||||||
|
|
||||||
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>
|
</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>
|
||||||
|
|||||||
@@ -35,12 +35,17 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const container = ref<HTMLElement | null>(null);
|
const container = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const path = computed(() => {
|
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("/")) {
|
if (!basePath.endsWith("/")) {
|
||||||
basePath += "/";
|
basePath += "/";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -168,7 +168,13 @@ export default {
|
|||||||
this.showHover({
|
this.showHover({
|
||||||
prompt: "newDir",
|
prompt: "newDir",
|
||||||
action: null,
|
action: null,
|
||||||
confirm: null,
|
confirm: (url) => {
|
||||||
|
const paths = url.split("/");
|
||||||
|
this.items.push({
|
||||||
|
name: paths[paths.length - 2],
|
||||||
|
url: url,
|
||||||
|
});
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
base: this.current === this.$route.path ? null : this.current,
|
base: this.current === this.$route.path ? null : this.current,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
v-model.trim="name"
|
v-model.trim="name"
|
||||||
tabindex="1"
|
tabindex="1"
|
||||||
/>
|
/>
|
||||||
<CreateFilePath :name="name" :is-dir="true" />
|
<CreateFilePath :name="name" :is-dir="true" :path="base" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-action">
|
<div class="card-action">
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, ref } from "vue";
|
import { computed, inject, ref } from "vue";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
@@ -53,17 +53,13 @@ import CreateFilePath from "@/components/prompts/CreateFilePath.vue";
|
|||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
base: String,
|
|
||||||
redirect: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const fileStore = useFileStore();
|
const fileStore = useFileStore();
|
||||||
const layoutStore = useLayoutStore();
|
const layoutStore = useLayoutStore();
|
||||||
|
|
||||||
|
const base = computed(() => {
|
||||||
|
return layoutStore.currentPrompt?.props?.base;
|
||||||
|
});
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
@@ -76,7 +72,7 @@ const submit = async (event: Event) => {
|
|||||||
|
|
||||||
// Build the path of the new directory.
|
// Build the path of the new directory.
|
||||||
let uri: string;
|
let uri: string;
|
||||||
if (props.base) uri = props.base;
|
if (base.value) uri = base.value;
|
||||||
else if (fileStore.isFiles) uri = route.path + "/";
|
else if (fileStore.isFiles) uri = route.path + "/";
|
||||||
else uri = "/";
|
else uri = "/";
|
||||||
|
|
||||||
@@ -89,12 +85,15 @@ const submit = async (event: Event) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await api.post(uri);
|
await api.post(uri);
|
||||||
if (props.redirect) {
|
if (layoutStore.currentPrompt?.props?.redirect) {
|
||||||
router.push({ path: uri });
|
router.push({ path: uri });
|
||||||
} else if (!props.base) {
|
} else if (!base.value) {
|
||||||
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
const res = await api.fetch(url.removeLastDir(uri) + "/");
|
||||||
fileStore.updateRequest(res);
|
fileStore.updateRequest(res);
|
||||||
}
|
}
|
||||||
|
if (layoutStore.currentPrompt?.confirm) {
|
||||||
|
layoutStore.currentPrompt?.confirm(uri);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
$showError(e);
|
$showError(e);
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<ModalsContainer />
|
<base-modal v-if="modal != null" :prompt="currentPromptName" @closed="close">
|
||||||
|
<keep-alive>
|
||||||
|
<component :is="modal" />
|
||||||
|
</keep-alive>
|
||||||
|
</base-modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { watch } from "vue";
|
import { computed } from "vue";
|
||||||
import { ModalsContainer, useModal } from "vue-final-modal";
|
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
|
|
||||||
@@ -49,27 +52,15 @@ const components = new Map<string, any>([
|
|||||||
["discardEditorChanges", DiscardEditorChanges],
|
["discardEditorChanges", DiscardEditorChanges],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
watch(currentPromptName, (newValue) => {
|
const modal = computed(() => {
|
||||||
const modal = components.get(newValue!);
|
const modal = components.get(currentPromptName.value!);
|
||||||
if (!modal) return;
|
if (!modal) null;
|
||||||
|
|
||||||
const { open, close } = useModal({
|
return modal;
|
||||||
component: BaseModal,
|
|
||||||
slots: {
|
|
||||||
default: modal,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
layoutStore.setCloseOnPrompt(close, newValue!);
|
|
||||||
open();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("keydown", (event) => {
|
const close = () => {
|
||||||
if (!layoutStore.currentPrompt) return;
|
if (!layoutStore.currentPrompt) return;
|
||||||
|
layoutStore.closeHovers();
|
||||||
if (event.key === "Escape") {
|
};
|
||||||
event.stopImmediatePropagation();
|
|
||||||
layoutStore.closeHovers();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -182,10 +182,11 @@ html[dir="rtl"] .breadcrumbs a {
|
|||||||
background: var(--textSecondary) !important;
|
background: var(--textSecondary) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.vfm-modal {
|
|
||||||
z-index: 9999999 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
body > div[style*="z-index: 9990"] {
|
body > div[style*="z-index: 9990"] {
|
||||||
z-index: 10000 !important;
|
z-index: 10000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#modal-background .button:focus {
|
||||||
|
outline: 1px solid #2195f32d;
|
||||||
|
outline-offset: 1px;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
@import "normalize.css/normalize.css";
|
@import "normalize.css/normalize.css";
|
||||||
@import "vue-toastification/dist/index.css";
|
@import "vue-toastification/dist/index.css";
|
||||||
@import "vue-final-modal/style.css";
|
|
||||||
@import "./_variables.css";
|
@import "./_variables.css";
|
||||||
@import "./_buttons.css";
|
@import "./_buttons.css";
|
||||||
@import "./_inputs.css";
|
@import "./_inputs.css";
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { disableExternal } from "@/utils/constants";
|
|||||||
import { createApp } from "vue";
|
import { createApp } from "vue";
|
||||||
import VueNumberInput from "@chenfengyuan/vue-number-input";
|
import VueNumberInput from "@chenfengyuan/vue-number-input";
|
||||||
import VueLazyload from "vue-lazyload";
|
import VueLazyload from "vue-lazyload";
|
||||||
import { createVfm } from "vue-final-modal";
|
|
||||||
import Toast, { POSITION, useToast } from "vue-toastification";
|
import Toast, { POSITION, useToast } from "vue-toastification";
|
||||||
import type {
|
import type {
|
||||||
ToastOptions,
|
ToastOptions,
|
||||||
@@ -27,7 +26,6 @@ dayjs.extend(relativeTime);
|
|||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
const pinia = createPinia(router);
|
const pinia = createPinia(router);
|
||||||
const vfm = createVfm();
|
|
||||||
|
|
||||||
const app = createApp(App);
|
const app = createApp(App);
|
||||||
|
|
||||||
@@ -39,7 +37,6 @@ app.use(Toast, {
|
|||||||
newestOnTop: true,
|
newestOnTop: true,
|
||||||
} satisfies PluginOptions);
|
} satisfies PluginOptions);
|
||||||
|
|
||||||
app.use(vfm);
|
|
||||||
app.use(i18n);
|
app.use(i18n);
|
||||||
app.use(pinia);
|
app.use(pinia);
|
||||||
app.use(router);
|
app.use(router);
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const useLayoutStore = defineStore("layout", {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
closeHovers() {
|
closeHovers() {
|
||||||
this.prompts.shift()?.close?.();
|
this.prompts.pop()?.close?.();
|
||||||
},
|
},
|
||||||
// easily reset state using `$reset`
|
// easily reset state using `$reset`
|
||||||
clearLayout() {
|
clearLayout() {
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ const isMarkdownFile =
|
|||||||
fileStore.req?.name.endsWith(".markdown");
|
fileStore.req?.name.endsWith(".markdown");
|
||||||
const katexOptions = {
|
const katexOptions = {
|
||||||
output: "mathml" as const,
|
output: "mathml" as const,
|
||||||
throwOnError: false
|
throwOnError: false,
|
||||||
};
|
};
|
||||||
marked.use(markedKatex(katexOptions));
|
marked.use(markedKatex(katexOptions));
|
||||||
|
|
||||||
@@ -233,6 +233,11 @@ const initEditor = (fileContent: string) => {
|
|||||||
|
|
||||||
editor.value.setFontSize(fontSize.value);
|
editor.value.setFontSize(fontSize.value);
|
||||||
editor.value.focus();
|
editor.value.focus();
|
||||||
|
|
||||||
|
const selection = editor.value?.getSelection();
|
||||||
|
selection.on("changeSelection", function () {
|
||||||
|
isSelectionEmpty.value = selection.isEmpty();
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const keyEvent = (event: KeyboardEvent) => {
|
const keyEvent = (event: KeyboardEvent) => {
|
||||||
@@ -296,6 +301,7 @@ const close = () => {
|
|||||||
prompt: "discardEditorChanges",
|
prompt: "discardEditorChanges",
|
||||||
confirm: (event: Event) => {
|
confirm: (event: Event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
editor.value?.session.getUndoManager().reset();
|
||||||
finishClose();
|
finishClose();
|
||||||
},
|
},
|
||||||
saveAction: async () => {
|
saveAction: async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user