feat: implement markdown file preview in Ace editor (#3431)
--------- Co-authored-by: Oleg Lobanov <oleg@lobanov.me>pull/3436/head
parent
f6f7e5fea3
commit
b0f4604f44
|
@ -18,6 +18,7 @@
|
||||||
"js-base64": "^3.7.7",
|
"js-base64": "^3.7.7",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"marked": "^14.1.0",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
|
@ -5810,6 +5811,18 @@
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "14.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/marked/-/marked-14.1.0.tgz",
|
||||||
|
"integrity": "sha512-P93GikH/Pde0hM5TAXEd8I4JAYi8IB03n8qzW8Bh1BIEFpEyBoYxi/XWZA53LSpTeLBiMQOoSMj0u5E/tiVYTA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/marks-pane": {
|
"node_modules/marks-pane": {
|
||||||
"version": "1.0.9",
|
"version": "1.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/marks-pane/-/marks-pane-1.0.9.tgz",
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"material-icons": "^1.13.12",
|
"material-icons": "^1.13.12",
|
||||||
|
"marked": "^14.1.0",
|
||||||
"normalize.css": "^8.0.1",
|
"normalize.css": "^8.0.1",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"pretty-bytes": "^6.1.1",
|
"pretty-bytes": "^6.1.1",
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
.md_preview {
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 80vh;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #000;
|
||||||
|
font-size: 20px;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#preview-container {
|
||||||
|
overflow: auto;
|
||||||
|
max-height: 80vh; /* Match the max-height of md_preview for scrolling */
|
||||||
|
}
|
|
@ -16,6 +16,7 @@
|
||||||
@import "./login.css";
|
@import "./login.css";
|
||||||
@import "./mobile.css";
|
@import "./mobile.css";
|
||||||
@import "./epubReader.css";
|
@import "./epubReader.css";
|
||||||
|
@import "./mdPreview.css";
|
||||||
|
|
||||||
/* For testing only
|
/* For testing only
|
||||||
:focus {
|
:focus {
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"permalink": "Get Permanent Link",
|
"permalink": "Get Permanent Link",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
|
"preview": "Preview",
|
||||||
"publish": "Publish",
|
"publish": "Publish",
|
||||||
"rename": "Rename",
|
"rename": "Rename",
|
||||||
"replace": "Replace",
|
"replace": "Replace",
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
"ok": "确定",
|
"ok": "确定",
|
||||||
"permalink": "获取永久链接",
|
"permalink": "获取永久链接",
|
||||||
"previous": "上一个",
|
"previous": "上一个",
|
||||||
|
"preview": "预览",
|
||||||
"publish": "发布",
|
"publish": "发布",
|
||||||
"rename": "重命名",
|
"rename": "重命名",
|
||||||
"replace": "替换",
|
"replace": "替换",
|
||||||
|
|
|
@ -11,11 +11,26 @@
|
||||||
:label="t('buttons.save')"
|
:label="t('buttons.save')"
|
||||||
@action="save()"
|
@action="save()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<action
|
||||||
|
icon="preview"
|
||||||
|
:label="t('buttons.preview')"
|
||||||
|
@action="preview()"
|
||||||
|
v-show="isMarkdownFile"
|
||||||
|
/>
|
||||||
</header-bar>
|
</header-bar>
|
||||||
|
|
||||||
<Breadcrumbs base="/files" noLink />
|
<Breadcrumbs base="/files" noLink />
|
||||||
|
|
||||||
<form id="editor"></form>
|
<!-- 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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -33,10 +48,11 @@ import Breadcrumbs from "@/components/Breadcrumbs.vue";
|
||||||
import { useAuthStore } from "@/stores/auth";
|
import { useAuthStore } from "@/stores/auth";
|
||||||
import { useFileStore } from "@/stores/file";
|
import { useFileStore } from "@/stores/file";
|
||||||
import { useLayoutStore } from "@/stores/layout";
|
import { useLayoutStore } from "@/stores/layout";
|
||||||
import { inject, onBeforeUnmount, onMounted, ref } from "vue";
|
import { inject, onBeforeUnmount, onMounted, ref, watchEffect } from "vue";
|
||||||
import { useRoute, useRouter } from "vue-router";
|
import { useRoute, useRouter } from "vue-router";
|
||||||
import { useI18n } from "vue-i18n";
|
import { useI18n } from "vue-i18n";
|
||||||
import { getTheme } from "@/utils/theme";
|
import { getTheme } from "@/utils/theme";
|
||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
const $showError = inject<IToastError>("$showError")!;
|
const $showError = inject<IToastError>("$showError")!;
|
||||||
|
|
||||||
|
@ -51,11 +67,37 @@ const router = useRouter();
|
||||||
|
|
||||||
const editor = ref<Ace.Editor | null>(null);
|
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(() => {
|
onMounted(() => {
|
||||||
window.addEventListener("keydown", keyEvent);
|
window.addEventListener("keydown", keyEvent);
|
||||||
|
window.addEventListener("wheel", handleScroll);
|
||||||
|
|
||||||
const fileContent = fileStore.req?.content || "";
|
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(
|
ace.config.set(
|
||||||
"basePath",
|
"basePath",
|
||||||
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
|
`https://cdn.jsdelivr.net/npm/ace-builds@${ace_version}/src-min-noconflict/`
|
||||||
|
@ -82,6 +124,7 @@ onMounted(() => {
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener("keydown", keyEvent);
|
window.removeEventListener("keydown", keyEvent);
|
||||||
|
window.removeEventListener("wheel", handleScroll);
|
||||||
editor.value?.destroy();
|
editor.value?.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -102,6 +145,13 @@ const keyEvent = (event: KeyboardEvent) => {
|
||||||
save();
|
save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScroll = (event: WheelEvent) => {
|
||||||
|
const editorContainer = document.getElementById("preview-container");
|
||||||
|
if (editorContainer) {
|
||||||
|
editorContainer.scrollTop += event.deltaY;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const save = async () => {
|
const save = async () => {
|
||||||
const button = "save";
|
const button = "save";
|
||||||
buttons.loading("save");
|
buttons.loading("save");
|
||||||
|
@ -126,4 +176,8 @@ const close = () => {
|
||||||
let uri = url.removeLastDir(route.path) + "/";
|
let uri = url.removeLastDir(route.path) + "/";
|
||||||
router.push({ path: uri });
|
router.push({ path: uri });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const preview = () => {
|
||||||
|
isPreview.value = !isPreview.value;
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
Loading…
Reference in New Issue