md/src/view/CodemirrorEditor.vue

482 lines
16 KiB
Vue
Raw Normal View History

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