md/src/view/CodemirrorEditor.vue
2020-10-20 12:03:36 +00:00

482 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="container" :class="{ container_night: nightMode }">
<el-container>
<el-header class="editor__header">
<editor-header
ref="header"
@refresh="onEditorRefresh"
@cssChanged="cssChanged"
@downLoad="downloadEditorContent"
@showCssEditor="showCssEditor = !showCssEditor"
@showAboutDialog="aboutDialogVisible = true"
@showDialogForm="dialogFormVisible = true"
@showDialogUploadImg="dialogUploadImgVisible = true"
@startCopy="(isCoping = true), (backLight = true)"
@endCopy="endCopy"
/>
</el-header>
<el-main class="main-body">
<el-row class="main-section">
<el-col
:span="12"
@contextmenu.prevent.native="openMenu($event)"
>
<textarea
id="editor"
type="textarea"
placeholder="Your markdown text here."
v-model="source"
>
</textarea>
</el-col>
<el-col
:span="12"
class="preview-wrapper"
id="preview"
ref="preview"
:class="{
'preview-wrapper_night': nightMode && isCoping,
}"
>
<section
id="output-wrapper"
:class="{ output_night: nightMode && !backLight }"
>
<div class="preview">
<section id="output" v-html="output"></section>
<div
class="loading-mask"
v-if="nightMode && isCoping"
>
<div class="loading__img"></div>
<span>正在生成</span>
</div>
</div>
</section>
</el-col>
<transition
name="custom-classes-transition"
enter-active-class="bounceInRight"
>
<el-col id="cssBox" :span="12" v-show="showCssEditor">
<textarea
id="cssEditor"
type="textarea"
placeholder="Your custom css here."
>
</textarea>
</el-col>
</transition>
</el-row>
</el-main>
</el-container>
<upload-img-dialog
v-model="dialogUploadImgVisible"
@close="dialogUploadImgVisible = false"
@uploaded="uploaded"
/>
<about-dialog v-model="aboutDialogVisible" />
<insert-form-dialog v-model="dialogFormVisible" />
<right-click-menu
v-model="rightClickMenuVisible"
:left="mouseLeft"
:top="mouseTop"
@menuTick="onMenuEvent"
@closeMenu="closeRightClickMenu"
/>
</div>
</template>
<script>
import editorHeader from "../components/CodemirrorEditor/header";
import aboutDialog from "../components/CodemirrorEditor/aboutDialog";
import insertFormDialog from "../components/CodemirrorEditor/insertForm";
import rightClickMenu from "../components/CodemirrorEditor/rightClickMenu";
import uploadImgDialog from "../components/CodemirrorEditor/uploadImgDialog";
import {
css2json,
downLoadMD,
setFontSize,
saveEditorContent,
customCssWithTemplate,
} from "../assets/scripts/util";
import { uploadImgFile } from "../assets/scripts/uploadImageFile";
require("codemirror/mode/javascript/javascript");
import { mapState, mapMutations } from "vuex";
export default {
data() {
return {
showCssEditor: false,
aboutDialogVisible: false,
dialogUploadImgVisible: false,
dialogFormVisible: false,
isCoping: false,
isImgLoading: false,
backLight: false,
timeout: null,
changeTimer: null,
source: "",
mouseLeft: 0,
mouseTop: 0,
};
},
components: {
editorHeader,
aboutDialog,
insertFormDialog,
rightClickMenu,
uploadImgDialog,
},
computed: {
...mapState({
wxRenderer: (state) => state.wxRenderer,
output: (state) => state.output,
editor: (state) => state.editor,
cssEditor: (state) => state.cssEditor,
currentSize: (state) => state.currentSize,
currentColor: (state) => state.currentColor,
nightMode: (state) => state.nightMode,
rightClickMenuVisible: (state) => state.rightClickMenuVisible,
}),
},
created() {
this.initEditorState();
this.$nextTick(() => {
this.initEditor();
this.initCssEditor();
this.onEditorRefresh();
});
},
methods: {
initEditor() {
this.initEditorEntity();
this.editor.on("change", (cm, e) => {
if (this.changeTimer) clearTimeout(this.changeTimer);
this.changeTimer = setTimeout(() => {
this.onEditorRefresh();
saveEditorContent(this.editor, "__editor_content");
}, 300);
});
// 粘贴上传图片并插入
this.editor.on("paste", (cm, e) => {
if (
!(e.clipboardData && e.clipboardData.items) ||
this.isImgLoading
) {
return;
}
for (
let i = 0, len = e.clipboardData.items.length;
i < len;
++i
) {
let item = e.clipboardData.items[i];
if (item.kind === "file") {
// 校验图床参数
const imgHost =
localStorage.getItem("imgHost") || "default";
if (
imgHost != "default" &&
!localStorage.getItem(`${imgHost}Config`)
) {
this.$message({
showClose: true,
message: "请先配置好图床参数",
type: "error",
});
continue;
}
this.isImgLoading = true;
const pasteFile = item.getAsFile();
uploadImgFile(pasteFile)
.then((res) => {
this.uploaded(res);
})
.catch((err) => {
this.$message({
showClose: true,
message: err,
type: "error",
});
});
this.isImgLoading = false;
}
}
});
this.editor.on("mousedown", () => {
this.$store.commit("setRightClickMenuVisible", false);
});
this.editor.on("blur", () => {
//!影响到右键菜单的点击事件右键菜单的点击事件在组件内通过mousedown触发
this.$store.commit("setRightClickMenuVisible", false);
});
this.editor.on("scroll", () => {
this.$store.commit("setRightClickMenuVisible", false);
});
},
initCssEditor() {
this.initCssEditorEntity();
// 自动提示
this.cssEditor.on("keyup", (cm, e) => {
if ((e.keyCode >= 65 && e.keyCode <= 90) || e.keyCode === 189) {
cm.showHint(e);
}
});
this.cssEditor.on("update", (instance) => {
this.cssChanged();
saveEditorContent(this.cssEditor, "__css_content");
});
},
cssChanged() {
let json = css2json(this.cssEditor.getValue(0));
let theme = setFontSize(this.currentSize.replace("px", ""));
theme = customCssWithTemplate(json, this.currentColor, theme);
this.setWxRendererOptions({
theme: theme,
});
this.onEditorRefresh();
},
// 图片上传结束
uploaded(response) {
if (!response) {
this.$message({
showClose: true,
message: "上传图片未知异常",
type: "error",
});
return;
}
this.dialogUploadImgVisible = false;
// 上传成功,获取光标
const cursor = this.editor.getCursor();
const imageUrl = response;
const markdownImage = `![](${imageUrl})`;
// 将 Markdown 形式的 URL 插入编辑框光标所在位置
this.editor.replaceSelection(`\n${markdownImage}\n`, cursor);
this.$message({
showClose: true,
message: "图片上传成功",
type: "success",
});
this.onEditorRefresh();
},
// 左右滚动
leftAndRightScroll() {
const scrollCB = (text) => {
let source, target;
clearTimeout(this.timeout);
if (text === "preview") {
source = this.$refs.preview.$el;
target = document.getElementsByClassName(
"CodeMirror-scroll"
)[0];
this.editor.off("scroll", editorScrollCB);
this.timeout = setTimeout(() => {
this.editor.on("scroll", editorScrollCB);
}, 300);
} else if (text === "editor") {
source = document.getElementsByClassName(
"CodeMirror-scroll"
)[0];
target = this.$refs.preview.$el;
target.removeEventListener(
"scroll",
previewScrollCB,
false
);
this.timeout = setTimeout(() => {
target.addEventListener(
"scroll",
previewScrollCB,
false
);
}, 300);
}
let percentage =
source.scrollTop /
(source.scrollHeight - source.offsetHeight);
let height =
percentage * (target.scrollHeight - target.offsetHeight);
target.scrollTo(0, height);
};
const editorScrollCB = () => {
scrollCB("editor");
};
const previewScrollCB = () => {
scrollCB("preview");
};
this.$refs.preview.$el.addEventListener(
"scroll",
previewScrollCB,
false
);
this.editor.on("scroll", editorScrollCB);
},
// 更新编辑器
onEditorRefresh() {
this.editorRefresh();
setTimeout(() => PR.prettyPrint(), 0);
},
// 复制结束
endCopy() {
this.backLight = false;
setTimeout(() => {
this.isCoping = false;
}, 800);
},
// 下载编辑器内容到本地
downloadEditorContent() {
downLoadMD(this.editor.getValue(0));
},
// 右键菜单
openMenu(e) {
const menuMinWidth = 105;
const offsetLeft = this.$el.getBoundingClientRect().left;
const offsetWidth = this.$el.offsetWidth;
const maxLeft = offsetWidth - menuMinWidth;
const left = e.clientX - offsetLeft;
this.mouseLeft = Math.min(maxLeft, left);
this.mouseTop = e.clientY + 10;
this.$store.commit("setRightClickMenuVisible", true);
},
closeRightClickMenu() {
this.$store.commit("setRightClickMenuVisible", false);
},
onMenuEvent(type, info = {}) {
switch (type) {
case "pageReset":
this.$refs.header.showResetConfirm = true;
break;
case "insertPic":
this.dialogUploadImgVisible = true;
break;
case "downLoad":
this.downloadEditorContent();
break;
case "insertTable":
this.dialogFormVisible = true;
default:
break;
}
},
...mapMutations([
"initEditorState",
"initEditorEntity",
"setWxRendererOptions",
"editorRefresh",
"initCssEditorEntity",
]),
},
mounted() {
setTimeout(() => {
this.leftAndRightScroll();
PR.prettyPrint();
}, 300);
},
};
</script>
<style lang="less" scoped>
.main-body {
padding-top: 12px;
overflow: hidden;
}
.el-main {
transition: all 0.3s;
padding: 0;
margin: 20px;
margin-top: 0;
}
.container {
transition: all 0.3s;
}
.preview {
transition: background 0s;
transition-delay: 0.2s;
}
.preview-wrapper_night {
overflow-y: inherit;
position: relative;
left: -3px;
.preview {
background-color: #fff;
}
}
#output-wrapper {
position: relative;
}
.loading-mask {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 376px;
height: 101%;
padding-top: 1px;
font-size: 15px;
color: gray;
background-color: #1e1e1e;
.loading__img {
position: absolute;
left: 50%;
top: 330px;
width: 50px;
height: 50px;
transform: translate(-50%, -50%);
background: url("../assets/images/favicon.png") no-repeat;
background-size: cover;
}
span {
position: absolute;
left: 50%;
top: 390px;
transform: translate(-50%, -50%);
}
}
.bounceInRight {
animation-name: bounceInRight;
animation-duration: 1s;
animation-fill-mode: both;
}
@keyframes bounceInRight {
0%,
60%,
75%,
90%,
100% {
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
}
0% {
opacity: 0;
transform: translate3d(3000px, 0, 0);
}
60% {
opacity: 1;
transform: translate3d(-25px, 0, 0);
}
75% {
transform: translate3d(10px, 0, 0);
}
90% {
transform: translate3d(-5px, 0, 0);
}
100% {
transform: none;
}
}
</style>
<style lang="less">
@import url("../assets/less/app.less");
@import url("../assets/less/style-mirror.css");
@import url("../assets/less/github-v2.min.css");
</style>