<template> <div id="editor-container" @wheel.prevent.stop> <header-bar> <action icon="close" :label="t('buttons.close')" @action="close()" /> <title>{{ fileStore.req?.name ?? "" }}</title> <action v-if="authStore.user?.perm.modify" id="save-button" icon="save" :label="t('buttons.save')" @action="save()" /> <action icon="preview" :label="t('buttons.preview')" @action="preview()" v-show="isMarkdownFile" /> </header-bar> <Breadcrumbs base="/files" noLink /> <!-- preview container --> <div v-show="isPreview && isMarkdownFile" id="preview-container" class="md_preview" v-html="previewContent" ></div> <form v-show="!isPreview || !isMarkdownFile" id="editor"></form> </div> </template> <script setup lang="ts"> import { files as api } from "@/api"; import buttons from "@/utils/buttons"; import url from "@/utils/url"; import ace, { Ace, version as ace_version } from "ace-builds"; import modelist from "ace-builds/src-noconflict/ext-modelist"; import "ace-builds/src-noconflict/ext-language_tools"; import HeaderBar from "@/components/header/HeaderBar.vue"; import Action from "@/components/header/Action.vue"; import Breadcrumbs from "@/components/Breadcrumbs.vue"; import { useAuthStore } from "@/stores/auth"; import { useFileStore } from "@/stores/file"; import { useLayoutStore } from "@/stores/layout"; import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue"; import { useRoute, useRouter } from "vue-router"; import { useI18n } from "vue-i18n"; import { getTheme } from "@/utils/theme"; import { marked } from "marked"; const $showError = inject<IToastError>("$showError")!; const fileStore = useFileStore(); const authStore = useAuthStore(); const layoutStore = useLayoutStore(); const { t } = useI18n(); const route = useRoute(); const router = useRouter(); const editor = ref<Ace.Editor | null>(null); const isPreview = ref(false); const previewContent = ref(""); const isMarkdownFile = fileStore.req?.name.endsWith(".md") || fileStore.req?.name.endsWith(".markdown"); onMounted(() => { window.addEventListener("keydown", keyEvent); window.addEventListener("wheel", handleScroll); const fileContent = fileStore.req?.content || ""; watchEffect(async () => { if (isMarkdownFile && isPreview.value) { const new_value = editor.value?.getValue() || ""; try { previewContent.value = await marked(new_value); } catch (error) { console.error("Failed to convert content to HTML:", error); previewContent.value = ""; } const previewContainer = document.getElementById("preview-container"); if (previewContainer) { previewContainer.addEventListener("wheel", handleScroll, { capture: true, }); } } }); ace.config.set( "basePath", `https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/` ); editor.value = ace.edit("editor", { value: fileContent, showPrintMargin: false, readOnly: fileStore.req?.type === "textImmutable", theme: "ace/theme/chrome", mode: modelist.getModeForPath(fileStore.req!.name).mode, wrap: true, enableBasicAutocompletion: true, enableLiveAutocompletion: true, enableSnippets: true, }); if (getTheme() === "dark") { editor.value!.setTheme("ace/theme/twilight"); } editor.value.focus(); }); onBeforeUnmount(() => { window.removeEventListener("keydown", keyEvent); window.removeEventListener("wheel", handleScroll); editor.value?.destroy(); }); const keyEvent = (event: KeyboardEvent) => { if (event.code === "Escape") { close(); } if (!event.ctrlKey && !event.metaKey) { return; } if (event.key !== "s") { return; } event.preventDefault(); save(); }; const handleScroll = (event: WheelEvent) => { const editorContainer = document.getElementById("preview-container"); if (editorContainer) { editorContainer.scrollTop += event.deltaY; } }; const save = async () => { const button = "save"; buttons.loading("save"); try { await api.put(route.path, editor.value?.getValue()); editor.value?.session.getUndoManager().markClean(); buttons.success(button); } catch (e: any) { buttons.done(button); $showError(e); } }; const close = () => { if (!editor.value?.session.getUndoManager().isClean()) { layoutStore.showHover("discardEditorChanges"); return; } fileStore.updateRequest(null); const uri = url.removeLastDir(route.path) + "/"; router.push({ path: uri }); }; const preview = () => { isPreview.value = !isPreview.value; }; </script>