前言
最近我在项目中需要实现一个 markdown编辑器 的需求,并且是以React
为开发基础的, 类似就可以。
在网上也一顿查找了一番,要么是vue的,要么源码拿来也用不了,所以。。。
需要实现的功能
我们自己实现的话,看看需要支持哪些功能,因为做一个初版的简易编辑器,所以功能实现得不会太多:
markdown语法解析,并实时渲染
markdown主题css样式
代码块高亮展示
编辑器工具栏中图片上传功能的实现(预览图片是带token才能预览)
这里先放上我最终实现好了的效果图:
1、安装依赖
npm install marked
npm install highlight.js
2、允许直接使用css, 在config.js中加入
context.resourcePath.includes('css') ||
3、创建index.js
// index.js
import { useState, useEffect } from 'react';
import { Icon, Modal, Upload, message, Button, Card, Form } from 'antd';
import { marked } from 'marked';
import hljs from 'highlight.js';
import './github-dark.css';
import './index.css';
import index from 'memoize-one';
class Marked extends React.Component {
state = {
text: '',
visible: false,
curTab: 'articles'
}
docInput = null
// 更改当前文档
changeCurrentArticle = async (url) => {
const res = await fetch(url);
const content = await res.text();
// console.log(content)
return content;
}
// 更改当前文档
getActicle = () => {
const fs = require('fs');
console.log(fs)
fs.readFile('E:/新建文件夹/宇信科技/ant-design-pro-2/public/ok.md', 'utf8', function (err, dataStr) {
if (err) {
return console.log("读取文件失败!" + err.message)
}
console.log("读取文件成功!" + dataStr)
})
}
componentDidMount() {
// 如果有md格式文件内容,先要直接展示出来
const { defaultContent } = this.props;
// 配置highlight
hljs.configure({
tabReplace: '',
classPrefix: 'hljs-',
languages: ['CSS', 'HTML', 'JavaScript', 'Python', 'TypeScript', 'Markdown'],
});
// 配置marked
marked.setOptions({
renderer: new marked.Renderer(),
highlight: code => hljs.highlightAuto(code).value,
gfm: true, //默认为true。 允许 Git Hub标准的markdown.
tables: true, //默认为true。 允许支持表格语法。该选项要求 gfm 为true。
breaks: true, //默认为false。 允许回车换行。该选项要求 gfm 为true。
});
this.docInput = document.getElementById("inputDiv");
// 图片加载成功时触发load事件,加载失败时触发error事件,成功不会触发
document.addEventListener("error", function (event) {
var ev = event ? event : window.event;
var elem = ev.target;
const token = "1213";
if (elem.tagName.toLowerCase() == 'img') {
var url = elem.getAttribute('src');
var request = new XMLHttpRequest();
request.responseType = 'blob';
request.open('get', `${url}`, true);
// 添加header里的token
request.setRequestHeader('actoken', token);
// 成功加载图片后把结果赋值给elem.src
// request.onreadystatechange = e => {
// if (request.readyState == XMLHttpRequest.DONE && request.status == 200) {
// elem.src = URL.createObjectURL(request.response);
// elem.onload = () => {
// URL.revokeObjectURL(elem.src);
// }
// }
// };
request.send(null);
// 图片加载失败 --替换为默认
// elem.src = "../img/default.jpg";
}
}, true);
// 初始值设置
if (defaultContent) {
this.docInput.innerText = defaultContent;
this.setState({
text: defaultContent
})
}
}
/**
* @param {string} name - 操作按钮类型
*/
handleIcons = (name) => {
if (name === 'picture') {
this.setState({ visible: true })
return;
}
}
setPicInfo = (name, path) => {
// 如果返回的是可以直接访问的地址就最好了,但我这里的预览地址不能直接访问,需要token
const picUro = `/api/center/previewImage?filePath=${path}`;
const resP = `![${name}](${picUro})`;
// 编辑框设置焦点
this.docInput.focus()
// 获取选定对象
var selection = window.getSelection();
// 判断是否有最后光标对象存在
if (this.lastEditRange) {
// 存在最后光标对象,选定对象清除所有光标并添加最后光标还原之前的状态
selection.removeAllRanges();
selection.addRange(this.lastEditRange)
}
if (selection.anchorNode.nodeName != '#text') {
// 如果是编辑框范围。则创建表情文本节点进行插入
var emojiText = document.createTextNode(resP)
const len = this.docInput.childNodes.length;
if (len) {
// 如果文本框的子元素大于0,则表示有其他元素,则按照位置插入表情节点
for (var i = 0; i < len; i++) {
if (i == selection.anchorOffset) {
this.docInput.insertBefore(emojiText, edit.childNodes[i])
}
}
} else {
// 否则直接插入一个表情元素
this.docInput.appendChild(emojiText)
}
} else {
// 如果是文本节点则先获取光标对象
var range = selection.getRangeAt(0)
// 获取光标对象的范围界定对象,一般就是textNode对象
var textNode = range.startContainer;
// 获取光标位置
var rangeStartOffset = range.startOffset;
// 文本节点在光标位置处插入新的表情内容
textNode.insertData(rangeStartOffset, resP)
// 光标移动到到原来的位置加上新内容的长度
range.setStart(textNode, rangeStartOffset + resP.length)
// 光标开始和光标结束重叠
range.collapse(true)
// 清除选定对象的所有光标对象
selection.removeAllRanges()
// 插入新的光标对象
selection.addRange(range)
}
// 无论如何都要记录最后光标对象
this.lastEditRange = selection.getRangeAt(0)
this.setState({
text: this.docInput.innerText
})
}
beforeUpload = (file) => {
const { dispatch } = this.props;
return new Promise((resolve, reject) => {
const typeF = file.type;
if (!['image/jpeg', 'image/png', 'image/gif', 'image/jpg'].includes(typeF)) {
message.error('只能选择jpeg、png、gif、jpg格式的图文文件');
}
const formData = new FormData();
formData.append('file', file);
// 调接口获得当前上传图片的path
// dispatch({
// type: '',
// payload: formData,
// callback: res => {
// }
// })
const res = '1213.jpg';
this.setPicInfo(file.name, res);
}).then(() => {
reject();
})
}
render() {
const { text } = this.state;
return (
<>
{/* 顶部图片上传按钮 */}
this.handleIcons('picture')} />
{/* markdown编辑和预览区域 */}
{/* 左侧markdown编辑区域 */}
{
this.setState({
text: e.target.innerText
})
}}
>
{/* 右侧markdown预览区域 */}
/g, ""),
}}
>
this.setState({ visible: false })}
onCancel={() => this.setState({ visible: false })}
>
,
>
);
}
};
export default Marked;
4、创建index.css
/* html,
body,
#root {
height: 100%;
margin: 0;
padding: 0;
} */
.header {
background-color: #d9d9d9;
border-bottom: 1px solid #ccc;
/* font-size: 30px; */
height: 50px;
display: flex;
/* justify-content: center; */
align-items: center;
/* border-bottom: 1px solid #eee; */
}
.header .input-icon {
font-size: 22px;
padding: 20px;
cursor: pointer;
}
.marked {
/* min-height: calc(100vh - 60px); */
height: 500px;
display: flex;
}
.input-region {
/* background-color: rgb(233, 233, 233); */
outline: none;
}
.input-region,
.show-region {
flex: 1;
padding: 20px;
font-size: 20px;
}
/* ——————————————————————————————显示区markdown样式—————————————————————————————— */
/* 左右区域纵向滚动条 */
.markdownStyle {
overflow-y: auto;
border: 1px solid #ccc;
}
/* 表格 */
.markdownStyle table {
border: 1px solid #000;
border-spacing: 0;
/*去掉单元格间隙*/
}
.markdownStyle table th,
.markdownStyle table td {
padding: 6px 14px;
border: 1px solid #000;
}
/* 行内代码 */
.markdownStyle code {
color: red;
background-color: #f9f2f4;
padding: 0 4px;
}
/* 图片 */
.markdownStyle p img {
display: block;
max-width: 100%;
margin: 14px auto;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
/* ———————————————————————————————————————————————————————————— */
5、创建github-dark.css
#hljs {
padding: 12px;
color: #c9d1d9;
background: #0d1117;
border-radius: 12px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
}
code {
font-family: 'FiraCode';
}
/* 代码片段 */
#hljs code {
color: #c9d1d9;
background: #0d1117;
padding: 0;
font-size: 16px;
}
.hljs-doctag,
.hljs-keyword,
.hljs-meta .hljs-keyword,
.hljs-template-tag,
.hljs-template-variable,
.hljs-type,
.hljs-variable.language_ {
/* prettylights-syntax-keyword */
color: #ff7b72;
}
.hljs-title,
.hljs-title.class_,
.hljs-title.class_.inherited__,
.hljs-title.function_ {
/* prettylights-syntax-entity */
color: #d2a8ff;
}
.hljs-attr,
.hljs-attribute,
.hljs-literal,
.hljs-meta,
.hljs-number,
.hljs-operator,
.hljs-variable,
.hljs-selector-attr,
.hljs-selector-class,
.hljs-selector-id {
/* prettylights-syntax-constant */
color: #79c0ff;
}
.hljs-regexp,
.hljs-string,
.hljs-meta .hljs-string {
/* prettylights-syntax-string */
color: #a5d6ff;
}
.hljs-built_in,
.hljs-symbol {
/* prettylights-syntax-variable */
color: #ffa657;
}
.hljs-comment,
.hljs-code,
.hljs-formula {
/* prettylights-syntax-comment */
color: #8b949e;
}
.hljs-name,
.hljs-quote,
.hljs-selector-tag,
.hljs-selector-pseudo {
/* prettylights-syntax-entity-tag */
color: #7ee787;
}
.hljs-subst {
/* prettylights-syntax-storage-modifier-import */
color: #c9d1d9;
}
.hljs-section {
/* prettylights-syntax-markup-heading */
color: #1f6feb;
font-weight: bold;
}
.hljs-bullet {
/* prettylights-syntax-markup-list */
color: #f2cc60;
}
.hljs-emphasis {
/* prettylights-syntax-markup-italic */
color: #c9d1d9;
font-style: italic;
}
.hljs-strong {
/* prettylights-syntax-markup-bold */
color: #c9d1d9;
font-weight: bold;
}
.hljs-addition {
/* prettylights-syntax-markup-inserted */
color: #aff5b4;
background-color: #033a16;
}
.hljs-deletion {
/* prettylights-syntax-markup-deleted */
color: #ffdcd7;
background-color: #67060c;
}
/* .hljs-char.escape_,
.hljs-link,
.hljs-params,
.hljs-property,
.hljs-punctuation,
.hljs-tag {
} */