相关文章推荐
伤情的创口贴  ·  解决: ...·  1 年前    · 
英勇无比的莴苣  ·  文本输入框 ...·  1 年前    · 
重感情的生姜  ·  解决Error: Couldn t ...·  1 年前    · 
大气的日光灯  ·  python爬虫神器 ...·  2 年前    · 

最近项目中在做协议相关功能,要求引入富文本编辑器,有一些功能需求点,包括锚点、表格、预览、从 word 中复制到富文本编辑器中保留相关格式等。

综合业务需求,我这边对 ckEditor5 和 tinymce 进行了尝试。ckEditor5 在接入插件时相对较麻烦,我这边想要封装成一个通用功能星组件,就尝试采用 源码中引入ckEditor5插件 ,该方式需要去修改底层相关配置,但是我这边得项目底层配置是经过封装的,在接入过程中碰到了难以处理的问题,转而去尝试引入 tinymce,就一感觉:真香!!!

首先,奉上文档:

  • tinymce5 官方文档(英文): www.tiny.cloud/docs/quick-…
  • tinymce5 中文翻译文档: tinymce.ax-z.cn/
  • 一、准备工作

    npm i tinymce
    

    我这里安装的版本是 5.10.2

    安装 tinymce 时会安装所有开源的插件,在 node_modules/tinymce/plugins 下,在需要时直接引入就可以。

    然后,node_modules 中找到 tinymce 目录,将目录中 skins 文件夹复制到新建的public/tinymce 文件夹中,然后去下载相关语言包,下载地址,放到 public/tinymce/language 中,后续需要引入。

    tinymce 有三种模式:经典模式(classic,默认)行内模式(inline)清爽模式(Distraction-free),这里介绍最常用的经典模式,其它的模式可自行查看文档。

    tinymce 的插件有开源插件付费插件,目前开源插件能满足我的需求,我这边采用开源插件进行开发。

    二、编辑器配置

    1. 基本配置

    添加最基本的配置:

    <template>
         <textarea :id="tinymceId" />
    </template>
    <script lang="ts">
    import { defineComponent, computed, onMounted, onBeforeUnmount, unref } from 'vue'
    import type { Editor, RawEditorSettings } from 'tinymce'
    import tinymce from 'tinymce/tinymce'
    import 'tinymce/themes/silver'
    import 'tinymce/icons/default/icons'
    export default defineComponent({
       setup(){
       const tinymceId = ref<string>(UUID())
       const editorRef = ref<Editor>()
       const initOptions = computed(():RawEditorSettings => {
           const publicPath = __webpack_public_path__
           return {
             selector: `#${tinymceId.value}`,
             language_url: `${publicPath}tinymce/langs/zh_CN.js`,
             language: 'zh_CN',
             skin_url: `${publicPath}tinymce/skins/ui/oxide`,
             content_css: `${publicPath}tinymce/skins/ui/oxide/content.min.css`,
       onMounted(() => {
           tinymce.init(initOptions.value)
       onBeforeUnmount(() => {
           destory()
       function destory() {
           if (tinymce !== null) {
             tinymce?.remove?.(unref(initOptions).selector!)
       return { tinymceId }
    </script>
    

    效果如下:

    2. 编辑器初始化

    在初始化 setup 的钩子中可以进行初始化的操作:

  • 向编辑器中填写初始化内容
  • 设置编辑器的 只读/编辑状态
  • 监听编辑器的相关操作
  • const initOptions = computed(() => {
        return {
          // .....
          setup: (editor: Editor) => {
              editorRef.value = editor
              editor.on('init', initSetup)
     // 编辑器初始化
     function initSetup() {
       const editor = unref(editorRef)
       if (!editor) {
         return
       const value = props.value || ''
       editor.setContent(value)
       bindModelHandlers(editor)
     function setValue(editor, val: string, prevVal?: string) {
         editor
         && typeof val === 'string'
         && val !== prevVal
         && val !== editor.getContent()
         editor.setContent(val)
     function bindModelHandlers(editor: any) {
       watch(() => props.value, 
       (val: string, prevVal) => setValue(editor, val, prevVal),
         { immediate: true },
       watch(
         () => props.disabled,
         val => {
           editor.setMode(val ? 'readonly' : 'design')
         { immediate: true },
       editor.on('change keyup undo redo', () => {
         const content = editor.getContent()
         emit('update:value', content)
         emit('change', content)
    

    3. 图片上传配置

    使用 images_upload_handler 可自定义上传处理逻辑,该自定义函数需提供三个参数:blobInfo、成功回调、失败回调 和 上传进度。使用该配置,则无需使用其他上传配置选项

    const initOptions = computed(() => {
         return {
           // .....
           images_upload_handler: handleImgUpload
    // 图片上传自定义逻辑
    function handleImgUpload(blobInfo, success, failure, progress) {
        var xhr, formData;
        var file = blobInfo.blob();//转化为易于理解的file对象
        xhr = new XMLHttpRequest();
        xhr.withCredentials = false;
        xhr.open('POST', '/demo/upimg.php');
        xhr.onload = function() {
            var json;
            if (xhr.status != 200) {
                failFun('HTTP Error: ' + xhr.status);
                return;
            json = JSON.parse(xhr.responseText);
            if (!json || typeof json.location != 'string') {
                failFun('Invalid JSON: ' + xhr.responseText);
                return;
            succFun(json.location);
        formData = new FormData();
        formData.append('file', file, file.name );
        xhr.send(formData);
    

    最终效果图:

    4. 完整版本代码

    注意:paste_retain_style_properties 属性可以保留复制过来的相关样式,比如要保留字体大小、颜色、背景颜色,可以将其配置为 paste_retain_style_properties: 'font-size color background background-color',如果要保留所有样式可以设置为 all,但是这样会造成代码量很大,并且这个属性将在 6 版本中移除,谨慎使用。

    <template>
      <textarea :id="tinymceId" />
    </template>
    <script lang="ts">
      import {
        defineComponent, computed, onMounted, ref, PropType, unref, watch, onBeforeUnmount,
      } from 'vue'
      import type { Editor, RawEditorSettings } from 'tinymce'
      import tinymce from 'tinymce/tinymce'
      import 'tinymce/themes/silver'
      import 'tinymce/icons/default/icons'
      import 'tinymce/plugins/advlist'
      import 'tinymce/plugins/anchor'
      import 'tinymce/plugins/autolink'
      import 'tinymce/plugins/autosave'
      import 'tinymce/plugins/code'
      import 'tinymce/plugins/codesample'
      import 'tinymce/plugins/directionality'
      import 'tinymce/plugins/fullscreen'
      import 'tinymce/plugins/hr'
      import 'tinymce/plugins/insertdatetime'
      import 'tinymce/plugins/link'
      import 'tinymce/plugins/lists'
      import 'tinymce/plugins/image'
      import 'tinymce/plugins/toc'
      import 'tinymce/plugins/nonbreaking'
      import 'tinymce/plugins/noneditable'
      import 'tinymce/plugins/pagebreak'
      import 'tinymce/plugins/paste'
      import 'tinymce/plugins/preview'
      import 'tinymce/plugins/print'
      import 'tinymce/plugins/save'
      import 'tinymce/plugins/searchreplace'
      import 'tinymce/plugins/spellchecker'
      import 'tinymce/plugins/tabfocus'
      import 'tinymce/plugins/table'
      import 'tinymce/plugins/template'
      import 'tinymce/plugins/textpattern'
      import 'tinymce/plugins/visualblocks'
      import 'tinymce/plugins/visualchars'
      import 'tinymce/plugins/wordcount'
      import { plugins as initialPlugins, toolbar as initialToolbar, fontFormats } from './tinymce'
      import { UUID } from 'uuid'
      type Recordable<T = any> = Record<string, T>
      export default defineComponent({
        props: {
          value: {
            type
    
    
    
    
        
    : String,
          disabled: {
            type: Boolean,
            default: false
          options: {
            type: Object as PropType<Partial<RawEditorSettings>>,
            default: () => ({}),
          toolbar: {
            type: String,
            default: initialToolbar,
          plugins: {
            type: Array as PropType<string[]>,
            default: initialPlugins,
          height: {
            type: [Number, String] as PropType<string | number>,
            required: false,
            default: 400,
          width: {
            type: [Number, String] as PropType<string | number>,
            required: false,
            default: 'auto',
        emits: ['change', 'update:value'],
        setup(props, { emit }) {
          const tinymceId = ref<string>(UUID())
          const editorRef = ref<Editor>()
          const initOptions = computed((): RawEditorSettings => {
            const publicPath = __webpack_public_path__
            const {
              height, options, toolbar, plugins,
            } = props
            return {
              selector: `#${tinymceId.value}`,
              language_url: `${publicPath}tinymce/langs/zh_CN.js`,
              language: 'zh_CN',
              skin_url: `${publicPath}tinymce/skins/ui/oxide`,
              content_css: `${publicPath}tinymce/skins/ui/oxide/content.min.css`,
              images_upload_handler: handleImgUpload,
              images_file_types: 'jpeg,jpg,png,gif,bmp,webp', // 准许的图片格式
              convert_urls: false,
              branding: false, // 隐藏品牌,隐藏状态栏中显示的“ Powered by Tiny ”链接
              placeholder: '请输入内容', // 占位符
              toolbar,
              plugins,
              height,
              toolbar_mode: 'sliding',
              toolbar_sticky: true,
              paste_block_drop: true, // 禁用将内容拖放到编辑器中
              paste_data_images: false, // 粘贴data格式的图像 谷歌浏览器无法粘贴
              font_formats: fontFormats,
              paste_retain_style_properties: 'color border border-left border-right border-bottom border-top', // MS Word 和类似 Office 套件产品保留样式
              paste_webkit_styles: 'none', // 允许在 WebKit 中粘贴时要保留的样式
              paste_tab_spaces: 2, // 将制表符转换成空格的个数
              content_style: `
              html, body                { height:100%; }
              img                       { max-width:100%; display:block;height:auto; }
              a                         { text-decoration: none; }
              p                         { line-height:1.6; margin: 0px; }
              table                     { word-wrap:break-word; word-break:break-all;max-width:100%; border:none; border-color:#999; }
              .mce-object-iframe        { width:100%; box-sizing:border-box; margin:0; padding:0; }
              ul,ol                     { list-style-position:inside; }
              ...options,
              setup: (editor: Editor) => {
                editorRef.value = editor
                editor.on('init', initSetup)
          onMounted(() => {
            tinymce.init(initOptions.value)
          onBeforeUnmount(() => {
            destory()
          function destory() {
            if (tinymce !== null) {
              tinymce?.remove?.(unref(initOptions).selector!)
          // 图片上传自定义逻辑
          function handleImgUpload(blobInfo, success, failure, progress) {
            console.log('blobInfo', blobInfo.blob(), blobInfo.filename())
            const { type: fileType, name: fileName } = blobInfo.blob()
            // xxxx 自定义上传逻辑
          // 编辑器初始化
          function initSetup() {
            const editor = unref(editorRef)
            if (!editor) {
              return
            const value = props.value || ''
            editor.setContent(value)
            bindModelHandlers(editor)
          function setValue(editor: Recordable, val: string, prevVal?: string) {
              editor
              && typeof val === 'string'
              && val !== prevVal
              && val !== editor.getContent()
              editor.setContent(val)
          function bindModelHandlers(editor: any) {
            watch(
              () => props.value,
              (val: string, prevVal) => setValue(editor, val, prevVal),
              { immediate: true },
            watch(
              () => props.disabled,
              val => {
                editor.setMode(val ? 'readonly' : 'design')
              { immediate: true },
            editor.on('change keyup undo redo', () => {
              const content = editor.getContent()
              emit('update:value', content)
              emit('change', content)
          return {
            tinymceId,
    </script>
    

    tinymce.ts 文件里是 tinymce 的 plugins、toolbar、fontFormats 的配置,这里基本上使用了所有的开源插件,功能比较齐全

    // tinymce.ts
    // imagetools
    export const plugins = [
      'advlist anchor autolink code codesample  directionality  fullscreen hr insertdatetime link lists nonbreaking noneditable pagebreak paste preview print save searchreplace tabfocus  template  textpattern visualblocks visualchars wordcount table image toc',
    export const toolbar = 'undo redo | bold italic underline strikethrough | fontselect fontsizeselect formatselect | toc alignleft aligncenter alignright alignjustify lineheight | outdent indent | numlist bullist | forecolor backcolor | pagebreak | charmap emoticons | fullscreen preview save print | hr link image | anchor pagebreak | insertdatetime | blockquote removeformat subscript superscript code codesample | searchreplace'
    export const fontFormats = '微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif,Andale Mono=andale mono,times;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier;Georgia=georgia,palatino;Helvetica=helvetica;Impact=impact,chicago;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco;Times New Roman=times new roman,times;Trebuchet MS=trebuchet ms,geneva;Verdana=verdana,geneva;Webdings=webdings;Wingdings=wingdings,zapf dingbats'
    

    三、 属性配置汇总

    width: '100%', //  设置富文本编辑器宽度
    height: '100%', //  设置富文本编辑器高度
    menubar: false, // 设置富文本编辑器菜单, 默认true
    branding: false, // 关闭底部官网提示 默认true
    statusbar: true, // 显示底部状态栏 默认true
    readonly: false, // 设置只读属性 默认 false
    resize: false, // 调节编辑器大小 默认 true
    branding: false, // 隐藏状态栏右下角显示的品牌
    placeholder: '请输入内容', // 占位符
    theme: 'silver', // 主题 必须引入
    skin_url: '/tinymce/skins/ui/oxide', // 主题路径
    icons: 'custom',  // 自定义图标名称
    icons_url: '/tinymce/icons/icons.js', // 自定义图标路径
    language_url: '/tinymce/langs/zh_CN.js', // 中文化 默认为英文
    language: 'zh_CN', // 设置富文本编辑器语言
    content_css: `/tinymce/skins/content/default`, // 富文本编辑器内容区域样式
    content_style: 'body, p{font-size: 12px}', // 为内容区编辑自定义css样式
    plugins: ['autosave help textpattern lineheight'], // 插件配置
    toolbar: 'fontselect styleselect fontsizeselect restoredraft undo redo | bold italic underline strikethrough subscript superscript removeformat forecolor backcolor lineheight align outdent indent help', // 工具栏配置
    toolbar_mode: 'sliding', // sliding生效条件toolbar必须为字符串,且有'|'区分,不能为数组
    toolbar_sticky: true, // 粘性工具栏 默认false (在向下滚动网页直到不再可见编辑器时,将工具栏和菜单停靠在屏幕顶部)
    // 快速工具栏配置,需引入插件 quickbars
    quickbars_selection_toolbar: 'bold italic underline strikethrough | link h2 h3 h4 blockquote', // 设置 快速选择 触发提供的工具栏 需引入插件  默认 'alignleft aligncenter alignright' 设置为false禁用
    quickbars_insert_toolbar: 'quickimage quicktable', // 设置 快速插入 触发提供的工具栏 需引入插件quickbars 默认 quickimage quicktable 设置为false禁用
    // font 相关配置
    fontsize_formats: '12px 14px 16px 18px 20px 22px 24px 26px 36px 48px 56px', // 工具栏自定义字体大小选项
    font_formats: "微软雅黑='微软雅黑'; 宋体='宋体'; 黑体='黑体'; 仿宋='仿宋'; 楷体='楷体'; 隶书='隶书'; 幼圆='幼圆'; 方正舒体='方正舒体'; 方正姚体='方正姚体'; 等线='等线'; 华文彩云='华文彩云'; 华文仿宋='华文仿宋'; 华文行楷='华文行楷'; 华文楷体='华文楷体'; 华文隶书='华文隶书'; Andale Mono=andale mono,times; Arial=arial; Arial Black=arial black;avant garde; Book Antiqua=book antiqua;palatino; Comic Sans MS=comic sans ms; Courier New=courier new;courier; Georgia=georgia; Helvetica=helvetica; Impact=impact;chicago; Symbol=symbol; Tahoma=tahoma;arial; sans-serif; Terminal=terminal,monaco; Times New Roman=times new roman,times; Trebuchet MS=trebuchet ms; Verdana=verdana;geneva; Webdings=webdings; Wingdings=wingdings", // 工具栏自定义字体选项
    // autosave 插件配置,需引入插件 autosave
    autosave_ask_before_unload: true, // 阻止有内容时浏览器阻塞行为, 默认 true
    autosave_interval: '3s', // 设置自动保存为草稿时间 单位只能为s 
    autosave_prefix: `editor_${route.path}`, // 设置自动保存为草稿时前缀 本地localStorage中存储
    autosave_retention: '300m', // 自动草稿的有效期 单位只能为m(分钟)
    // image 相关配置,需引入插件image
    images_upload_handler: (blobInfo, success, failure) => {
        // 发送请求, 获取图片路径后, 将路径传给success
        success('xxxx')
    }, // 图片上传函数 
    image_advtab: true, // 为上传图片窗口添加高级属性
    // paste 相关配置,需引入插件paste
    paste_data_images: true, // 粘贴data格式的图像
    paste_block_drop: true, // 禁用将内容拖放到编辑器中
    paste_as_text: true, // 默认粘贴为文本
    paste_retain_style_properties: 'color border', // MS Word 和类似 Office 套件产品保留样式
    // template 内容模板配置,需引入插件template
    templates: [{ title: '标题', description: '描述', content: '内容' }], // 内容模板
    // 快速排版配置,需引入插件 textpattern
    textpattern_patterns: [
        { start: '*', end: '*', format: 'italic' },
        { start: '**', end: '**', format: 'bold' },
        { start: '#', format: 'h1' },
        { start: '##', format: 'h2' },
        { start: '###', format: 'h3' },
        { start: '####', format: 'h4' },
        { start: '#####', format: 'h5' },
        { start: '######', format: 'h6' },
        { start: '1. ', cmd: 'InsertOrderedList' },
        { start: '* ', cmd: 'InsertUnorderedList' },
        { start: '- ', cmd: 'InsertUnorderedList' }
    ], // 快速排版  类似于markdown
    init_instance_callback: editor => { // 初始化结束后执行, 里面实现双向数据绑定功能
        editor.on('Input undo redo Change execCommand SetContent', (e) => {
        // editor.getContent({ format: ''text }) // 获取纯文本
        $emit('change', editor.getContent())
    setup: (editor) => { // 初始化前执行
    // xxxx
        Crystal
            前端打杂师 @ 小红书
            16.6k
           
    粉丝