heather


发表于 2019-08-18 15:30


heather

heather是一款web端的在线markdown编辑器

说明

markdown编辑器,特性如下:

  1. 支持mermaid图表、katex
  2. 自定义工具条|辅助工具条
  3. 支持拖曳html|md文件,自动将html转化为markdown
  4. 本地存储(localStorage)支持
  5. 编辑预览同步滚动
  6. 主题设置
  7. 支持手机端
  8. 离线访问
  9. mermaid、katex懒加载
  10. 全屏编辑
  11. 文件上传
  12. 快速编辑表格
  13. 所见即所得编辑(alpha)

在线demo

https://md.qyh.me

源码

https://github.com/mhlx/heather
https://gitee.com/wwwqyhme/heather

使用

https://github.com/mhlx/heather 下载最新的文件

引入css

<link rel="stylesheet"  href="codemirror/lib/codemirror.css" media="screen">
<link rel="stylesheet" 
   href="codemirror/addon/scroll/simplescrollbars.css" media="screen">
<link rel="stylesheet"  href="css/markdown.css" media="all">
<link href="fontawesome-free/css/all.min.css" rel="stylesheet" media="screen">
<link rel="stylesheet" href="css/style.css" media="screen">
<link rel="stylesheet" href="css/print.css" media="print">

引入js

<script src="jquery/jquery.min.js"></script>
<script src="js/htmlparser.js"></script>
<script src="js/morphdom-umd.min.js"></script>
<script src="codemirror/lib/codemirror.js"></script>
<script src="codemirror/addon/mode/overlay.js"></script>
<script src="codemirror/mode/javascript/javascript.js"></script>
<script src="codemirror/mode/xml/xml.js"></script>
<script src="codemirror/mode/markdown/markdown.js"></script>
<script src="codemirror/mode/gfm/gfm.js"></script>
<script src="codemirror/addon/selection/mark-selection.js"></script>
<script src="codemirror/addon/search/searchcursor.js"></script>
<script src="codemirror/addon/search/search.js"></script>
<script src="codemirror/addon/edit/continuelist.js"></script>
<script src="codemirror/addon/scroll/simplescrollbars.js"></script>
<script src="sweet2alert/dist/sweetalert2.all.min.js"></script>
<script src="js/turndown.js"></script>
<script src="js/turndown-plugin-gfm.js"></script>
<script src="highlight/highlight.pack.js"></script>
<script src="js/jquery.touchwipe.min.js"></script>
<script src="js/md.js"></script>
<script src="js/all.js"></script>

创建编辑器

var config = {};
var wrapper = EditorWrapper.create(config);

完整代码

<!DOCTYPE html>
<html lang="zh-CN">
   <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport"
         content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
      <meta name="description" content="Heather is a online Markdown Editor">
      <meta name="keywords" content="Markdown, Heather, Editor, CodeMirror, Github, Open Source">
      <link rel="stylesheet"  href="codemirror/lib/codemirror.css" media="screen">
      <link rel="stylesheet" 
       href="codemirror/addon/scroll/simplescrollbars.css" media="screen">
      <link rel="stylesheet"  href="css/markdown.css" media="all">
      <link href="fontawesome-free/css/all.min.css" rel="stylesheet" media="screen">
      <link rel="stylesheet" href="css/style.css" media="screen">
      <link rel="stylesheet" href="css/print.css" media="print">
   </head>
   <body>
      <script src="jquery/jquery.min.js"></script>
      <script src="js/htmlparser.js"></script>
      <script src="js/morphdom-umd.min.js"></script>
      <script src="codemirror/lib/codemirror.js"></script>
      <script src="codemirror/addon/mode/overlay.js"></script>
      <script src="codemirror/mode/javascript/javascript.js"></script>
      <script src="codemirror/mode/xml/xml.js"></script>
      <script src="codemirror/mode/markdown/markdown.js"></script>
      <script src="codemirror/mode/gfm/gfm.js"></script>
      <script src="codemirror/addon/selection/mark-selection.js"></script>
      <script src="codemirror/addon/search/searchcursor.js"></script>
      <script src="codemirror/addon/search/search.js"></script>
      <script src="codemirror/addon/edit/continuelist.js"></script>
      <script src="codemirror/addon/scroll/simplescrollbars.js"></script>
      <script src="sweet2alert/dist/sweetalert2.all.min.js"></script>
      <script src="js/turndown.js"></script>
      <script src="js/turndown-plugin-gfm.js"></script>
      <script src="highlight/highlight.pack.js"></script>
      <script src="js/jquery.touchwipe.min.js"></script>
      <script src="js/md.js"></script>
      <script src="js/all.js"></script>
      <script>
        var config = {};
        var wrapper = EditorWrapper.create(config);
        //https://www.qyh.me/space/web/article/598
        var userAgent = navigator.userAgent;
        if(userAgent.indexOf('Windows NT 10.0') != -1 && userAgent.indexOf('Firefox') != -1){
            var textarea = wrapper.editor.getWrapperElement().querySelector('textarea');
            textarea.style.width = "1000px";
        }
      </script>
   </body>
</html>

其他

  1. codemirror.js codemirror核心文件 本地处理过,请勿使用cdn
  2. md.js markdown-it 本地处理过,请勿使用cdn
  3. 一个页面只能存在一个EditorWrapper实例,创建另一个EditorWrapper实例时会自动销毁已经存在的实例
  4. 编辑器不会替代任何你页面的html元素,它只会额外创建一些html元素,它始终是全屏的,并且无法改变
  5. EditorWrapper依赖以下html元素(初始化实例时自动创建),请保证ID的唯一性
<div id="editor_wrapper">
  <div id="editor_toc"></div>
  <div id="editor_in">
    <div id="editor_toolbar"></div>
    <textarea  style="width: 100%; height: 100%"></textarea>
    <div id="editor_stat"></div>
    <div id="editor_innerBar"></div>
  </div>
  <div class="markdown-body " id="editor_out" ></div>
</div>

配置

配置项 说明 默认值
toolbar_icons 配置顶部工具条图标 [ 'toc','innerBar','backup','new','search','config' ]
backupEnable 是否开启备份
backup_autoSaveMs 当配置backupEnable为true时生效,当编辑器内容发生变更时,多少毫秒后自动保存编辑器内容 500
innerBar_icons 配置辅助工具条的图标 ['heading','bold','italic','quote','strikethrough','link','code','code-block','uncheck','check','table','undo','redo','close']
render_allowHtml 是否允许html标签 false
render_plugins 配置markdown渲染支持的插件 ['footnote','katex','mermaid','anchor','task-lists','sup','sub','abbr']
render_beforeRender 配置在渲染前元素处理器
render_ms 当编辑器内容发生变更时,多少毫秒后开始渲染 500
stat_enable 是否启用状态条 true
stat_formatter 当启用状态条时,用于获取状态条渲染内容的方法
sync_animateMs 配置同步滚动时滚动动画时间 0
sync_enable 是否启用同步滚动
swipe_animateMs 预览、编辑、toc切换时动画时间 500
renderAllDocEnable 代码高亮的同步预览中,由于codemirror只渲染当前视窗,因此会出现不同步的现象,开启这个选项在载入文档时可以消除这个现象,但是在文本量比较大的时候,加载非常缓慢,可以选择关闭 true
disableWysiwyg 是否禁用所见即所得编辑模式(只有在全屏模式 手机端工作)

方法

编辑器

获取codemirror对象

wrapper.editor

获取编辑器内容

wrapper.getValue()

设置编辑器内容

wrapper.setValue(newValue)

获取html内容

wrapper.getHtml()

渲染内容

wrapper.doRender(patch) patch:是否patch更新

同步滚动条

wrapper.doSync()

启用编辑和预览的同步

wrapper.enableSync()

停用编辑和预览的同步

wrapper.disableSync()

切换到编辑器页面

wrapper.toEditor()

切换到TOC页面

wrapper.toToc()

切换到预览页面

wrapper.toPreview()只在手机端或者全屏模式下有效

顶部工具栏

wrapper.toolbar

添加一个图标

wrapper.toolbar.addIcon(clazz,hander,callback)

clazz为fontawesome图标的样式,例如fa fa-file icon,handler为图标被点击时触发的方法,callback则为图标元素的回调,例如为添加的图标加上id属性:

wrapper.toolbar.addIcon(clazz,hander,function(icon){
  icon.setAttribute(id,'icon-id');
})
额外的图标样式
  1. mobile-hide在手机端隐藏
  2. pc-hide在pc端隐藏
  3. nofullscreen在全屏模式下隐藏
  4. onfullscreen 只在全屏模式下出现

删除图标

wrapper.toolbar.removeIcon(function(icon){return bool})

隐藏

wrapper.toolbar.hide()

显示

wrapper.toolbar.show()

保持隐藏状态

wrapper.toolbar.keepHidden = true;
wrapper.toolbar.hide();

辅助工具栏

wrapper.innerBar

方法同顶部工具条

主题

wrapper.theme

配置编辑器主题

var theme = wrapper.theme;
theme.setEditorTheme(wrapper.editor,'abcdef',function(){
	theme.render();
})

配置代码高亮主题

var theme = wrapper.theme;
theme.hljs.theme = 'a11y-light'
theme.render();

配置顶部工具条颜色

var theme = wrapper.theme;
theme.toolbar.color = '#fff'
theme.render();

配置辅助工具条颜色

var theme = wrapper.theme;
theme.bar.color = '#fff'
theme.render();

配置状态条字体颜色

var theme = wrapper.theme;
theme.stat.color = '#fff'
theme.render();

自定义css

var css = '';
wrapper.theme.customCss = css;
wrapper.theme.render();

保存主题

wrapper.saveTheme();

重置主题

wrapper.resetTheme();

执行指令

wrapper.execCommand(commandName)

系统内置指令

名称 说明 快捷键_mac 快捷键_pc
search 开启|关闭一个搜索框 Ctrl S Alt S
emoji 打开emoji选择框
heading 打开heading选择框 Ctrl H Ctrl H
bold 加粗文本 Ctrl B Ctrl B
italic 倾斜文本 Ctrl I Ctrl I
quote 引用文本 Ctrl Q Ctrl Q
strikethrough 为文本添加删除线
link 插入超链接 Ctrl L Ctrl L
codeBlock 插入代码块 Shift Cmd B Alt B
code 插入行内代码
uncheck 插入待办事项 Shift Cmd U Alt U
check 插入已完成待办事项 Shift Cmd I Alt I
undo 撤回 Cmd Z Ctrl Z
redo 取消撤回 Cmd Y Ctrl Y
table 插入表格 Shift Cmd T Alt T

新增指令

EditorWrapper.commands[commandName] = commandHandler commandHandler接受一个参数wrapper,例如:

新增一个插入图片的指令,并且为该指令绑定Ctrl+G的快捷键:

EditorWrapper.commands['image'] = function(wrapper){
  var editor = wrapper.editor;
  var text = editor.getSelection();
  if (text == '') {
      editor.replaceRange("![]()", editor.getCursor());
      editor.focus();
      var start_cursor = editor.getCursor();
      var cursorLine = start_cursor.line;
      var cursorCh = start_cursor.ch;
      editor.setCursor({
          line: cursorLine,
          ch: cursorCh - 1
      });
  } else {
      editor.replaceSelection("![" + text + "]()");
  }
}
wrapper.bindKey({'Ctrl-G':'image'});

快捷键绑定

wrapper.bindKey(keyMap)keyMap对象属性为快捷键名称,例如Ctrl-A,属性值可以为string或者一个方法,如果为string类型,那么则会绑定对应名称的命令,如果为方法,则会直接绑定该方法

解除快捷键绑定

wrapper.unbindKey(keyArray) keyArray为快捷键数组,例如['Ctrl-A']

全屏编辑

wrapper.requestFullScreen() 只在PC端有效

退出全屏

wrapper.exitFullScreen() 只在PC端有效

当前是否为全屏模式

wrapper.inFullscreen()

销毁实例

wrapper.remove()

监听实例销毁

wrapper.onRemove(fun)

取消监听实例销毁

wrapper.offRemove(fun)

是否开启文件上传

wrapper.fileUploadEnable()

文件上传(1.6)

开启

通过配置upload_urlupload_finish即可开启文件上传,例如:

var config = {
    upload_url:'https://putsreq.com/aPamE6UIaFogo0JwhL6N',
    upload_finish:function(resp){
        swal('仅供测试上传所用,固定返回同一地址')
        return {
            type : 'image',
            url : 'https://www.qyh.me/image/news/8BE085FBC2F48482047C510EE0A36C4F.jpeg/600'
        };
    }
};
var wrapper = EditorWrapper.create(config);

其中upload_url为上传的地址,upload_finish为上传成功后的回调函数,接受一个参数,该参数内容为服务器响应的内容 ,同时返回地址信息,地址信息分为三种:图片|视频|一般文件。

  1. 图片地址,固定格式为{type:'image',url:'图片地址'}
  2. 视频地址,一般格式为{type:'video',url:'视频地址'},同时可以通过设置poster属性设定一个封面,例如:{type:'video',url:'视频地址','poster':'封面图片地址'},如果需要设置多个source,可以设置sources属性,source属性应该为一个数组,单个数组元素内容格式为{'type':'video/mp4|video/ogg等','src':'视频地址'}
  3. 一般文件地址,固定格式为{type:'file',url:'文件地址'}

设定上传前参数

通过配置upload_before可以在上传前增加额外的参数,例如

config.upload_before = function(formData,file){
  formData.append("key", file.name);
}

设定文件上传名称

通过配置upload_fileName可以设定文件上传名称,默认为file

七牛云文件上传

一个简单的七牛云文件上传的例子:

前端:

var config = {
  upload_url:'http://upload.qiniu.com/',
  upload_before:function(formData,file){
      $.ajax({
          async:false,//这里一定要同步获取token
          url : 'http://localhost:8081/test/test',
          success:function(token){
              formData.append("token", token);
              formData.append("key", file.name);
          }
      })
  },
  upload_finish:function(resp){
      resp = $.parseJSON(resp);
      if(resp.error){
          swal(resp.error);
          return ;
      }
      return {
          type : 'image',
          url : 'http://img.qyh.me/'+resp.key
      };
  }
};

后端:

package test;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.qiniu.util.Auth;

public class TestServlet extends HttpServlet{
	
	private static final long serialVersionUID = 1L;
	private final String ak = "";
	private final String sk = "";
	@Override
	protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
		Auth auth = Auth.create(ak, sk);
		String upToken = auth.uploadToken("mhlx");
		resp.addHeader("Access-Control-Allow-Origin", "*");
		resp.getWriter().write(upToken);
	}
}

导出PDF

没有直接导出PDF的方法,但是可以通过以下步骤,让chrome浏览器的打印功能来实现

设置打印样式

<link rel="stylesheet"  href="codemirror/lib/codemirror.css" media="screen">
<link rel="stylesheet" 
 href="codemirror/addon/scroll/simplescrollbars.css" media="screen">
<link rel="stylesheet"  href="css/markdown.css" media="all">
<link href="fontawesome-free/css/all.min.css"  media="all" rel="stylesheet">
<link rel="stylesheet" href="css/style.css" media="screen">
<link rel="stylesheet" href="css/print.css" media="print">

上述html中,media="print"的只会在打印时使用,media="screen"的则只会在页面渲染时使用,而media="all" 则在所有情况下都会使用,请参考 https://www.w3schools.com/tags/att_link_media.asp

打印前渲染

mermaid渲染出来的元素大小会根据视窗大小自动调整,由于左右预览的原因,打印出来的大小会跟预期的大小不一致,此时可以通过监听打印事件使得在打印前再次渲染。

var wrapper = EditorWrapper.create({});
var beforePrintHandler = function(mql) {
    if (mql.matches) {
        wrapper.doRender(false);
    }
}
if (window.matchMedia) {
    var mediaQueryList = window.matchMedia('print');
    mediaQueryList.addListener(beforePrintHandler);
}
wrapper.onRemove(function(){
    mediaQueryList.removeListener(beforePrintHandler);
});

强制分页

通过添加以下代码可以让pdf文件强制分页

<div style="page-break-after: always;"></div>

支持的浏览器

只在chrome上做了测试,但应该支持一些其他的现代化浏览器

感谢

  1. 采用codemirror作为编辑器
  2. 采用markdown-it渲染markdown
  3. 采用jquery简化dom操作
  4. 采用fontawesome美化图标
  5. 采用highlight.js高亮代码
  6. 采用katex渲染数学公式
  7. 采用sweet2alert美化弹出框
  8. 采用mermaid绘制流程图、甘特图和时序图
  9. 采用turndown将html转化为markdown
  10. 采用morphdom来patch更新dom

搜索