quill-editor扩展的正确姿势

一、无关的插曲
曾几何时,风云万里,万海桑田。耕耘于代码堆里多年。做过android移动端,做过web端,做过java后端和.net,也做过python数据分析。但真正扩展源码的亦有亦无。如今更趋向与智能化,重复的机器可代替,如此节约时间甚好。我们努力前行,我们都在路上,我们有自己的最求和向往,但任凭达到何等境地,都无法避免的一个,那就是终结陨落。所以在陨落之前,有的人擅长了独门绝技,有的人擅长多种技术柔和。反正天外有天,人外有人。兴趣决定了我们的高度。敲代码,是最穷的,但敲代码是最有创意的,如此甚好!

二、需求场景
最近前端的需求是要求一个富文本编辑器,里面可插入音频,视频和图片等。。。然后就飞发发的查资料去了,有国产wangEditor,齐全但臃肿的百度UEditor,有文档齐全的Kindeditor,也有不错的Tinymce。感觉各有优缺点吧!最终还是选择vue-quill-editor,就是图它的方便,扩展性好,如此而已。

三、插入图片
在文本编辑器中插入图片,直接使用插件中就有的功能就行,至于图片可拖动和大小变化,在文件里分别引入一下模块即可:

import * as Quill from 'quill'

import ImageResize from 'quill-image-resize-module' //调节图片大小
Quill.register('modules/imageResize', ImageResize)
import { ImageDrop } from 'quill-image-drop-module' // 图片可拖动
Quill.register('modules/imageDrop', ImageDrop)

到此还不行,还得配置quill-editor的options

editorOption: {
  placeholder: '请输入',
  theme: 'snow', // or 'bubble' 
  modules: {
    toolbar: {
      container: '#toolbar',
      handlers: handlers
    },
    imageDrop: true,
    imageResize: {
      displayStyles: {
        backgroundColor: 'black',
        border: 'none',
        color: 'white'
      },
      modules: ['Resize', 'DisplaySize', 'Toolbar']
    }
  }
}

关键在这里哟:

imageDrop: true,
imageResize: {
  displayStyles: {
    backgroundColor: 'black',
    border: 'none',
    color: 'white'
  },
  modules: ['Resize', 'DisplaySize', 'Toolbar']
}

这样图片我们差不多处理好了

四、视频的处理
由于quill-editor原生提供的视频会插入iframe,可能会导致其他终端无法正常播放,所以我们得扩展一下,让其直接插入video。
1、新建video.js文件:

import { Quill } from 'vue-quill-editor'

// 源码中是import直接倒入,这里要用Quill.import引入
const BlockEmbed = Quill.import('blots/block/embed')
const Link = Quill.import('formats/link')

const ATTRIBUTES = ['height', 'width']

class Video extends BlockEmbed {
  static create (value) {
    const node = super.create(value)
    // 添加video标签所需的属性
    node.setAttribute('controls', 'controls')
    node.setAttribute('type', 'video/mp4')
    node.setAttribute('src', this.sanitize(value))
    return node
  }

  static formats (domNode) {
    return ATTRIBUTES.reduce((formats, attribute) => {
      if (domNode.hasAttribute(attribute)) {
        formats[attribute] = domNode.getAttribute(attribute)
      }
      return formats
    }, {})
  }

  static sanitize (url) {
    return Link.sanitize(url) // eslint-disable-line import/no-named-as-default-member
  }

  static value (domNode) {
    return domNode.getAttribute('src')
  }

  format (name, value) {
    if (ATTRIBUTES.indexOf(name) > -1) {
      if (value) {
        this.domNode.setAttribute(name, value)
      } else {
        this.domNode.removeAttribute(name)
      }
    } else {
      super.format(name, value)
    }
  }

  html () {
    const { video } = this.value()
    return `<a href="${video}">${video}</a>`
  }
}
Video.blotName = 'video' // 这里不用改,楼主不用iframe,直接替换掉原来,如果需要也可以保留原来的,这里用个新的blot
Video.className = 'ql-video'
Video.tagName = 'video' // 用video标签替换iframe

export default Video

	2、在插件页面引入新建的video.js:
import Video from './video'
Quill.register(Video, true)

五、插入音频
插入的音频跟视频的一样,复制一份video.js,将其改为audio.js,然后将里面video改成audio就行。

六、完整的quill-edittor如下

<template>
  <div>
    <el-card style="height: 610px;">
      <quill-editor v-model="content"
                    ref="myQuillEditor"
                    style="height: 500px;"
                    :options="editorOption">
        <!-- 自定义toolar -->
        <div id="toolbar"
             slot="toolbar">
          <!-- Add a bold button -->
          <button class="ql-bold"
                  title="加粗">Bold</button>
          <button class="ql-italic"
                  title="斜体">Italic</button>
          <button class="ql-underline"
                  title="下划线">underline</button>
          <button class="ql-strike"
                  title="删除线">strike</button>
          <button class="ql-blockquote"
                  title="引用"></button>
          <button class="ql-code-block"
                  title="代码"></button>
          <button class="ql-header"
                  value="1"
                  title="标题1"></button>
          <button class="ql-header"
                  value="2"
                  title="标题2"></button>
          <!--Add list -->
          <button class="ql-list"
                  value="ordered"
                  title="有序列表"></button>
          <button class="ql-list"
                  value="bullet"
                  title="无序列表"></button>
          <!-- Add font size dropdown -->
          <select class="ql-header"
                  title="段落格式">
            <option selected>段落</option>
            <option value="1">标题1</option>
            <option value="2">标题2</option>
            <option value="3">标题3</option>
            <option value="4">标题4</option>
            <option value="5">标题5</option>
            <option value="6">标题6</option>
          </select>
          <select class="ql-size"
                  title="字体大小">
            <option value="10px">10px</option>
            <option value="12px">12px</option>
            <option value="14px">14px</option>
            <option value="16px"
                    selected>16px</option>
            <option value="18px">18px</option>
            <option value="20px">20px</option>
          </select>
          <select class="ql-font"
                  title="字体">
            <option value="SimSun">宋体</option>
            <option value="SimHei">黑体</option>
            <option value="Microsoft-YaHei">微软雅黑</option>
            <option value="KaiTi">楷体</option>
            <option value="FangSong">仿宋</option>
            <option value="Arial">Arial</option>
          </select>
          <!-- Add subscript and superscript buttons -->
          <select class="ql-color"
                  value="color"
                  title="字体颜色"></select>
          <select class="ql-background"
                  value="background"
                  title="背景颜色"></select>
          <select class="ql-align"
                  value="align"
                  title="对齐"></select>
          <button class="ql-clean"
                  title="还原"></button>
          <button class="ql-image"
                  title="图片"></button>
          <!-- <button class="ql-video"
                  title="视频"
                  @click="spClick"></button> -->
          <!-- You can also add your own -->
          <button id="custom-button"
                  @click.prevent="fnOpenUploadVideo('video','添加视频')"
                  title="视频">
            <i class="el-icon-film"></i>
          </button>
          <button id="custom-button"
                  @click.prevent="fnOpenUploadVideo('audio','添加音频')"
                  title="音频">
            <i class="el-icon-headset"></i>
          </button>
        </div>
      </quill-editor>
    </el-card>
    <el-dialog append-to-body
               :title="title"
               width="30%"
               :visible.sync="dialogFnOpenUpload"
               :close-on-click-modal="false">
      <!-- <file-upload accept="https://www.imflea.cn/ZeroFile/flea/file/upload" :data_extra="data_extra" @fnUploadSucess="fnUploadSucess" @fnCloseDialog="dialogFnOpenUpload = false" ref="fileUpload"></file-upload> -->
      <el-upload class="avatar-uploader"
                 action="https://www.imflea.cn/ZeroFile/flea/file/upload"
                 :on-success="fnUploadSucess"
                 :before-upload="beforeAvatarUpload"
                 ref="fileUpload">
        <i class="el-icon-upload avatar-uploader-icon"></i>
      </el-upload>
    </el-dialog>
  </div>
</template>

<script>
import * as Quill from 'quill'
import {
  quillEditor
} from 'vue-quill-editor'
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
import BMF from 'browser-md5-file'
import ImageResize from 'quill-image-resize-module' //调节图片大小
import { ImageDrop } from 'quill-image-drop-module' // 图片可拖动
// 这里引入修改过的video模块并注册
import Video from './video'
import Audio from './audio'

Quill.register(Video, true)
Quill.register(Audio, true)

Quill.register('modules/imageResize', ImageResize)
Quill.register('modules/imageDrop', ImageDrop)

const bmf = new BMF()

// 自定义字体大小
let Size = Quill.import('attributors/style/size')
Size.whitelist = ['10px', '12px', '14px', '16px', '18px', '20px']
Quill.register(Size, true)

// 自定义字体类型
var fonts = ['SimSun', 'SimHei', 'Microsoft-YaHei', 'KaiTi', 'FangSong', 'Arial', 'Times-New-Roman', 'sans-serif',
  '宋体', '黑体'
]
var Font = Quill.import('formats/font')
Font.whitelist = fonts
Quill.register(Font, true)

const uploadConfig = {
  fileRead: '', // 图片读取地址
  action: '',  // 必填参数 图片上传地址
  methods: 'POST',  // 必填参数 图片上传方式
  token: '',  // 可选参数 如果需要token验证,假设你的token有存放在sessionStorage
  name: 'img',  // 必填参数 文件的参数名
  size: 500,  // 可选参数   图片大小,单位为Kb, 1M = 1024Kb
  accept: 'image/png, image/gif, image/jpeg, image/bmp, image/x-icon'  // 可选 可上传的图片格式
}

function handle (file, callback) {
  bmf.md5(
    file,
    (err, md5) => {
      callback(md5)
    }
  )
}

const handlers = {
  image: function image () {
    var selft = this
    var fileInput = this.container.querySelector('input.ql-image[type=file]')
    if (fileInput === null) {
      fileInput = document.createElement('input')
      fileInput.setAttribute('type', 'file')
      // 设置图片参数名
      if (uploadConfig.name) {
        fileInput.setAttribute('name', uploadConfig.name)
      }
      // 可设置上传图片的格式
      fileInput.setAttribute('accept', uploadConfig.accept)
      fileInput.classList.add('ql-image')
      // 监听选择文件
      fileInput.addEventListener('change', function () {
        var file = fileInput.files[0]
        // 创建formData
        var formData = new FormData()
        var md5 = ''
        handle(file, r => {
          md5 = r
          var size = file.size
          // formData.append(uploadConfig.name, file)
          formData.append('md5', md5)
          formData.append('fileSize', size)
          formData.append('userId', '00000000000000000000000000000000')
          formData.append('businessSysCode', '1000000010000000000000000000')
          formData.append('businessSysModuleCode', '0000000000000000000000000000002')
          formData.append('businessId', 'B00000000000000000000000000000001')
          formData.append('name', file.name)
          formData.append('type', file.type)
          formData.append('lastModified', file.lastModified)
          formData.append('lastModifiedDate', file.lastModifiedDate)
          formData.append('size', size)
          formData.append('file', file)
          // 如果需要token且存在token
          if (uploadConfig.token) {
            formData.append('token', uploadConfig.token)
          }

          // 图片上传
          var xhr = new XMLHttpRequest()

          xhr.open(uploadConfig.methods, uploadConfig.action, true)
          xhr.setRequestHeader('Access-Control-Allow-Headers', '*')
          xhr.setRequestHeader('Access-Control-Allow-Origin', '*')
          // 上传数据成功,会触发
          xhr.onload = function () {
            if (xhr.status === 200) {
              var res = JSON.parse(xhr.responseText)
              let length = selft.quill.getSelection(true).index
              //这里很重要,你图片上传成功后,img的src需要在这里添加,res.path就是你服务器返回的图片链接。            
              selft.quill.insertEmbed(length, 'image', uploadConfig.fileRead + res.model.data.id)
              selft.quill.setSelection(length + 1)
            }
            fileInput.value = ''
          }
          // 开始上传数据
          xhr.upload.onloadstart = function () {
            fileInput.value = ''
          }
          // 当发生网络异常的时候会触发,如果上传数据的过程还未结束
          xhr.upload.onerror = function () {
          }
          // 上传数据完成(成功或者失败)时会触发
          xhr.upload.onloadend = function () {
            // console.log('上传结束')
          }
          xhr.send(formData)
        })
      })
      this.container.appendChild(fileInput)
    }
    fileInput.click();
  }
};

export default {
  name: 'ui-quill-editor',
  components: {
    quillEditor
  },
  props: {
    value: {
      type: String
    }
  },
  watch: {
    // 监听prop传的value,如果父级有变化了,将子组件的myValue也跟着变,达到父变子变的效果
    value (newVal) {
      this.content = newVal
    },
    // 监听content,如果子组件中的内容变化了,通知父级组件,将新的值告诉父级组件,我更新了,父级组件接受到值后页就跟着变了
    // 参考官网:https://cn.vuejs.org/v2/guide/components-custom-events.html#%E8%87%AA%E5%AE%9A%E4%B9%89%E7%BB%84%E4%BB%B6%E7%9A%84-v-model
    content (newVal) {
      this.$emit('input', newVal)
    }
  },
  data () {
    return {
      content: this.value,
      type: null,
      editorOption: {
        placeholder: '请输入',
        theme: 'snow', // or 'bubble' 
        modules: {
          toolbar: {
            container: '#toolbar',
            handlers: handlers
          },
          imageDrop: true,
          imageResize: {
            displayStyles: {
              backgroundColor: 'black',
              border: 'none',
              color: 'white'
            },
            modules: ['Resize', 'DisplaySize', 'Toolbar']
          }
        }
      },
      uploadType: '',
      title: '',
      dialogFnOpenUpload: false
    }
  },
  computed: {
    editor () {
      return this.$refs.myQuillEditor.quill
    }
  },
  methods: {
    beforeAvatarUpload (file) {
      if (this.uploadType === 'video') {
        this.type = file.type
        const isJPG = file.type === 'video/mp4'
        if (!isJPG) {
          this.$message.error('上传视频只能是 mp4 格式!')
        }
        return isJPG
      } else {
        this.type = file.type
        const isJPG = file.type === 'audio/mp3'
        if (!isJPG) {
          this.$message.error('上传音频只能是 mp3 格式!')
        }
        return isJPG
      }
    },
    spClick () {
      setTimeout(() => {
        let qlEditing = document.getElementsByClassName('ql-editing')
        if (qlEditing) {
          qlEditing[0].style.left = null
          qlEditing[0].style.right = '4px'
          qlEditing[0].style.top = '-6px'
        }
      }, 10)
    },
    fnOpenUploadVideo (type, title) {
      this.uploadType = type
      // this.accept = config.accept.video
      this.title = title
      this.dialogFnOpenUpload = true
    },
    fnUploadSucess (res) {
      this.editor.focus()
      let url = uploadConfig.fileRead + res.model.data.id
      let length = this.editor.getSelection().index
      this.editor.insertEmbed(length, this.uploadType, url)
      this.editor.setSelection(length + 1)
      this.dialogFnOpenUpload = false
    }
  }
}
</script>

<style less>
.ql-editor .ql-video {
  width: 340px;
  height: 180px;
}
.ql-size-small {
  font-size: 0.8rem;
}
.ql-size-normal {
  font-size: 1rem;
}
.ql-size-large {
  font-size: 1.2rem;
}
.ql-size-huge {
  font-size: 1.5rem;
  font-weight: bold;
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='SimSun']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='SimSun']::before {
  content: '宋体';
  font-family: 'SimSun';
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='SimHei']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='SimHei']::before {
  content: '黑体';
  font-family: 'SimHei';
}

.ql-snow
  .ql-picker.ql-font
  .ql-picker-label[data-value='Microsoft-YaHei']::before,
.ql-snow
  .ql-picker.ql-font
  .ql-picker-item[data-value='Microsoft-YaHei']::before {
  content: '微软雅黑';
  font-family: 'Microsoft YaHei';
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='KaiTi']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='KaiTi']::before {
  content: '楷体';
  font-family: 'KaiTi';
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='FangSong']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='FangSong']::before {
  content: '仿宋';
  font-family: 'FangSong';
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='Arial']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='Arial']::before {
  content: 'Arial';
  font-family: 'Arial';
}

.ql-snow
  .ql-picker.ql-font
  .ql-picker-label[data-value='Times-New-Roman']::before,
.ql-snow
  .ql-picker.ql-font
  .ql-picker-item[data-value='Times-New-Roman']::before {
  content: 'Times New Roman';
  font-family: 'Times New Roman';
}

.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='sans-serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='sans-serif']::before {
  content: 'sans-serif';
  font-family: 'sans-serif';
}

.ql-font-SimSun {
  font-family: 'SimSun';
}

.ql-font-SimHei {
  font-family: 'SimHei';
}

.ql-font-Microsoft-YaHei {
  font-family: 'Microsoft YaHei';
}

.ql-font-KaiTi {
  font-family: 'KaiTi';
}

.ql-font-FangSong {
  font-family: 'FangSong';
}

.ql-font-Arial {
  font-family: 'Arial';
}

.ql-font-Times-New-Roman {
  font-family: 'Times New Roman';
}

.ql-font-sans-serif {
  font-family: 'sans-serif';
}
.avatar-uploader {
  text-align: center;
}
.avatar-uploader-icon {
  font-size: 67px;
  color: #c0c4cc;
  margin: 20px 0 26px;
  line-height: 50px;
}
</style>

六、总结
很懒,什么也没有留下

七、参考文档

1、添加链接描述
2、添加链接描述
3、添加链接描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值