jsqip 2019-07-01
笔者在18年年末的时候接到一个开发任务——搭建一个AI项目的开放平台,其中的产品文档为转化为HTML格式的markdown文档。考虑到文档的即时更新,将文档信息做成了Ajax接口的形式。因此管理后台只需将textarea表单的内容通过markdown解析器进行HTML格式转化,然后将markdown内容和经转化的HTML文档都保存到数据库即可。
基本需求完成后,为了更好的用户体验,考虑将常用的编辑功能添加进来。改进版不仅支持了常用的文本编辑功能,还实现的UI界面的配置化。本着造福伸手党的目的,以及积累些开源经验,笔者将该react 组件 react-markdown-editor-lite 进行了封装改造,并且发布到了开源社区。
在线体验 https://harrychen0506.github.io/react-markdown-editor-lite/
大多数常见的编辑器,包括富文本编辑器,利用了某些元素如div的contenteditable属性,配合selection、range、execCommand等API,实现了富文本编辑功能。这里面的实现比较复杂,所以有了"为什么都说富文本编辑器是天坑?"这个说法。
而markdown编辑器,核心的处理内容为简单语法的纯文本,复杂度相对来说比较低,并且input标签自带onSelect事件,可以很方便的获取选择信息(选择起始位置和选择文本值),因此要想实现编辑功能,只需将要改动的内容进行文本转换,然后进行重新拼接首尾,大功告成。
考察了几个社区流行的markdown解析器,比较流行的有markdown, markdown-it, marked 等等。综合考虑扩展性以及稳定性,笔者选择了markdown-it作为markdown的词法解析器,结果也比较满意。
当选择分栏编辑的时候,滚动左侧的编辑区,右侧的预览区能自动滚动到对应的区域。方案参考了《手把手教你用 100行代码实现基于 react的 markdown 输入 + 即时预览在线编辑器(一)》。只需先计算出输入框容器元素与预览框容器元素之间最大scroll范围的比例值,然后根据主动滚动元素自身的scrollTop做相应的比例换算,即可知道对方区域的scrollTop值。
关于UI
npm install react-markdown-editor-lite --save
Property | Description | Type | default | Remarks |
---|---|---|---|---|
value | markdown content | String | '' | |
style | component container style | Object | {height: '100%'} | |
config | component config | Object | {view: {...}, logger: {...}} | |
config.view | component UI | Object | {menu: true, md: true, html: true} | |
config.imageUrl | default image url | String | '' | |
config.linkUrl | default link url | String | '' | |
config.logger | logger in order to undo or redo | Object | {interval: 3000} | |
onChange | emitting when editor has changed | Function | ({html, md}) => {} |
'use strict'; import React from 'react' import ReactDOM from 'react-dom' import MdEditor from 'react-markdown-editor-lite' const mock_content = "Hello.\n\n * This is markdown.\n * It is fun\n * Love it or leave it." export default class Demo extends React.Component { mdEditor = null handleEditorChange ({html, md}) { console.log('handleEditorChange', html, md) } handleGetMdValue = () => { this.mdEditor && alert(this.mdEditor.getMdValue()) } handleGetHtmlValue = () => { this.mdEditor && alert(this.mdEditor.getHtmlValue()) } render() { return ( <div> <nav> <button onClick={this.handleGetMdValue} >getMdValue</button> <button onClick={this.handleGetHtmlValue} >getHtmlValue</button> </nav> <section style="height: 500px"> <MdEditor ref={node => this.mdEditor = node} value={mock_content} style={{height: '400px'}} config={{ view: { menu: true, md: true, html: true }, imageUrl: 'https://octodex.github.com/images/minion.png' }} onChange={this.handleEditorChange} /> </section> </div> ) } }
欢迎大家使用和反馈,项目地址 (https://github.com/HarryChen0..., 你的点赞将是我莫大的动力