富文本编辑器vue-quill-editor项目实战

5,166 阅读4分钟

官网使用教程:github.com/surmon-chin…

安装

npm install vue-quill-editor --save

全局使用

import Vue from 'vue'
import VueQuillEditor from 'vue-quill-editor'

// require styles
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'

Vue.use(VueQuillEditor, /* { default global options } */)

注册自定义模块

// register quill modules, you need to introduce and register before the vue program is instantiated
import Quill from 'quill'
import yourQuillModule from '../yourModulePath/yourQuillModule.js'
Quill.register('modules/yourQuillModule', yourQuillModule)

组件中使用方法

<template>
  <!-- bidirectional data binding(双向数据绑定) -->
  <quill-editor v-model="content"
                ref="myQuillEditor"
                :options="editorOption"
                @blur="onEditorBlur($event)"
                @focus="onEditorFocus($event)"
                @ready="onEditorReady($event)">
  </quill-editor>

  <!-- Or manually control the data synchronization(或手动控制数据流) -->
  <quill-editor :content="content"
                :options="editorOption"
                @change="onEditorChange($event)">
  </quill-editor>
</template>

<script>

  // you can also register quill modules in the component
  import Quill from 'quill'
  import { someModule } from '../yourModulePath/someQuillModule.js'
  Quill.register('modules/someModule', someModule)
  
  export default {
    data () {
      return {
        content: '<h2>I am Example</h2>',
        editorOption: {
          // some quill options
        }
      }
    },
    // manually control the data synchronization
    // 如果需要手动控制数据同步,父组件需要显式地处理changed事件
    methods: {
      onEditorBlur(quill) {
        console.log('editor blur!', quill)
      },
      onEditorFocus(quill) {
        console.log('editor focus!', quill)
      },
      onEditorReady(quill) {
        console.log('editor ready!', quill)
      },
      onEditorChange({ quill, html, text }) {
        console.log('editor change!', quill, html, text)
        this.content = html
      }
    },
    computed: {
      editor() {
        return this.$refs.myQuillEditor.quill
      }
    },
    mounted() {
      console.log('this is current quill instance object', this.editor)
    }
  }
</script>

自己项目中使用的vue-quill-editor

<template >
    <div title="拖动右下角,改变高度" class="quill-editor-container">
        <quill-editor
            ref="quillEditor"
            v-model="editorValue"
            :options="editorOption"
            :disabled="disabled">
        </quill-editor>
    </div>
</template>

<script>
import Vue from 'vue'
import { quillEditor, Quill } from 'vue-quill-editor';
import ImageResize from 'quill-image-resize-module';
import { container, ImageExtend, QuillWatch } from './quill-image-extend-module';
import { VideoExtend, QuillVideoWatch } from './quill-video-extend-module'
import FileBlot from './quill-blot/file-blot.js';
import MagicUrl from './quill-magic-url/src/index.js';
// import MagicUrl from 'quill-magic-url';
import 'quill/dist/quill.snow.css';

Quill.debug('error');
Quill.register('modules/imageResize', ImageResize);
Quill.register('modules/ImageExtend', ImageExtend);
Quill.register('modules/VideoExtend', VideoExtend);
Quill.register('modules/magicUrl', MagicUrl);
Quill.register(FileBlot);

let icons = Quill.import('ui/icons');
icons['video'] = '<i class="iconfont icon-fujian2" style="font-size: 14px;"></i>';

export default {
    components: {
        quillEditor
    },
    props: {
        value: {
            type: String,
            default: ''
        },
        disabled: {
            type: Boolean,
            default: false
        },
        placeholder: {
            type: String,
            default: ''
        }
    },
    data: function () {
        return {
            editorValue: this.value,
            editorOption: {
                modules: {
                    toolbar: {
                        container: [
                            ['bold', 'italic', 'underline', 'strike', 'clean', 'blockquote'],
                            [{ 'header': 1 }, { 'header': 2 }],
                            [{ 'list': 'ordered' }, { 'list': 'bullet' }],
                            [{ 'indent': '-1' }, { 'indent': '+1' }],
                            [{ 'color': [] }],
                            [{ 'align': [] }],
                            ['link', 'image', 'video']
                        ],
                        handlers: {
                            'image': function () {
                                QuillWatch.emit(this.quill.id);
                            },
                            'video': function() {
                                QuillVideoWatch.emit(this.quill.fileId);
                            }
                        }
                    },
                    imageResize: {
                        displaySize: true
                    },
                    ImageExtend: {
                        loading: true,
                        name: 'file',
                        action: '/api/v1.0/external/image',
                        response: (res) => {
                            if (res.location) return res.location;
                            console.log('upload image err: ', res.error);
                            return;
                        }
                    },
                    VideoExtend: {
                        loading: true,
                        name: 'file',
                        action: '/api/v1.0/external/file',
                        response: (res) => {
                            if (res.location) return res.location;
                            console.log('upload file err: ', res.error);
                            return;
                        }
                    },
                    magicUrl: {
                        globalRegularExpression: /(https?:\/\/|www\.|mailto:|tel:)[\S]+/g,
                        urlRegularExpression: /(https?:\/\/[\S]+)|(www.[\S]+)|(mailto:[\S]+)|(tel:[\S]+)/
                    }
                },
                theme: 'snow',
                placeholder: this.placeholder
            }
        };
    },
    mount () {
        // API.generateS3url({file_name: '12.txt'})
    },
    methods: {
        onClick(e) {
            this.$emit('onClick', e, tinymce)
        },
        //可以添加一些自己的自定义事件,如清空内容
        clear() {
            this.editorValue = ''
        }
    },
    watch: {
        value(newValue) {
            if (newValue.indexOf('<img') > -1 && newValue.indexOf('src="https://km.sankuai.com') > -1) {
                let reg = /<img.*?(?:>|\/>)/gi;
                let removeImgValue = newValue.replace(reg, '');
                this.editorValue = removeImgValue;
            } else {
                this.editorValue = newValue;
            }
        },
        editorValue(newValue) {
            this.$emit('input', newValue)
        }
    }
};
</script>

<style lang="postcss">
.quill-editor-container {
    .quill-editor {
        div::after {
          content: '';
          display: block;
          width: 10px;
          height: 10px;
          /* background-color: gray; */
          /* background-image:
            linear-gradient(hsla(0,0%,80%,1) 50%, transparent 0),
            linear-gradient(90deg,hsla(0,0%,80%,1) 50%, transparent 0); */
          position: absolute;
          right: 0;
          bottom: 0;
          cursor: grab;
        }
        .ql-container {
            /* height: 400px; */
            resize: vertical;
            overflow-y: scroll;
            .ql-editor {
                color: #464646;
                font-size: 14px;
                font-family: PingFangSC-Regular,PingFangSC,"Helvetica Neue","Hiragino Sans GB","Arial","Microsoft YaHei","sans-serif";
                blockquote {
                    display: block;
                    margin-block-start: 1em;
                    margin-block-end: 1em;
                    margin-inline-start: 40px;
                    margin-inline-end: 40px;
                    padding-left: 16px;
                    border-left: 2px solid #ddd;
                    margin: 0px 0px 0px 8px;
                    color: #999;
                    ::after ::before {
                        box-sizing: border-box;
                    }
                }
            }
        }
    }
}

</style>

自定义quill-image-extend-module

/**
 *@description 观察者模式 全局监听富文本编辑器
 */
export const QuillWatch = {
    watcher: {},  // 登记编辑器信息
    active: null,  // 当前触发的编辑器
    on: function (imageExtendId, ImageExtend) {  // 登记注册使用了ImageEXtend的编辑器
        if (!this.watcher[imageExtendId]) {
            this.watcher[imageExtendId] = ImageExtend
        }
    },
    emit: function (activeId, type = 1) {  // 事件发射触发
        this.active = this.watcher[activeId]
        if (type === 1) {
            imgHandler()
        }
    }
}

/**
 * @description 图片功能拓展: 增加上传 拖动 复制
 */
export class ImageExtend {
    /**
     * @param quill {Quill}富文本实例
     * @param config {Object} options
     * config  keys: action, headers, editForm start end error  size response
     */
    constructor(quill, config = {}) {
        this.id = Math.random()
        this.quill = quill
        this.quill.id = this.id
        this.config = config
        this.file = ''  // 要上传的图片
        this.imgURL = ''  // 图片地址
        quill.root.addEventListener('paste', this.pasteHandle.bind(this), false)
        quill.root.addEventListener('drop', this.dropHandle.bind(this), false)
        quill.root.addEventListener('dropover', function (e) {
            e.preventDefault()
        }, false)
        this.cursorIndex = 0
        QuillWatch.on(this.id, this)
    }

    /**
     * @description 粘贴
     * @param e
     */
    pasteHandle(e) {
        // e.preventDefault()
        QuillWatch.emit(this.quill.id, 0)
        let clipboardData = e.clipboardData
        let i = 0
        let items, item, types

        if (clipboardData) {
            items = clipboardData.items;

            if (!items) {
                return;
            }
            item = items[0];
            types = clipboardData.types || [];

            for (; i < types.length; i++) {
                if (types[i] === 'Files') {
                    item = items[i];
                    break;
                }
            }
            if (item && item.kind === 'file' && item.type.match(/^image\//i)) {
                this.file = item.getAsFile()
                let self = this
                // 如果图片限制大小
                if (self.config.size && self.file.size >= self.config.size * 1024 * 1024) {
                    if (self.config.sizeError) {
                        self.config.sizeError()
                    }
                    return
                }
                if (this.config.action) {
                    this.uploadImg()
                } else {
                    QuillWatch.active.uploading();
                    QuillWatch.active.uploadSuccess();
                    this.toBase64()
                }
            }
        }
    }

    /**
     * 拖拽
     * @param e
     */
    dropHandle(e) {
        QuillWatch.emit(this.quill.id, 0)
        const self = this
        e.preventDefault()
        // 如果图片限制大小
        if (self.config.size && self.file.size >= self.config.size * 1024 * 1024) {
            if (self.config.sizeError) {
                self.config.sizeError()
            }
            return
        }
        self.file = e.dataTransfer.files[0]; // 获取到第一个上传的文件对象
        if (this.config.action) {
            self.uploadImg()
        } else {
            self.toBase64()
        }
    }

    /**
     * @description 将图片转为base4
     */
    toBase64() {
        const self = this
        const reader = new FileReader()
        reader.onload = (e) => {
            // 返回base64
            self.imgURL = e.target.result
            self.insertImg()
        }
        reader.readAsDataURL(self.file)
    }

    /**
     * @description 上传图片到服务器
     */
    uploadImg() {
        const self = this
        let quillLoading = self.quillLoading
        let config = self.config
        // 构造表单
        let formData = new FormData()
        formData.append(config.name, self.file)
        // 自定义修改表单
        if (config.editForm) {
            config.editForm(formData)
        }
        // 创建ajax请求
        let xhr = new XMLHttpRequest()
        xhr.open('post', config.action, true)
        // 如果有设置请求头
        if (config.headers) {
            config.headers(xhr)
        }
        if (config.change) {
            config.change(xhr, formData)
        }
        xhr.onreadystatechange = function () {
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {
                    //success
                    let res = JSON.parse(xhr.responseText)
                    self.imgURL = config.response(res)
                    QuillWatch.active.uploadSuccess()
                    self.insertImg()
                    if (self.config.success) {
                        self.config.success()
                    }
                } else {
                    //error
                    if (self.config.error) {
                        self.config.error()
                    }
                    QuillWatch.active.uploadError()
                }
            }
        }
        // 开始上传数据
        xhr.upload.onloadstart = function (e) {
            QuillWatch.active.uploading()
            // let length = (self.quill.getSelection() || {}).index || self.quill.getLength()
            // self.quill.insertText(length, '[uploading...]', { 'color': 'red'}, true)
            if (config.start) {
                config.start()
            }
        }
        // 上传过程
        xhr.upload.onprogress = function (e) {
            let complete = (e.loaded / e.total * 100 | 0) + '%'
            QuillWatch.active.progress(complete)
        }
        // 当发生网络异常的时候会触发,如果上传数据的过程还未结束
        xhr.upload.onerror = function (e) {
            QuillWatch.active.uploadError()
            if (config.error) {
                config.error()
            }
        }
        // 上传数据完成(成功或者失败)时会触发
        xhr.upload.onloadend = function (e) {
            if (config.end) {
                config.end()
            }
        }
        xhr.send(formData)
    }

    /**
     * @description 往富文本编辑器插入图片
     */
    insertImg() {
        const self = QuillWatch.active
        if (!this.config.timeline) {
            self.quill.insertEmbed(QuillWatch.active.cursorIndex, 'image', self.imgURL)
            self.quill.update()
        }
        // self.quill.blur()
        self.quill.setSelection(self.cursorIndex);
    }

    /**
     * @description 显示上传的进度
     */
    progress(pro) {
        pro = '[' + 'uploading' + pro + ']'
        QuillWatch.active.quill.root.innerHTML
            = QuillWatch.active.quill.root.innerHTML.replace(/\[uploading.*?\]/, pro)
    }

    /**
     * 开始上传
     */
    uploading() {
        let length = (QuillWatch.active.quill.getSelection() || {}).index || QuillWatch.active.quill.getLength()
        QuillWatch.active.cursorIndex = length
        if (this.config.timeline) {
            QuillWatch.active.quill.insertText(QuillWatch.active.cursorIndex - 1, '[uploading...]', {'color': 'red'}, true)
        } else {
            QuillWatch.active.quill.insertText(QuillWatch.active.cursorIndex, '[uploading...]', {'color': 'red'}, true)
        }
    }

    /**
     * 上传失败
     */
    uploadError() {
        QuillWatch.active.quill.root.innerHTML
            = QuillWatch.active.quill.root.innerHTML.replace(/\[uploading.*?\]/, '[upload error]')
    }

    uploadSuccess() {
        QuillWatch.active.quill.root.innerHTML
            = QuillWatch.active.quill.root.innerHTML.replace(/\[uploading.*?\]/, '')
    }
}

/**
 * @description 点击图片上传
 */
export function imgHandler() {
    let fileInput = document.querySelector('.quill-image-input');
    if (fileInput === null) {
        fileInput = document.createElement('input');
        fileInput.setAttribute('type', 'file');
        fileInput.classList.add('quill-image-input');
        fileInput.style.display = 'none'
        // 监听选择文件
        fileInput.addEventListener('change', function () {
            let self = QuillWatch.active
            self.file = fileInput.files[0]
            fileInput.value = ''
            // 如果图片限制大小
            if (self.config.size && self.file.size >= self.config.size * 1024 * 1024) {
                if (self.config.sizeError) {
                    self.config.sizeError()
                }
                return
            }
            if (self.config.action) {
                self.uploadImg()
            } else {
                self.toBase64()
            }
        })
        document.body.appendChild(fileInput);
    }
    fileInput.click();
}

/**
 *@description 全部工具栏
 */
export const container = [
    ['bold', 'italic', 'underline', 'strike'],
    ['blockquote', 'code-block'],
    [{'header': 1}, {'header': 2}],
    [{'list': 'ordered'}, {'list': 'bullet'}],
    [{'script': 'sub'}, {'script': 'super'}],
    [{'indent': '-1'}, {'indent': '+1'}],
    [{'direction': 'rtl'}],
    [{'size': ['small', false, 'large', 'huge']}],
    [{'header': [1, 2, 3, 4, 5, 6, false]}],
    [{'color': []}, {'background': []}],
    [{'font': []}],
    [{'align': []}],
    ['clean'],
    ['link', 'image', 'video']
]