# tui.editor

houdunren.com (opens new window) @ 向军大叔

xj-small

tui.editor (opens new window)编辑器支持markdown与标准富文本内容的编辑器

image-20201120033559294

# Laravel

下面将编辑器定义成laravel的组件方便在项目中调用。

# 后台业务

执行以下创建生成组件后台处理文件 app/View/Components/Editor.php

php artisan make:component Editor

内容如下

<?php

namespace App\View\Components;

use Illuminate\View\Component;

class Editor extends Component
{
  public $theme;
  public $name;
  public $value;
  public $height;
  public $type;
  public $action;

  /**
   * 初始参数
   * @param string $theme 编辑器类型
   * @param mixed $name 表单名称
   * @param string $value 默认值
   * @return void
   */
  public function __construct($theme = 'wang', $name, $value = '', $height = 300, $type = 'markdown', $action = null)
  {
    $this->theme = $theme;
    $this->name = $name;
    $this->value = $value;
    $this->height = $height;
    $this->type = $type;
    $this->action = $action ?? route('common.upload.make');
  }

  public function validateName($name)
  {
    return str_replace(['[', ']'], ['.', ''], $name);
  }

  public function render()
  {
    return view('components.editor.' . $this->theme);
  }
}

# 组件模板

创建组件模板文件 resources/views/components/editor/toast.blade.php,内容如下

代码中使用了 @push 指定,所以需要在你项目中的父模板中使用 @stack('styles') 与 @stack('scripts')

<div id="{{ $name }}" class="editor-container"></div>
{{-- 表单验证 --}}
@error($name)
<strong class="form-text text-danger small font-weight-bold error-{{$name}}">{{ $message }}</strong>
@enderror

<div class="text-secondary mt-2 small">
  <i class="fas fa-info-circle"></i> 你可以在编辑器底部切换为markdown模式,编辑器也支持托放上传图片。
</div>

{{-- 同步编辑器内容提交到后使用 --}}
<textarea name="{{ $name }}" hidden>{{ $value }}</textarea>

@push('scripts')
<script src="https://cdn.staticfile.org/codemirror/5.55.0/codemirror.js"></script>
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<script src="/plugins/tuiEditor/tuiEditor.js"></script>
<script>
  const textarea = document.querySelector(`[name="{{ $name }}"]`);
  new TotalEditor({
    el:`#{{ $name }}`,
    //图片上传地址
    action:`{{ route('common.upload.make') }}`,
    //编辑器高度
    height:300,
    //编辑器类型
    type:'markdown',
    //初始值
    content:textarea.value,
    //编辑器内容更改后回调
    onchange:(content)=>{
      textarea.value = content;
    }
  });
</script>
@endpush

@push("styles")
<link rel="stylesheet" href="https://cdn.staticfile.org/codemirror/5.55.0/codemirror.min.css" />
<link rel="stylesheet" href="https://uicdn.toast.com/editor/latest/toastui-editor.min.css" />

<style>
  .editor-container {
    border: solid 1px #ddd;
  }
</style>
@endpush

上面示例中使用 new TotalEditor 创建了编辑器,下面对选项值进行说明

选项 说明
el 编辑器div元素选择器
action 后台图片上传地址
height 编辑器高度
type 编辑器类型:wysiwyg 或 markdown
content 初始值
onchange 编辑器内容更改后回调

# 组件业务

创建前台脚本文件 public/plugins/tuiEditor/tuiEditor.js内容如下

class TotalEditor {
  constructor(options) {
    this.options = options
    this.editor = this.init()
    this.fullScreenEvent()
  }

  init() {
    const options = this.options
    return new toastui.Editor({
      el: document.querySelector(this.options.el),
      previewStyle: 'vertical',
      initialValue: this.options.content || '',
      initialEditType: this.options.type || 'wysiwyg',
      height: this.options.height || 300,
      language: 'zh-CN',
      placeholder: '',
      events: {
        //监听编辑器输入
        change: () => {
          if (options.onchange) {
            options.onchange(this.editor.getMarkdown())
          }
        },
      },
      hooks: {
        async addImageBlobHook(blob, callback) {
          let formData = new FormData()
          //添加post数据
          formData.append('file', blob, blob.name)
          //上传图片
          let response = await axios.post(options.action || '/common/upload/make', formData)
          //更改编辑器内容
          callback(response.path, blob.name)
          return false
        },
      },
      toolbarItems: this.toolbar(),
    })
  }

  //添加按钮
  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',
    ]
  }
}

# 使用组件

在模板中使用以下代码调用编辑器

<x-editor theme="toast" name="content" value="默认值-后盾人" type="markdown" height="300"
                  action="/common/upload/make" />

下面来对属性进行说明

属性 说明
theme 值固定为toast
name 表单name
value 初始值
type 编辑器类型:wysiwyg 或 markdown
height 编辑器高度
action 后台图片上传地址

# VUE

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

  • 支持后台图片上传而不使用默认的base64
  • 添加了全屏显示按钮

首先安装扩展包

cnpm install @toast-ui/editor
cnpm install codemirror

# 组件代码

然后创建 HdTuiEditor.vue组件文件

<template>
    <div>
        <div id="hdEditor"></div>
        <div class="text-secondary mt-2 p-2 d-block mb-2">
            <i class="fas fa-info-circle"></i> 你可以在编辑器底部切换为markdown模式,编辑器也支持托放上传图片。
        </div>
    </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: {
        //后台上传地址
        action: { required: true, type: String },
        //编辑器高度
        height: { type: String, default: '300px' },
        //显示方式
        previewStyle: { type: String, default: 'vertical' },
        initialEditType: { type: String, default: 'wysiwyg' }
    },
    data() {
        return {
            editor: null
        }
    },
    mounted() {
        this.initEditor()
        this.fullScreenEvent()
    },
    methods: {
        initEditor() {
            const Vue = this
            const editor = new Editor({
                el: document.querySelector('#hdEditor'),
                previewStyle: this.previewStyle,
                initialValue: Vue.$attrs.value,
                initialEditType: this.initialEditType,
                height: this.height,
                language: 'zh-CN',
                placeholder: '',
                events: {
                    //监听编辑器输入
                    change: function() {
                        Vue.$emit('input', editor.getMarkdown())
                    }
                },
                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 !important;
    z-index: 999;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background: #fff;
}
.tui-editor-defaultUI {
    border: none;
}
</style>

# 使用方式

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

<template>
  <div>
     <hd-tui-editor v-model="content" action="/common/upload" />
  </div>
</template>

<script>
const form = { content: '' }
export default {
  data() {
      return form
  }
}
</script>

<style></style>

下面对属性进行说明

属性 说明
content 绑定数据
action 图片上传地址