为 Vue 构建你自己的所见即所得 Markdown 编辑器 📝👀
入门
将“富”融入“富文本”
添加 markdown
总结
HTML5 和现代 JavaScript 让很多事情比过去简单得多。复杂的事情不再需要大量的 hack,很多功能都是现成的。
市面上有很多现成的所见即所得 (WYSIWYG,所见即所得,又称“富文本”)编辑器,比如CKEditor。它们提供了大量的功能以及各种框架的指南、特性和插件,但它们的代码库通常非常庞大。我的意思是,CKEditor 5 的代码库里有大约 2000 个 JS 文件,总计约 30 万行代码——这真是令人难以置信,不是吗?
而且可能没有必要:大多数用例不需要 PDF 或 Word 导出、实时协作、数学和化学集成、修订、自动创建参考书目或功能齐全的 Excel 克隆。当您只需要一些基本的文本编辑功能时,为什么不一次性构建自己的所见即所得编辑器呢?
在这篇文章中,我将解释如何为 Vue 创建自己的所见即所得的 markdown 编辑器!
入门
该编辑器将使用 markdown:它是一种简单的语法,可以按照我想要的方式设置样式,并且比纯 HTML 更安全地保存和再次输出。
首先,我需要几个包,分别是@ts-stack/markdown和turndown,它们分别@ts-stack/markdown
用来将 Markdown 显示为 HTML 和turndown
将 HTML 转换回 Markdown。
接下来,我创建一个支持 的基本 Vue 组件v-model
并将其命名为WysiwygEditor.vue
。我已经可以在这里使用<div>
带有 属性 的 了contenteditable
。我还添加了一些 Tailwind 样式,让它看起来更美观。
<!-- WysiwygEditor.vue -->
<template>
<div>
<div
@input="onInput"
v-html="innerValue"
contenteditable="true"
class="wysiwyg-output outline-none border-2 p-4 rounded-lg border-gray-300 focus:border-green-300"
/>
</div>
</template>
<script>
export default {
name: 'WysiwygEditor',
props: ['value'],
data() {
return {
innerValue: this.value
}
},
methods: {
onInput(event) {
this.$emit('input', event.target.innerHTML)
}
}
}
</script>
现在可以像这样使用该组件:
<!-- Some other component -->
<template>
<!-- ... -->
<wysiwyg-editor v-model="someText" />
<!-- ... -->
</template>
<!-- ... -->
这看起来像这样:
现在,div 的行为基本上与 a 类似,textarea
但有一点不同:它会生成 HTML。
将“富”融入“富文本”
你可能知道一些按钮,它们可以用来给文本加粗、斜体或加下划线,以及在 Google Docs 或 Word 等程序中添加列表、标题等。接下来,我们来添加这些按钮。为此,我安装了Fontawesome 图标,并将这些按钮添加到 textarea-div 的正上方。但首先:一些样式:
.button {
@apply border-2;
@apply border-gray-300;
@apply rounded-lg;
@apply px-3 py-1;
@apply mb-3 mr-3;
}
.button:hover {
@apply border-green-300;
}
我稍后会添加点击监听器并实现所使用的方法。
<!-- WysiwygEditor.vue -->
<template>
<!-- ... -->
<div class="flex flex-wrap">
<button @click="applyBold" class="button">
<font-awesome-icon :icon="['fas', 'bold']" />
</button>
<button @click="applyItalic" class="button">
<font-awesome-icon :icon="['fas', 'italic']" />
</button>
<button @click="applyHeading" class="button">
<font-awesome-icon :icon="['fas', 'heading']" />
</button>
<button @click="applyUl" class="button">
<font-awesome-icon :icon="['fas', 'list-ul']" />
</button>
<button @click="applyOl" class="button">
<font-awesome-icon :icon="['fas', 'list-ol']" />
</button>
<button @click="undo" class="button">
<font-awesome-icon :icon="['fas', 'undo']" />
</button>
<button @click="redo" class="button">
<font-awesome-icon :icon="['fas', 'redo']" />
</button>
</div>
<!-- ... -->
</template>
<!-- ... -->
编辑器现在看起来像这样:
太棒了!现在我需要给这个东西添加实际功能。为此,我将使用document.execCommand
,它或多或少是为创建所见即所得的编辑器而设计的。尽管 MDN 声明此功能已被弃用,但大多数浏览器仍然提供一些支持,因此对于最基本的功能来说,它应该仍然可以使用。
让我们实现这个applyBold
方法:
methods: {
// ...
applyBold() {
document.execCommand('bold')
},
// ...
}
好的,这很简单。现在剩下的:
// ...
applyItalic() {
document.execCommand('italic')
},
applyHeading() {
document.execCommand('formatBlock', false, '<h1>')
},
applyUl() {
document.execCommand('insertUnorderedList')
},
applyOl() {
document.execCommand('insertOrderedList')
},
undo() {
document.execCommand('undo')
},
redo() {
document.execCommand('redo')
}
// ...
这里唯一弹出的方法是applyHeading
,因为我需要在这里明确指定我想要的元素。有了这些命令,我可以继续稍微调整一下输出的样式:
.wysiwyg-output h1 {
@apply text-2xl;
@apply font-bold;
@apply pb-4;
}
.wysiwyg-output p {
@apply pb-4;
}
.wysiwyg-output p {
@apply pb-4;
}
.wysiwyg-output ul {
@apply ml-6;
@apply list-disc;
}
.wysiwyg-output ol {
@apply ml-6;
@apply list-decimal;
}
完成的编辑器(包含一些示例内容,如下所示:
为了让事情表现得更好一些,我还需要将空段落设置为空内容的默认值,并将默认的“换行符”也设置为段落:
// ...
data() {
return {
innerValue: this.value || '<p><br></p>'
}
},
mounted() {
document.execCommand('defaultParagraphSeparator', false, 'p')
},
// ...
添加 markdown
所以,我想把 Markdown放进编辑器,然后再从编辑器里取出 Markdown 。我先定义一些 Markdown 字符串,看看会发生什么:
# Hello, world!
**Lorem ipsum dolor** _sit amet_
* Some
* Unordered
* List
1. Some
1. Ordered
1. List
是的,什么也没发生。还记得@ts-stack/markdown
我之前安装的库吗?让我们使用它:
import { Marked } from '@ts-stack/markdown'
export default {
name: 'WysiwygEditor',
props: ['value'],
data() {
return {
innerValue: Marked.parse(this.value) || '<p><br></p>'
}
},
// ...
现在输入将被呈现为 HTML:
太棒了!现在,为了从组件中获取 Markdown ,我使用turndown
:
import TurndownService from 'turndown'
export default {
// ...
methods: {
onInput(event) {
const turndown = new TurndownService({
emDelimiter: '_',
linkStyle: 'inlined',
headingStyle: 'atx'
})
this.$emit('input', turndown.turndown(event.target.innerHTML))
},
// ...
让我们通过在预先格式化的 div 中输出我们收到的 markdown 来查看它是否有效:
<!-- Some other component -->
<template>
<!-- ... -->
<wysiwyg-editor v-model="someText" />
<pre class="p-4 bg-gray-300 mt-12">{{ someText }}</pre>
<!-- ... -->
</template>
太棒了!完成了!我们来测试一下:
似乎有效!
作为参考,这里是整个组件:
<template>
<div>
<div class="flex flex-wrap">
<button @click="applyBold" class="button">
<font-awesome-icon :icon="['fas', 'bold']" />
</button>
<button @click="applyItalic" class="button">
<font-awesome-icon :icon="['fas', 'italic']" />
</button>
<button @click="applyHeading" class="button">
<font-awesome-icon :icon="['fas', 'heading']" />
</button>
<button @click="applyUl" class="button">
<font-awesome-icon :icon="['fas', 'list-ul']" />
</button>
<button @click="applyOl" class="button">
<font-awesome-icon :icon="['fas', 'list-ol']" />
</button>
<button @click="undo" class="button">
<font-awesome-icon :icon="['fas', 'undo']" />
</button>
<button @click="redo" class="button">
<font-awesome-icon :icon="['fas', 'redo']" />
</button>
</div>
<span class="nt"><div</span>
<span class="err">@</span><span class="na">input=</span><span class="s">"onInput"</span>
<span class="na">v-html=</span><span class="s">"innerValue"</span>
<span class="na">contenteditable=</span><span class="s">"true"</span>
<span class="na">class=</span><span class="s">"wysiwyg-output outline-none border-2 p-4 rounded-lg border-gray-300 focus:border-green-300"</span>
<span class="nt">/></span>
</div>
</template>
<script>
import { Marked } from '@ts-stack/markdown'
import TurndownService from 'turndown'
export default {
name: 'WysiwygEditor',
props: ['value'],
data() {
return {
innerValue: Marked.parse(this.value) || '<p><br></p>'
}
},
mounted() {
document.execCommand('defaultParagraphSeparator', false, 'p')
},
methods: {
onInput(event) {
const turndown = new TurndownService({
emDelimiter: '_',
linkStyle: 'inlined',
headingStyle: 'atx'
})
this.$emit('input', turndown.turndown(event.target.innerHTML))
},
applyBold() {
document.execCommand('bold')
},
applyItalic() {
document.execCommand('italic')
},
applyHeading() {
document.execCommand('formatBlock', false, '<h1>')
},
applyUl() {
document.execCommand('insertUnorderedList')
},
applyOl() {
document.execCommand('insertOrderedList')
},
undo() {
document.execCommand('undo')
},
redo() {
document.execCommand('redo')
}
}
}
</script>
总结
这很有趣。87 行 Vue 代码就能实现一个所见即所得的编辑器,相当小巧。组件的行为类似于输入框,v-model
这更加方便。在我看来,对于一个业余项目来说,这个编辑器足以应付内容不多的小规模情况。
不过,在客户项目中,我更倾向于使用现成的解决方案,因为它更易于维护,功能更强大,支持也更好。不过,构建这个项目本身就是一次很棒的学习机会!
希望你喜欢阅读这篇文章,就像我喜欢写它一样!如果喜欢,请留下❤️或🦄 !我空闲时间会写科技文章,偶尔也喜欢喝咖啡。
如果您想支持我的努力, 请给我买杯咖啡☕ 或 在 Twitter 上关注我🐦 !
文章来源:https://dev.to/thormeier/build-your-own-wysiwyg-markdown-editor-for-vue-318j