# tui-editor

houdunren.com @ 向军大叔

xj-small

tui-editor 是非常漂亮的支持markdown格式的文本编辑器,后盾人网站就在使用该编辑器,配置简单功能丰富。你可访问官方文档获取更多使用说明。

image-20200712233900443

# 基本使用

有些应用基于SCRIPT标签引入库的方式进行开发。引入文件比较好的方式是使用CDN,但在很多CDN网站中的tui-editor版本过低,下面我们使用最新版本的tui-editor。

# 下载软件包

使用NPM下载包,可以在任何目录下执行,因为我们只是想复制里面的dist目录

npm install --save @toast-ui/editor

进入 node_modules/_@toast-ui_editor@2.2.0@@toast-ui/editor 目录,然后复制其他的dist目录到你的项目中,我将它复制到项目的 public/plugins/tuiEditor/dist目录中

# 编辑器增强

我们创建 public/plugins/tuiEditor/tuiEditor.js 文件,一般来讲不需要定义这个文件直接使用官方示例代码就可以工作了。我们定义文件的目的主要有以下几点。

  1. 增加后台上传图片功能
  2. 增加全屏编辑按钮。
const tuiEditorTool = {
  //编辑器实例
  editor: null,
  //添加全屏按钮事件
  fullScreenEvent() {
    const toolbar = this.editor.getUI().getToolbar()
    const cm = this.editor.mdEditor.cm
    //设置按钮点击事件
    this.editor.eventManager.addEventType('fullscreen')
    this.editor.eventManager.listen('fullscreen', () => {
      this.editor.previewStyle = 'vertical'
      //保存点击状态
      cm.setOption('fullScreen', !cm.getOption('fullScreen'))
      let ui = document.querySelector('.tui-editor-defaultUI')
      console.log(ui)

      if (cm.getOption('fullScreen')) {
        ui.classList.add('tuiEditor-fullScreen')
      } else {
        ui.classList.remove('tuiEditor-fullScreen')
      }
    })
  },
  //添加工具条按钮
  createButton(className) {
    const button = document.createElement('button')
    button.className = className
    button.innerHTML = `<i class="fa fa-arrows-alt" style="color:#666;"></i>`
    return button
  },
  //自定义工具条
  toolbar() {
    return [
      //添加全屏按钮
      {
        type: 'button',
        options: {
          el: this.createButton('last'),
          name: 'fullscreen',
          tooltip: 'fullscreen',
          event: 'fullscreen',
        },
      },
      'codeblock',
      'divider',
      'heading',
      'bold',
      'italic',
      'strike',
      'divider',
      'hr',
      'quote',
      'divider',
      'ul',
      'ol',
      'task',
      'indent',
      'outdent',
      'divider',
      'table',
      'image',
      'link',
      'divider',
    ]
  },
}

window.hdEditor = function (el, options = {}) {
  const config = Object.assign(
    {
      el: document.querySelector(el),
      //图片上传地址
      action: '',
      height: '300px',
      initialEditType: 'markdown',
      previewStyle: 'vertical',
      hooks: {
        async addImageBlobHook(blob, callback) {
          let formData = new FormData()
          //添加post数据
          formData.append('file', blob, blob.name)
          //上传图片
          let response = await axios.post(config.action, formData)
          //更改编辑器内容
          callback(response.path, blob.name)
          return false
        },
      },
      toolbarItems: tuiEditorTool.toolbar(),
    },
    options
  )

  const editor = new toastui.Editor(config)
  tuiEditorTool.editor = editor
  tuiEditorTool.fullScreenEvent()
  return editor
}

增加全屏样式 public/plugins/tuiEditor/tuiEditor.css 文件,主要是增加全屏显示的样式

.tuiEditor-fullScreen {
  position: fixed !important;
  z-index: 999;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
}

# 开始使用

首先引入需要的CSS与JS文件

<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.55.0/codemirror.min.css" />
<link rel="stylesheet" href="/plugins/tuiEditor/dist/toastui-editor.css" />
<link rel="stylesheet" href="/plugins/tuiEditor/tuiEditor.css" />

<script src="https://cdn.staticfile.org/codemirror/5.55.0/codemirror.js"></script>
<script src="/plugins/tuiEditor/dist/toastui-editor.js"></script>
<script src="/plugins/tuiEditor/tuiEditor.js"></script>

现在就可以展示编辑器了

<div id="editor"></div>
<script>
    hdEditor('#editor',{
        action:"/common/upload"
    })
</script>

下面是hdEditor函数的第二个参数说明

选项 说明
action 后台图片上传地址,后台需要返回上传的图片路径
height 编辑器高度
previewStyle 显示方式:vertical ,tab
initialEditType 编辑器模式:wysiwyg ,markdown
initialValue 默认值

# Laravel

为了让编辑器可复用,需要在LARAVEL框架中编写组件

# 创建组件

通过Artisan命令创建组件

php artisan make:component Editor

修改组件模块 resources/views/components/editor.blade.php文件内容如下

@push("styles")
<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.55.0/codemirror.min.css" />
<link rel="stylesheet" href="/plugins/tuiEditor/dist/toastui-editor.css" />
<link rel="stylesheet" href="/plugins/tuiEditor/tuiEditor.css" />
@endpush

@push('comment')
<script src="https://cdn.staticfile.org/codemirror/5.55.0/codemirror.js"></script>
<script src="/plugins/tuiEditor/dist/toastui-editor.js"></script>
<script src="/plugins/tuiEditor/tuiEditor.js"></script>
<script>
    let editor =  hdEditor('#{{ $attributes['name'] }}',{
        action:"{{  $attributes['action'] }}",
        initialValue:window.{{ $attributes['name'] }},
        events: {
          //监听编辑器输入
          change: function () {
            const input = document.querySelector(`[name='{{ $attributes['name'] }}']`)
            input.value = editor.getMarkdown()
          },
        },
    })

    const el = document.getElementById(`{{ $attributes['name'] }}`);
    el.addEventListener('click',(el)=>{
        const errorEl = document.querySelector(`.error-{{ $attributes['name'] }}`)
        errorEl && errorEl.remove()
    })
</script>
@endpush

<div id="{{ $attributes['name'] }}"></div>
@error( $attributes['name'] )
<strong class="form-text text-danger small font-weight-bold error-{{ $attributes['name'] }}">{{ $message }}</strong>
@enderror

<input type="hidden" name="{{ $attributes['name'] }}" />

# 父模板

通过上面的代码应该以看到,需要在父模板中相应的标志位

  1. 定义 @stack('styles') 用于放置 CSS 内容
  2. 定义 @stack('comment') 用于放置评论 JS代码
  3. 编辑器使用axios处理上传图片,所以要在父级模板中正确引入laravel编译的前端文件 app.js

下面是一个简单的父模板示例代码

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8">
    @stack('styles')
    @stack('links')
</head>

<body>
    @yield('content')

    @stack('vue')
    <script src="/js/app.js"></script>
    @stack('scripts')
    @stack('comment')
</body>

</html>

# 使用组件

现在可以方便的使用编辑器了

@push('comment')
  <script>
      window.content = @json( old('content',$topic['content']));
  </script>
@endpush

<x-editor action="{{ route('common.upload') }}" name="content"></x-editor>

# VUE组件

下面再来将tui-editor开发为vue的组件,首先在VUE项目中安装tui-editor编辑器

npm install --save @toast-ui/editor

# 组件开发

下面基于tui-editor开发的VUE组件,扩展了以下几个功能

  • 支持后台图片上传而不使用默认的base64
  • 添加了全屏显示按钮
<template>
  <div>
    <div id="editor" @click="show_error = false" :class="{ 'is-invalid': error }" ref="editor"></div>
    <strong class="form-text text-danger invalid-feedback" v-if="show_error">{{ error }}</strong>

    <textarea hidden :name="name" v-model="form.markdown"></textarea>
    <textarea hidden :name="name + 'html'" v-model="form.html"></textarea>
  </div>
</template>

<script>
import 'codemirror/lib/codemirror.css'
import '@toast-ui/editor/dist/toastui-editor.css'
import '@toast-ui/editor/dist/i18n/zh-cn'
import Editor from '@toast-ui/editor'

export default {
  props: {
    error: { default: '' },
    name: { required: true, type: String },
    content:{required:true},
    //后台上传地址
    action: { type: String, default: `common/upload` },
    //编辑器高度
    height: { type: String, default: '300px' },
    //显示方式
    previewStyle: { type: String, default: 'vertical' },
    initialEditType: { type: String, default: 'markdown' },
  },
  data() {
    return {
      editor: null,
      form: { markdown: '', html: '' },
      show_error:true,
    }
  },
  mounted() {
    this.initEditor()
    // this.fullScreenEvent()
  },
  methods: {
    initEditor() {
      const Vue = this
      const editor = new Editor({
        el: document.querySelector('#editor'),
        previewStyle: this.previewStyle,
        // initialValue: window.editor_content,
        initialValue: this.content,
        initialEditType: this.initialEditType,
        height: this.height,
        language: 'zh-CN',
        placeholder: '',
        // plugins: [Editor.codeSyntaxHighlight],
        events: {
          //监听编辑器输入
          change: function () {
            Vue.$set(Vue.form, 'markdown', editor.getMarkdown())
            Vue.$set(Vue.form, 'html', editor.getHtml())
          },
        },
        hooks: {
          async addImageBlobHook(blob, callback) {
            let formData = new FormData()
            //添加post数据
            formData.append('file', blob, blob.name)
            //上传图片
            let response = await Vue.axios.post(Vue.action, formData)
            //更改编辑器内容
            callback(response.path, blob.name)
            return false
          },
        },
        toolbarItems: this.toolbar(),
      })
      this.editor = editor
    },
    /**
     * 添加工具条按钮
     */
    createButton(className) {
      const button = document.createElement('button')
      button.className = className
      button.innerHTML = `<i class="fa fa-arrows-alt" style="color:#666;"></i>`
      return button
    },
    /**
     * 添加全屏按钮事件
     */
    fullScreenEvent() {
      const toolbar = this.editor.getUI().getToolbar()
      const cm = this.editor.mdEditor.cm
      //设置按钮点击事件
      this.editor.eventManager.addEventType('fullscreen')
      this.editor.eventManager.listen('fullscreen', () => {
        this.editor.previewStyle = 'vertical'
        //保存点击状态
        cm.setOption('fullScreen', !cm.getOption('fullScreen'))
        let ui = document.querySelector('.tui-editor-defaultUI')
        if (cm.getOption('fullScreen')) {
          ui.classList.add('fullScreen')
        } else {
          ui.classList.remove('fullScreen')
        }
      })
    },
    /**
     * 自定义工具条
     */
    toolbar() {
      return [
        {
          type: 'button',
          options: {
            el: this.createButton('last'),
            name: 'fullscreen',
            tooltip: 'fullscreen',
            event: 'fullscreen',
          },
        },
        'codeblock',
        'divider',
        'heading',
        'bold',
        'italic',
        'strike',
        'divider',
        'hr',
        'quote',
        'divider',
        'ul',
        'ol',
        'task',
        'indent',
        'outdent',
        'divider',
        'table',
        'image',
        'link',
        'divider',
      ]
    },
  },
}
</script>

<style lang="scss">
// 事件按钮需要使用类所以不能加scoped
.fullScreen {
  position: fixed;
  z-index: 999;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: #fff;
}
</style>

# 使用组件

使用vue组件的好处就是编写时比较辛苦,但使用时就很轻松了,下面再介绍一下组件的可以props属性。

属性 说明
error 错误消息
content Markdown内容
action 后台上传图片地址,需要返回上传成功的图片路径
height 编辑器高度
name 上传后台的post表单name
previewStyle 显示方式:vertical ,tab
initialEditType 编辑器模式:wysiwyg ,markdown

下面是使用的示例

<editor name="content" error="@error('content'){{ $message }} @enderror" :content="content">
</editor>