动手写一个属于自己的 Markdown 编辑器

2020年04月07日 16:34 · 周二/阅读 474 次/评论 0 条

博客的后台一直没有找到适合的 Markdown 或带 WYSIWYG 的编辑器,所以暂时都是使用 textarea 顶着的 ?
在 Github 上面翻了许久,但是都没有发现一个适合自己的 Markdown 编辑器,特别是很多编辑器需要其他依赖和 Bootstrap 之类的样式,所以最终还是没有使用第三方的编辑器。
转过头来想想语法也不是特别麻烦,就打算自己造轮子,写一个适合自己使用的编辑器。

功能方面也是比较基础,但是已经涵盖了大部分正常文章所需的功能,主要有以下功能:

  • 格式:正文、加粗、斜体、删除线、行内代码、引用、分割线
  • 插入:代码块、链接、图片、表格(这个还没有完成…)
  • 列表:无序列表、有序列表
  • 功能:预览、全屏

编辑器样式方面则是自己想的,也是为了方便自己使用,虽然大多数编辑器都是长这样,实现也主要是通过 flex 布局和定位。
功能栏的图标是 Remixicon 的,这个图标库设计合理,简单易用,搭配 iconfont 就可以很方便地在项目中引入图标字体,而且这个图标库中涵盖了大多数开发所需的图标,墙裂推荐 👍

最终效果截图:

表单模式

表单模式

全屏模式

全屏模式

分栏预览模式

分栏预览模式

插入图片弹窗

插入图片

插入超链接弹窗

插入超链接

功能与实现

使用 textarea 来处理 Markdown 编辑器的功能按钮需要做一些调整,下面主要是一些实现的总结和记录。

因为是博客的主要框架是 Angular + Typescript,所以如果你的代码是纯 JS 的话,下方的代码可能需要修改修改才能用。

首先先上 HTML 部分:

<textarea [(ngModel)]="content"
          (keydown.tab)="onTextAreaPressTab($event)"
          (keyup.enter)="onTextAreaPressEnter($event)"
          (ngModelChange)="onTextAreaChange()"
          (mouseup)="onMouseOrKeyUp($event)"
          (keyup)="onMouseOrKeyUp($event)"
          #textarea></textarea>

获取 textarea 中光标的所在位置

需要获取 textarea 光标的所在位置比较简单,监听 textarea 的 keyupmouseup ,通过 event.target 获得触发事件的 textarea 元素,即可获得光标的位置,当然可能需要记录一些其他需要的值,比如选中长度和所处段落

Typescript 代码:

/**
* 在鼠标或者键盘抬起事件后获取选中的区域范围
* @param e - 鼠标或键盘事件
*/
onMouseOrKeyUp(e: KeyboardEvent) {
    const textarea = e.target; // 获取执行的 textarea
    this.editor.selection.start = textarea.selectionStart; // 获得光标选中的开始位置
    this.editor.selection.end = textarea.selectionEnd; // 获得光标选中的结束位置
    this.editor.selection.len = textarea.selectionEnd - textarea.selectionStart; // 获得光标选中的长度
    this.editor.selection.row = this.content ? this.content.substr(0, textarea.selectionStart).split('\n').length : 1; // 获取 textarea 光标在哪一行(段落)
}

解决 textarea 无法使用 Tab 缩进的问题

在 textarea 中使用按下按键 Tab 的触发效果,正常想应该是需要在文本插入 Tab 缩进,但是由于浏览器的机制,是切换到下一个 tabIndex 项(表单或者按钮),所以这边需要将 textarea 的默认事件给阻止掉:

Typescript 代码:

/**
* 在 textarea 中按下 tab 键时
* @param e - 鼠标或键盘事件
*/
onTextAreaPressTab(e: KeyboardEvent) {
    e.preventDefault(); // 阻止默认事件
    const indent = '  '; // 空格的数量任意,想缩进多少就设置多少个空格
    const separate = this.separate(); // 根据光标所在位置切割内容,获得 { before: string, after: string, selected: string }
    this.content = separate.before + indent + separate.after; // 组成内容
    this.setCursorPosition(this.editor.selection.start + indent.length); // 重新设置光标所在位置
}

实时预览 Markdown 效果

通过搭配开源库 markdown-it 就能很好的实现预览功能。
首先在项目中引入 markdown-it 库:

npm i markdown-it --save

创建一个 markdown-it 对象:

const md = new MarkdownIt({
    html: true, // 允许行内的 html
    typographer: true, // 允许印刷符号如 copyright 之类的符号
    breaks: true // 使用 <br /> 代替 \n
});
this.render = md.render(value); // 测试:将内容渲染成 Markdown

最后在 Angular 中通过监听 textarea 值的变化(这里可能需要防抖,通过使用 rxjs 的 debounceTime 管道),通过 innerHTML 绑定到html,需要注意的一点是:不使用 DomSanitizer 将危险的 HTML 文本通过 innerHTML 绑定,会出现标签属性被移除的问题,所以需要一个 safeHtml 管道来将危险的 HTML 净化。

DomSanitizer 可以把值净化为在不同 DOM 上下文中的安全内容,来帮你防范跨站脚本攻击(XSS)类的安全问题。 来自:Angular DomSanitizer

<div class="markdown-html markdown" #markdown [innerHTML]="(detail || null) | safeHtml"></div>

管道实现(safe-html.pipe.ts):

transform(value: string) {
    return this.sanitizer.bypassSecurityTrustHtml(value);
}

在 textarea 中的某一行(段落)最前面插入 Markdown 符号

首先获取光标所在的位置(没有任何触发,默认参数:起点 0、终点 0、长度 0、行 1),使用 split('\n') 将 textarea 的值切割成数组,获取所有的段落,通过修改当前行的值并通过 join('\n') 重新组合成新的值即可:

const rows = this.content.split('\n'); // 获得由段落组成的数组
const row = this.editor.selection.row - 1; // this.editor.selection.row 当前光标所在行,按键或者鼠标触发监听的结果
rows[row] = '# '+ rows[row]; // 比如插入个 h1 标题
this.content = rows.join('\n'); // 将所有的段落 join 一遍

小结

写下来其实都是些比较基础的代码,其中很多也应该可以再优化一下。相比其他很多开源库,可能功能不够齐全,但是使用自己实现的可以更加去贴合自己的使用习惯,做出各种自定义的功能。
目前这一篇文章就是通过这个编辑器发布的,总体使用下来效果还是可以的。

分类:笔记

更新 :2021年03月01日 19:13 · 周一

版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明出处!

文章评论 (0)

暂无评论