提交 c97c8733 编写于 作者: 璃白.'s avatar 璃白. 🌻

feat:添加功能键

上级 c2be8bbe
{
"scripts": {
"dev": "webpack serve --mode=development",
"build": "webpack"
"build": "webpack --mode=production"
},
"devDependencies": {
"css-loader": "^5.2.6",
......@@ -17,6 +17,7 @@
},
"dependencies": {
"marked": "^2.0.6",
"vue": "^2.6.12"
"vue": "^2.6.12",
"vuex": "^3.6.2"
}
}
<template>
<div class="md_container">
<markdown-header :showPreview.sync="showPreview" />
<markdownPreview v-if="showPreview" :content="html" />
<textarea
v-else
@change="changeText"
@focus="isFocus = true"
@blur="isFocus = false"
v-model="text"
rows="10"
></textarea>
<div :class="['md_container', { active: isFocus }]">
<markdown-header />
<markdownPreview v-if="showPreview" />
<markdown-editor v-else />
<markdown-footer v-if="!showPreview" />
</div>
</template>
<script>
import markdownHeader from "./components/md-header";
import markdownFooter from "./components/md-footer";
import markdownPreview from "./components/md-preview";
import marked from "marked";
import markdownHeader from "./components/header/md-header";
import markdownFooter from "./components/footer/md-footer";
import markdownEditor from "./components/content/md-textarea";
import markdownPreview from "./components/content/md-preview";
import { mapState } from "vuex";
export default {
components: { markdownHeader, markdownFooter, markdownPreview },
data() {
return {
text: "",
html: "",
showPreview: false,
isFocus: false
};
components: {
markdownHeader,
markdownFooter,
markdownEditor,
markdownPreview
},
methods: {
changeText() {
const text = this.text;
this.html = marked(text);
this.$emit("change", { text, html: this.html });
}
computed: {
...mapState(["showPreview", "isFocus"])
}
};
</script>
......@@ -46,15 +32,9 @@ export default {
border-radius: 4px;
padding: 10px 16px;
box-sizing: border-box;
textarea {
display: block;
width: 100%;
padding: 10px 0;
box-sizing: border-box;
color: #303030;
resize: none;
font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",
"Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
transition: border 0.3s;
&.active {
border: 1px solid var(--md-editor-theme-color-active);
}
}
</style>
export function getSelectionInfo(selectorId) {
const selector = document.getElementById(selectorId);
// const selection = window.getSelection();
const { selectionStart = 0, selectionEnd = 0 } = selector;
if (selectionStart === selectionEnd) return "";
return {
selectionStart,
selectionEnd
};
}
export function formatText(text, selectionInfo, startStr = "", endStr = "") {
if (!selectionInfo) return text + startStr + endStr;
return (
text.slice(0, selectionInfo.selectionStart) +
startStr +
text.slice(selectionInfo.selectionStart, selectionInfo.selectionEnd) +
endStr +
text.slice(selectionInfo.selectionEnd)
);
}
@font-face {
font-family: "iconfont"; /* Project id */
src: url('iconfont.ttf?t=1622541171105') format('truetype');
src: url('./font/iconfont.ttf?t=1622632202276') format('truetype');
}
.iconfont {
......@@ -51,6 +51,10 @@
content: "\eaef";
}
.icon-quxiaoquanping_o:before {
content: "\eb98";
}
.icon-tupian:before {
content: "\e607";
}
......
:root {
--md-editor-theme-color: #dbdbdb;
--md-editor-theme-color-active: #409eff;
--md-editor-text-color: #303030;
--md-editor-text-color-active: #000;
}
<template>
<div class="md_preview">
<div v-html="content"></div>
<div v-html="html"></div>
</div>
</template>
<script>
import { mapState, mapMutations } from "vuex";
import marked from "marked";
export default {
props: {
content: {
type: String,
default: ""
}
},
data() {
return {};
},
computed: {
...mapState(["text", "html"])
},
methods: {
...mapMutations(["setHtml"]),
transferMarkdown(val) {
if (!val.trim()) return;
const html = marked(val);
this.setHtml(html);
}
},
watch: {
text: {
immediate: true,
handler: function(val) {
this.transferMarkdown(val);
}
}
}
};
</script>
......@@ -21,6 +36,7 @@ export default {
min-height: 170px;
padding: 10px 0;
box-sizing: border-box;
color: #303030;
color: var(--md-editor-text-color);
word-break: break-all;
}
</style>
<template>
<div :class="['md_textarea', { fullScreen }]">
<textarea
:id="id"
@change="setText(textContent)"
@focus="setFocus(true)"
@blur="setFocus(false)"
v-model="textContent"
rows="10"
>
</textarea>
<span
@click="setFullScreen(false)"
v-if="fullScreen"
class="icon iconfont icon-quxiaoquanping_o"
></span>
</div>
</template>
<script>
import { mapState, mapMutations } from "vuex";
import { getSelectionInfo } from "@/assets/js/utils";
export default {
data() {
return {
id: new Date().getTime(),
isFocus: false,
textContent: ""
};
},
created() {
document.addEventListener("mouseup", this.checkSelection);
},
watch: {
text: {
immediate: true,
handler: function(val) {
this.textContent = val;
}
}
},
beforeDestroy() {
document.removeEventListener("mouseup", this.checkSelection);
},
computed: {
...mapState(["text", "fullScreen"])
},
methods: {
...mapMutations([
"setText",
"setFullScreen",
"setSelectionInfo",
"setFocus"
]),
checkSelection() {
const info = getSelectionInfo(this.id);
if (!info) return;
this.setSelectionInfo(info);
}
}
};
</script>
<style lang="less" scoped>
.md_textarea {
position: relative;
padding: 10px 0;
&.fullScreen {
position: fixed;
width: 100vw;
height: 100vh;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 99;
padding: 40px 60px;
box-sizing: border-box;
textarea {
font-size: 20px;
}
}
textarea {
display: block;
width: 100%;
height: 100%;
box-sizing: border-box;
color: var(--md-editor-text-color);
resize: none;
font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",
"Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
}
.icon {
position: absolute;
top: 20px;
right: 20px;
font-size: 32px;
cursor: pointer;
}
}
</style>
<template>
<div class="md_footer">
<div :class="['md_footer', { active: isFocus }]">
<div class="doc"></div>
<upload-files />
</div>
</template>
<script>
import uploadFiles from "./upload-files";
import { mapState } from "vuex";
export default {
components: { uploadFiles },
data() {
return {};
},
computed: {
...mapState(["isFocus"])
}
};
</script>
......@@ -22,6 +26,10 @@ export default {
padding-top: 10px;
box-sizing: border-box;
font-size: 14px;
color: #666;
color: var(--md-editor-text-color);
transition: border-top 0.3s;
&.active {
border-top: 1px solid var(--md-editor-theme-color-active);
}
}
</style>
<template>
<div class="upload_files" @click="upload">
<span :class="['icon iconfont', 'icon-tupian']"></span>
<span class="icon iconfont icon-tupian"></span>
<span>添加附件</span>
</div>
</template>
......@@ -19,12 +19,12 @@ export default {
cursor: pointer;
.icon {
font-size: 20px;
color: #666;
color: var(--md-editor-text-color);
cursor: pointer;
display: inline-block;
vertical-align: bottom;
&:hover {
color: #000;
color: var(--md-editor-text-color-active);
}
}
}
......
<template>
<div class="md_header">
<div :class="['md_header', { active: isFocus }]">
<div class="header_tabs">
<div
:class="['tab_item', { active: !showPreview }]"
@click="toggleTab('edit')"
@click="setShowPreview(false)"
>
编辑
</div>
<div
:class="['tab_item', { active: showPreview }]"
@click="toggleTab('preview')"
@click="setShowPreview(true)"
>
预览
</div>
......@@ -25,55 +25,14 @@
</template>
<script>
import toolButton from "./tool-button";
import { mapState, mapMutations } from "vuex";
export default {
components: { toolButton },
props: {
showPreview: {
type: Boolean,
default: false
}
},
data() {
return {
toolButtonList: [
{
name: "bold",
format: "**"
},
{
name: "italic"
},
{
name: "baojiaquotation"
},
{
name: "code"
},
{
name: "lianjie"
},
{
name: "unorderedList"
},
{
name: "youxuliebiao"
},
{
name: "renwu"
},
{
name: "biaoge"
},
{
name: "fullScreen"
}
]
};
computed: {
...mapState(["toolButtonList", "isFocus", "showPreview"])
},
methods: {
toggleTab(type) {
this.$emit("update:showPreview", type === "preview");
}
...mapMutations(["setShowPreview"])
}
};
</script>
......@@ -83,7 +42,11 @@ export default {
justify-content: space-between;
align-items: center;
height: 32px;
transition: border-bottom 0.3s;
border-bottom: 1px solid var(--md-editor-theme-color);
&.active {
border-bottom: 1px solid var(--md-editor-theme-color-active);
}
.header_tabs {
display: flex;
justify-content: space-between;
......@@ -91,7 +54,7 @@ export default {
padding-bottom: 10px;
box-sizing: border-box;
.tab_item {
color: #666;
color: var(--md-editor-text-color);
cursor: pointer;
position: relative;
padding: 0 6px;
......@@ -110,14 +73,14 @@ export default {
}
&:hover {
color: #000;
color: var(--md-editor-text-color-active);
&::after {
width: 100%;
background: #666;
background: var(--md-editor-theme-color);
}
}
&.active {
color: #000;
color: var(--md-editor-text-color-active);
font-weight: 700;
&::after {
width: 100%;
......
<template>
<div @click="handleTool(info.name)" class="tool_button">
<span :class="['icon iconfont', `icon-${info.icon}`]"></span>
</div>
</template>
<script>
import { mapMutations, mapState } from "vuex";
import { formatText } from "@/assets/js/utils";
export default {
props: {
info: {
type: Object,
default: () => {}
}
},
computed: {
...mapState(["selectionInfo", "text", "ulNum"])
},
methods: {
...mapMutations(["setFullScreen", "setText", "setUlNum"]),
handleTool(type) {
switch (type) {
case "bold":
this.updateText("**", "**");
break;
case "italic":
this.updateText("_", "_");
break;
case "quote":
this.updateText("\n> ", "");
break;
case "code":
this.updateText("`", "`");
break;
case "link":
this.updateText("[", "](url)");
break;
case "ul":
this.updateText("\n- ", "");
break;
case "ol":
let ulNum = this.ulNum;
this.updateText(`\n${ulNum++}. `, "");
this.setUlNum(ulNum);
break;
case "task":
this.updateText("\n- [ ] ", "");
break;
case "table":
this.updateText(
`\n\n| header | header |\n| ------ | ------ |\n| cell | cell |\n| cell | cell |\n\n`,
""
);
break;
case "fullScreen":
this.setFullScreen(true);
break;
default:
break;
}
},
updateText(startStr, endStr) {
const selectionInfo = this.selectionInfo;
const originalText = this.text;
const newText = formatText(originalText, selectionInfo, startStr, endStr);
if (!newText) return;
this.setText(newText);
}
}
};
</script>
<style lang="less" scoped>
.tool_button {
.icon {
font-size: 18px;
color: var(--md-editor-text-color);
cursor: pointer;
&:hover {
color: var(--md-editor-text-color-active);
}
}
}
</style>
<template>
<div class="tool_button">
<span :class="['icon iconfont', `icon-${info.name}`]"></span>
</div>
</template>
<script>
export default {
props: {
info: {
type: Object,
default: () => {}
}
}
};
</script>
<style lang="less" scoped>
.tool_button {
.icon {
font-size: 18px;
color: #666;
cursor: pointer;
&:hover {
color: #000;
}
}
}
</style>
import Vue from "vue";
import App from "./App";
import store from "./store";
import "@/assets/style/global.less";
function initStyle(val) {
document.documentElement.style.setProperty(
"--md-editor-theme-color-active",
val
);
}
function MdEditor(obj) {
const { el, onChange } = obj;
const { el, onChange, themeActive } = obj;
if (!el || !document.querySelector(el)) throw new Error("请指定容器");
initStyle(themeActive);
new Vue({
store,
render: h =>
h(App, {
on: {
......
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {
fullScreen: false,
isFocus: false,
showPreview: false,
toolButtonList: [
{
name: "bold",
icon: "bold",
format: "**"
},
{
name: "italic",
icon: "italic"
},
{
name: "quote",
icon: "baojiaquotation"
},
{
name: "code",
icon: "code"
},
{
name: "link",
icon: "lianjie"
},
{
name: "ul",
icon: "unorderedList"
},
{
name: "ol",
icon: "youxuliebiao"
},
{
name: "task",
icon: "renwu"
},
{
name: "table",
icon: "biaoge"
},
{
name: "fullScreen",
icon: "fullScreen"
}
],
ulNum: 1,
text: "4564564564657567\n3456456456456",
selectionInfo: "",
html: ""
},
mutations: {
setFullScreen(state, val) {
state.fullScreen = val;
},
setShowPreview(state, val) {
state.showPreview = val;
},
setFocus(state, val) {
state.isFocus = val;
},
setText(state, val) {
state.text = val;
},
setSelectionInfo(state, val) {
state.selectionInfo = val;
},
setHtml(state, val) {
state.html = val;
},
setUlNum(state, val) {
state.ulNum = val;
}
}
});
......@@ -28,7 +28,7 @@ module.exports = {
},
{
test: /\.ttf$/,
test: /\.(ttf|woff|png)$/,
loader: "url-loader"
}
]
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册