vue-quill-editor的使用

1,175 阅读1分钟

基础使用

vue-quill-editor入口

import _Quill from 'quill'
import quillEditor from './editor.vue'

const Quill = window.Quill || _Quill
const install = (Vue, globalOptions) => {
  if (globalOptions) {
    quillEditor.props.globalOptions.default = () => globalOptions
  }
  Vue.component(quillEditor.name, quillEditor)
}

const VueQuillEditor = { Quill, quillEditor, install }

export default VueQuillEditor
export { Quill, quillEditor, install }

由vue-quill-editor入口可以知道,可以直接通过Vue.use直接注册,便可使用组件,如果不希望全局注册,也可以接受导出的组件quillEditor来使用

项目中的使用

import 'quill/dist/quill.core.css';
import 'quill/dist/quill.snow.css';
import 'quill/dist/quill.bubble.css';

import { quillEditor, Quill } from 'vue-quill-editor';

export { Quill };
export default quillEditor;

导出组件和Quill,Quill方便扩展使用

组件的使用

<template>
  <div>
    <Quill-editor
      ref="myQuillEditor"
      :value="value"
      :options="editorOption"
      @input="handleInput"
      @blur="onEditorBlur($event)"
      @focus="onEditorFocus($event)"
      @ready="onEditorReady($event)"
      @change="onEditorChange($event)"
    />
  </div>
</template>
<script>
import QuillEditor from './vue-quill-editor';

export default {
  components: {
    QuillEditor
  },
  props: {
    value: {
      type: String,
      default: ''
    }
  },
  data() {
    return {
      editorOption: {
        placeholder: '输入文本...',
        theme: 'snow',
        modules: {
          toolbar: {
            container: [
              ['bold', 'italic', 'underline', 'strike'], // 加粗,斜体,下划线,删除线
              ['blockquote', 'code-block'], // 引用,代码块
              [{ header: 1 }, { header: 2 }], // 标题,键值对的形式;1、2表示字体大小
              [{ list: 'ordered' }, { list: 'bullet' }], // 列表
              [{ script: 'sub' }, { script: 'super' }], // 上下标
              [{ indent: '-1' }, { indent: '+1' }], // 缩进
              [{ direction: 'rtl' }], // 文本方向
              [{ header: [1, 2, 3, 4, 5, 6, false] }], // 几级标题
              [{ color: [] }, { background: [] }], // 字体颜色,字体背景颜色
              [{ align: [] }], // 对齐方式
              ['clean'], // 清除字体样式
              ['image', 'video'] // 上传图片、上传视频
            ]
          }
        }
      }
    };
  },
  methods: {
    handleInput(val) {
      this.$emit('input', val);
    },
    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;
    }
  }
};
</script>


源码流程分析

Quill注册方法

class Quill {
  static register(path, target, overwrite = false) {
  // 如果传入的path不是字符串,那么就根据传入的参数判断,转为对应的字符串参数继续调用
    if (typeof path !== 'string') {
      let name = path.attrName || path.blotName;
      if (typeof name === 'string') {
        // register(Blot | Attributor, overwrite)
        this.register('formats/' + name, path, target);
      } else {
        Object.keys(path).forEach((key) => {
          this.register(key, path[key], target);
        });
      }
    } else {
      if (this.imports[path] != null && !overwrite) {
        debug.warn(`Overwriting ${path} with`, target);
      }
      // 将字符串和对应构造函数添加到Quill.import的对象中
      this.imports[path] = target;
      // 如果是以blots/或者以formats/开头,那么用Parchment注册
      if ((path.startsWith('blots/') || path.startsWith('formats/')) &&
          target.blotName !== 'abstract') {
        Parchment.register(target);
       	// 如果以modules开头,并且有register且为函数,则调用注册
      } else if (path.startsWith('modules') && typeof target.register === 'function') {
        target.register();
      }
    }
  }
}
Quill.imports = {
  'delta'       : Delta,
  'parchment'   : Parchment,
  'core/module' : Module,
  'core/theme'  : Theme
};

获取Quill注册的构造函数

class Quill {
	// 传入注册的名字即可获得构造函数
  static import(name) {
    if (this.imports[name] == null) {
      debug.error(`Cannot import ${name}. Are you sure it was registered?`);
    }
    return this.imports[name];
  }
}

主要的流程new Quill

class Quill {
  constructor(container, options = {}) {
    this.options = expandConfig(container, options); // 合并theme,Quill,传入的option参数
    this.container = this.options.container;
    if (this.container == null) {
      return debug.error('Invalid Quill container', container);
    }
    if (this.options.debug) {
      Quill.debug(this.options.debug);
    }
    let html = this.container.innerHTML.trim();
    this.container.classList.add('ql-container');
    this.container.innerHTML = '';
    this.container.__quill = this;
    this.root = this.addContainer('ql-editor');
    this.root.classList.add('ql-blank');
    this.root.setAttribute('data-gramm', false);
    this.scrollingContainer = this.options.scrollingContainer || this.root;
    this.emitter = new Emitter(); // 监听
    this.scroll = Parchment.create(this.root, {
      emitter: this.emitter,
      whitelist: this.options.formats
    });
    this.editor = new Editor(this.scroll); // 实例化editor(编辑内容相关)
    this.selection = new Selection(this.scroll, this.emitter); // 实例化selection(光标相关)
    this.theme = new this.options.theme(this, this.options); // 实例化对应的theme(工具主题相关)
    this.keyboard = this.theme.addModule('keyboard'); // 实例化keyboard(快捷键相关)
    this.clipboard = this.theme.addModule('clipboard'); // 实例化clipboard(复制黏贴相关)
    this.history = this.theme.addModule('history'); // 实例化history(前进撤销相关)
    this.theme.init(); // 进入
    this.emitter.on(Emitter.events.EDITOR_CHANGE, (type) => {
      if (type === Emitter.events.TEXT_CHANGE) {
        this.root.classList.toggle('ql-blank', this.editor.isBlank());
      }
    });
    this.emitter.on(Emitter.events.SCROLL_UPDATE, (source, mutations) => {
      let range = this.selection.lastRange;
      let index = range && range.length === 0 ? range.index : undefined;
      modify.call(this, () => {
        return this.editor.update(null, mutations, index);
      }, source);
    });
    let contents = this.clipboard.convert(`<div class='ql-editor' style="white-space: normal;">${html}<p><br></p></div>`);
    this.setContents(contents);
    this.history.clear();
    if (this.options.placeholder) {
      this.root.setAttribute('data-placeholder', this.options.placeholder);
    }
    if (this.options.readOnly) {
      this.disable();
    }
  }
}

Theme构造函数的init(主要的是toolbar的实例化)

class Theme {
  constructor(quill, options) {
    this.quill = quill;
    this.options = options;
    this.modules = {};
  }

  init() {
  // 实例化合并后的option中的modules
    Object.keys(this.options.modules).forEach((name) => {
      if (this.modules[name] == null) {
        this.addModule(name);
      }
    });
  }
  addModule(name) {
    let moduleClass = this.quill.constructor.import(`modules/${name}`);
    // 传入模块实例化的参数为对应模块名的option(传入的modules和其他默认值合并后的结果)
    this.modules[name] = new moduleClass(this.quill, this.options.modules[name] || {});
    return this.modules[name];
  }
}

class BaseTheme extends Theme {
  addModule(name) {
    let module = super.addModule(name);
    if (name === 'toolbar') {
    	// toolbar实例化后进入
      this.extendToolbar(module);
    }
    return module;
  }
}

Toolbar构造函数

class Toolbar extends Module {
  constructor(quill, options) {
    super(quill, options);
    if (Array.isArray(this.options.container)) {
      let container = document.createElement('div');
      // 根据module.toolbar里面的container,建立对应dom
      addControls(container, this.options.container);
      quill.container.parentNode.insertBefore(container, quill.container);
      this.container = container;
    } else if (typeof this.options.container === 'string') {
      this.container = document.querySelector(this.options.container);
    } else {
      this.container = this.options.container;
    }
    if (!(this.container instanceof HTMLElement)) {
      return debug.error('Container required for toolbar', this.options);
    }
    this.container.classList.add('ql-toolbar');
    this.controls = [];
    this.handlers = {};
    // 将module.toolbar里面的handlers都赋值给this.handlers
    Object.keys(this.options.handlers).forEach((format) => {
      this.addHandler(format, this.options.handlers[format]);
    });
    // 给根据toolbar.container建立的dom元素都建立事件,触发对应的效果
    [].forEach.call(this.container.querySelectorAll('button, select'), (input) => {
      this.attach(input);
    });
    this.quill.on(Quill.events.EDITOR_CHANGE, (type, range) => {
      if (type === Quill.events.SELECTION_CHANGE) {
        this.update(range);
      }
    });
    this.quill.on(Quill.events.SCROLL_OPTIMIZE, () => {
      let [range, ] = this.quill.selection.getRange();  // quill.getSelection triggers update
      this.update(range);
    });
  }

  addHandler(format, handler) {
    this.handlers[format] = handler;
  }

  attach(input) {
    let format = [].find.call(input.classList, (className) => {
      return className.indexOf('ql-') === 0;
    });
    if (!format) return;
    format = format.slice('ql-'.length);
    if (input.tagName === 'BUTTON') {
      input.setAttribute('type', 'button');
    }
    if (this.handlers[format] == null) {
      if (this.quill.scroll.whitelist != null && this.quill.scroll.whitelist[format] == null) {
        debug.warn('ignoring attaching to disabled format', format, input);
        return;
      }
      if (Parchment.query(format) == null) {
        debug.warn('ignoring attaching to nonexistent format', format, input);
        return;
      }
    }
    // 确保触发的事件正确
    let eventName = input.tagName === 'SELECT' ? 'change' : 'click';
    input.addEventListener(eventName, (e) => {
      let value;
      if (input.tagName === 'SELECT') {
        if (input.selectedIndex < 0) return;
        let selected = input.options[input.selectedIndex];
        if (selected.hasAttribute('selected')) {
          value = false;
        } else {
          value = selected.value || false;
        }
      } else {
        if (input.classList.contains('ql-active')) {
          value = false;
        } else {
          value = input.value || !input.hasAttribute('value');
        }
        e.preventDefault();
      }
      this.quill.focus();
      let [range, ] = this.quill.selection.getRange();
      // 查看toolbar.handlers里面有没有跟触发事件的dom相同名字的,有的话就调用,没有的话就根据事先定义好的方法进行处理
      if (this.handlers[format] != null) {
        this.handlers[format].call(this, value);
      // Parchment.query应该是找到Parchment注册的内容,之后按照找到的内容进行处理,待了解
      } else if (Parchment.query(format).prototype instanceof Parchment.Embed) {
        value = prompt(`Enter ${format}`);
        if (!value) return;
        this.quill.updateContents(new Delta()
          .retain(range.index)
          .delete(range.length)
          .insert({ [format]: value })
        , Quill.sources.USER);
      } else {
        this.quill.format(format, value, Quill.sources.USER);
      }
      this.update(range);
    });
    // TODO use weakmap
    this.controls.push([format, input]);
  }

  update(range) {
    let formats = range == null ? {} : this.quill.getFormat(range);
    this.controls.forEach(function(pair) {
      let [format, input] = pair;
      if (input.tagName === 'SELECT') {
        let option;
        if (range == null) {
          option = null;
        } else if (formats[format] == null) {
          option = input.querySelector('option[selected]');
        } else if (!Array.isArray(formats[format])) {
          let value = formats[format];
          if (typeof value === 'string') {
            value = value.replace(/\"/g, '\\"');
          }
          option = input.querySelector(`option[value="${value}"]`);
        }
        if (option == null) {
          input.value = '';   // TODO make configurable?
          input.selectedIndex = -1;
        } else {
          option.selected = true;
        }
      } else {
        if (range == null) {
          input.classList.remove('ql-active');
        } else if (input.hasAttribute('value')) {
          // both being null should match (default values)
          // '1' should match with 1 (headers)
          let isActive = formats[format] === input.getAttribute('value') ||
                         (formats[format] != null && formats[format].toString() === input.getAttribute('value')) ||
                         (formats[format] == null && !input.getAttribute('value'));
          input.classList.toggle('ql-active', isActive);
        } else {
          input.classList.toggle('ql-active', formats[format] != null);
        }
      }
    });
  }
}
Toolbar.DEFAULTS = {};


function addButton(container, format, value) {
  let input = document.createElement('button');
  input.setAttribute('type', 'button');
  input.classList.add('ql-' + format);
  if (value != null) {
    input.value = value;
  }
  container.appendChild(input);
}

// 遍历container数组里面数组的每一项,
// 如果是字符串就创建button,
// 如果是对象,就看对象的值是不是数组,是的话,就创建select,option值为对象数组内的每一项
// 不是的话同样创建button
function addControls(container, groups) {
  if (!Array.isArray(groups[0])) {
    groups = [groups];
  }
  groups.forEach(function(controls) {
    let group = document.createElement('span');
    group.classList.add('ql-formats');
    controls.forEach(function(control) {
      if (typeof control === 'string') {
        addButton(group, control);
      } else {
        let format = Object.keys(control)[0];
        let value = control[format];
        if (Array.isArray(value)) {
          addSelect(group, format, value);
        } else {
          addButton(group, format, value);
        }
      }
    });
    container.appendChild(group);
  });
}

function addSelect(container, format, values) {
  let input = document.createElement('select');
  input.classList.add('ql-' + format);
  values.forEach(function(value) {
    let option = document.createElement('option');
    if (value !== false) {
      option.setAttribute('value', value);
    } else {
      option.setAttribute('selected', 'selected');
    }
    input.appendChild(option);
  });
  container.appendChild(input);
}

extendToolbar(如果实例化的模块式toolbar,则还需要进行这个处理)

class BaseTheme extends Theme {
  addModule(name) {
    let module = super.addModule(name);
    if (name === 'toolbar') {
      this.extendToolbar(module);
    }
    return module;
  }

  buildButtons(buttons, icons) {
    buttons.forEach((button) => {
      let className = button.getAttribute('class') || '';
      className.split(/\s+/).forEach((name) => {
        if (!name.startsWith('ql-')) return;
        name = name.slice('ql-'.length);
        if (icons[name] == null) return;
        if (name === 'direction') {
          button.innerHTML = icons[name][''] + icons[name]['rtl'];
        } else if (typeof icons[name] === 'string') {
          button.innerHTML = icons[name];
        } else {
          let value = button.value || '';
          if (value != null && icons[name][value]) {
            button.innerHTML = icons[name][value];
          }
        }
      });
    });
  }

  buildPickers(selects, icons) {
    this.pickers = selects.map((select) => {
      if (select.classList.contains('ql-align')) {
        if (select.querySelector('option') == null) {
          fillSelect(select, ALIGNS);
        }
        // 根据data-value映射事先定义好的图标对象获取对应图标,data-value为创建option时的value
        return new IconPicker(select, icons.align);
      } else if (select.classList.contains('ql-background') || select.classList.contains('ql-color')) {
        let format = select.classList.contains('ql-background') ? 'background' : 'color';
        if (select.querySelector('option') == null) {
          fillSelect(select, COLORS, format === 'background' ? '#ffffff' : '#000000');
        }
        // 获取对应颜色,,data-value为创建option时的value
        return new ColorPicker(select, icons[format]);
      } else {
        if (select.querySelector('option') == null) {
          if (select.classList.contains('ql-font')) {
            fillSelect(select, FONTS);
          } else if (select.classList.contains('ql-header')) {
            fillSelect(select, HEADERS);
          } else if (select.classList.contains('ql-size')) {
            fillSelect(select, SIZES);
          }
        }
        // 取值,,data-value为创建option时的value   进入
        return new Picker(select);
      }
    });
    let update = () => {
      this.pickers.forEach(function(picker) {
        picker.update();
      });
    };
    this.quill.on(Emitter.events.EDITOR_CHANGE, update);
  }
}


class SnowTheme extends BaseTheme {
  extendToolbar(toolbar) {
    toolbar.container.classList.add('ql-snow');
    // 给所有类型为button的添加图标
    this.buildButtons([].slice.call(toolbar.container.querySelectorAll('button')), icons);
    // 给所有类型为select的添加图标或者设置值,并且,看传入的toolbar.container里面的align,background,color,font,header,size数组是否为空,如果是,那么就采用默认配置
    this.buildPickers([].slice.call(toolbar.container.querySelectorAll('select')), icons);
    this.tooltip = new SnowTooltip(this.quill, this.options.bounds);
    if (toolbar.container.querySelector('.ql-link')) {
      this.quill.keyboard.addBinding({ key: 'K', shortKey: true }, function(range, context) {
        toolbar.handlers['link'].call(toolbar, !context.format.link);
      });
    }
  }
}

buildPickers(toolbar里面类型为select的设置)

class Picker {
  constructor(select) {
    this.select = select;
    this.container = document.createElement('span');
    // 进入
    this.buildPicker();
    this.select.style.display = 'none';
    this.select.parentNode.insertBefore(this.container, this.select);
    this.label.addEventListener('mousedown', () => {
      this.togglePicker();
    });
    this.label.addEventListener('keydown', (event) => {
      switch(event.keyCode) {
        // Allows the "Enter" key to open the picker
        case Keyboard.keys.ENTER:
          this.togglePicker();
          break;

        // Allows the "Escape" key to close the picker
        case Keyboard.keys.ESCAPE:
          this.escape();
          event.preventDefault();
          break;
        default:
      }
    });
    this.select.addEventListener('change', this.update.bind(this));
  }

  buildItem(option) {
    let item = document.createElement('span');
    item.tabIndex = '0';
    item.setAttribute('role', 'button');
	// 给新的dom设置类名
    item.classList.add('ql-picker-item');
    // 获取之前toolbar.container创建好的option的value,给新的dom设置data-value
    // 即之后这个设置值可以这样   对应类型 + .ql-picker-item[data-value="10px"]::before 里面给content: 设值
    if (option.hasAttribute('value')) {
      item.setAttribute('data-value', option.getAttribute('value'));
    }
    if (option.textContent) {
      item.setAttribute('data-label', option.textContent);
    }
    // 点击调用取值对应的option的事件
    item.addEventListener('click', () => {
      this.selectItem(item, true);
    });
    item.addEventListener('keydown', (event) => {
      switch(event.keyCode) {
        // Allows the "Enter" key to select an item
        case Keyboard.keys.ENTER:
          this.selectItem(item, true);
          event.preventDefault();
          break;

        // Allows the "Escape" key to close the picker
        case Keyboard.keys.ESCAPE:
          this.escape();
          event.preventDefault();
          break;
        default:
      }
    });

    return item;
  }

  buildOptions() {
    let options = document.createElement('span');
    options.classList.add('ql-picker-options');

    // Don't want screen readers to read this until options are visible
    options.setAttribute('aria-hidden', 'true');
    options.tabIndex = '-1';

    // Need a unique id for aria-controls
    options.id = `ql-picker-options-${optionsCounter}`;
    optionsCounter += 1;
    this.label.setAttribute('aria-controls', options.id);

    this.options = options;
	// 到这里,遍历之前根据toolbar.container创建好的option
    [].slice.call(this.select.options).forEach((option) => {
    	// 进入
      let item = this.buildItem(option);
      options.appendChild(item);
      if (option.selected === true) {
        this.selectItem(item);
      }
    });
    this.container.appendChild(options);
  }

  buildPicker() {
    [].slice.call(this.select.attributes).forEach((item) => {
      this.container.setAttribute(item.name, item.value);
    });
    // 创建新容器
    this.container.classList.add('ql-picker');
    this.label = this.buildLabel();
    // 进入
    this.buildOptions();
  }
	// 点击新容器中的选项会触发旧select中的事件
  selectItem(item, trigger = false) {
    let selected = this.container.querySelector('.ql-selected');
    if (item === selected) return;
    if (selected != null) {
      selected.classList.remove('ql-selected');
    }
    if (item == null) return;
    item.classList.add('ql-selected');
    this.select.selectedIndex = [].indexOf.call(item.parentNode.children, item);
    if (item.hasAttribute('data-value')) {
      this.label.setAttribute('data-value', item.getAttribute('data-value'));
    } else {
      this.label.removeAttribute('data-value');
    }
    if (item.hasAttribute('data-label')) {
      this.label.setAttribute('data-label', item.getAttribute('data-label'));
    } else {
      this.label.removeAttribute('data-label');
    }
    if (trigger) {
      if (typeof Event === 'function') {
        this.select.dispatchEvent(new Event('change'));
      } else if (typeof Event === 'object') {     // IE11
        let event = document.createEvent('Event');
        event.initEvent('change', true, true);
        this.select.dispatchEvent(event);
      }
      this.close();
    }
  }


扩展

  • 当new Quill,实例化module时,是将配置的option.module参数传入对应的module里面所以,新增模块时,可以自定义参数参入自定义的模块里面
  • Quill.import('对应名字'),可以拿到里面注册的构造函数,修改会影响原有功能
  • toolbar如果传false工具栏就不会出现了,参数container为数组的话,会覆盖默认的值,所以需要手动配置,工具栏可以触发定义好的事件,默认有一些处理事件,如果需要可以在handlers中拦截,需要拦截函数需要命名和container定义的一样,会将当前触发事件后拿到的值传过来.如果有默认的处理方式也会被重写
  • toolbar.container中select会被创建多一份容器用作显示,当用作显示的呗选择时,会触发原本select的事件,新容器中的option类名为.ql-picker-item,并且data-value为原本option中的value,所以,可以借此来通过css来设置值
  • 在toolbar.container配置新的select类型的项目,或者对旧的进行修改,配置的内容需要和format中对应,而下拉框的值,可以通过 类型 + .ql-picker-item[data-value="对应的值"]::before,设置content,如果需要其他样式接着在这设置,只要toolbar.container设置的值和format中对应了,效果也就可以了

例子1: 自定义对复制黏贴进行处理模块

export class PasteFormat {
  constructor(quill, config) {
    this.quill = quill;
    this.config = Object.assign(
      {
        onInsertImage: () => {},
        onInsertRichText: () => {},
        onInsertText: () => {},
        onInsertLink: () => {}
      },
      config
    );

    quill.root.addEventListener('paste', this.pasteHandler.bind(this), false);
  }

  /**
   * 右键黏贴内容
   */
  pasteHandler() {
    const matchers = this.quill && this.quill.clipboard.matchers;
    const pasteMatcher = [Node.ELEMENT_NODE, this.nodeHandler.bind(this)];
    matchers.push(pasteMatcher);
  }

  /**
   * 针对黏贴内容进行处理
   * 可以控制内容格式、或禁止内容输入
   * https://quilljs.com/docs/modules/clipboard/
   */
  nodeHandler(node, delta) {
    // eslint-disable-next-line no-param-reassign
    delta.ops = delta.ops
      .map(op => {
        if (typeof op.insert === 'string') {
          if (op.attributes && op.attributes.link) {
            return this.config.onInsertLink(op);
          }
          return this.config.onInsertText(op);
        }
        if (op.insert && op.insert.image) {
          return this.config.onInsertImage(op);
        }
        return this.config.onInsertRichText(op);
      })
      .filter(op => typeof op === 'object');
    return delta;
  }
}

<template>
  <div>
    <Quill-editor
      ref="myQuillEditor"
      :value="value"
      :options="editorOption"
      @input="handleInput"
    />
  </div>
</template>
<script>
import QuillEditor, { Quill } from './vue-quill-editor';
import { PasteFormat } from './utils/PasteFormat';
Quill.register('modules/PasteFormat', PasteFormat); // 注册到modules/PasteFormat,之后传参名也需要与PasteFormat一致
export default {
  components: {
    QuillEditor
  },
  props: {
    value: {
      type: String,
      default: ''
    },
    width: {
      type: [String, Number],
      default: '100%'
    },
    height: {
      type: [String, Number],
      default: '300px'
    }
  },
  data() {
    return {
      editorOption: {
        placeholder: '输入文本...',
        theme: 'snow', // 主题
        modules: {
        // 传参进自定义module
          PasteFormat: {
            onInsertImage: () => undefined,
            onInsertRichText: () => undefined,
            onInsertText: op => ({
              insert: op.insert
            }),
            onInsertLink: op => ({
              insert: op.insert
            })
          },
          toolbar: {
            container: [
              ['bold', 'italic', 'underline', 'strike'], // 加粗,斜体,下划线,删除线
              ['blockquote', 'code-block'], // 引用,代码块
              [{ header: 1 }, { header: 2 }], // 标题,键值对的形式;1、2表示字体大小
              [{ list: 'ordered' }, { list: 'bullet' }], // 列表
              [{ script: 'sub' }, { script: 'super' }], // 上下标
              [{ indent: '-1' }, { indent: '+1' }], // 缩进
              [{ direction: 'rtl' }], // 文本方向
              [{ header: [1, 2, 3, 4, 5, 6, false] }], // 几级标题
              [{ color: [] }, { background: [] }], // 字体颜色,字体背景颜色
              [{ align: [] }], // 对齐方式
              ['clean'], // 清除字体样式
              ['image', 'video'] // 上传图片、上传视频
            ]
          }
        }
      }
    };
  },
  computed: {
    editor() {
      return this.$refs.myQuillEditor.quill;
    }
  },
  mounted() {
    this.editor.container.style.width = this.width;
    this.editor.container.style.height = this.height;
  },
  methods: {
    handleInput(val) {
      this.$emit('input', val);
    }
  }
};
</script>

例子2: 对字体大小的修改

<template>
  <div>
    <Quill-editor
      ref="myQuillEditor"
      :value="value"
      :options="editorOption"
      @input="handleInput"
    />
  </div>
</template>
<script>
import QuillEditor, { Quill } from './vue-quill-editor';
const SizeStyle = Quill.import('attributors/style/size');
SizeStyle.whitelist = [ //和下面配置对应
  '10px',
  '12px',
  '14px',
  '16px',
  '18px',
  '20px',
  '24px',
  false
];
Quill.register(SizeStyle, true);

export default {
  components: {
    QuillEditor
  },
  props: {
    value: {
      type: String,
      default: ''
    },
    width: {
      type: [String, Number],
      default: '100%'
    },
    height: {
      type: [String, Number],
      default: '300px'
    }
  },
  data() {
    return {
      editorOption: {
        placeholder: '输入文本...',
        theme: 'snow', // 主题
        modules: {
          toolbar: {
            container: [
              ['bold', 'italic', 'underline', 'strike'], // 加粗,斜体,下划线,删除线
              ['blockquote', 'code-block'], // 引用,代码块
              [{ header: 1 }, { header: 2 }], // 标题,键值对的形式;1、2表示字体大小
              [{ list: 'ordered' }, { list: 'bullet' }], // 列表
              [{ script: 'sub' }, { script: 'super' }], // 上下标
              [{ indent: '-1' }, { indent: '+1' }], // 缩进
              [{ direction: 'rtl' }], // 文本方向
              [
                {
                  size: [
                    '10px',
                    '12px',
                    '14px',
                    '16px',
                    '18px',
                    '20px',
                    '24px',
                    false
                  ]
                }
              ], // 字体大小,false设置自定义大小
              [{ header: [1, 2, 3, 4, 5, 6, false] }], // 几级标题
              [{ color: [] }, { background: [] }], // 字体颜色,字体背景颜色
              [{ align: [] }], // 对齐方式
              ['clean'], // 清除字体样式
              ['image', 'video'] // 上传图片、上传视频
            ]
          }
        }
      }
    };
  },
  computed: {
    editor() {
      return this.$refs.myQuillEditor.quill;
    }
  },
  mounted() {
    this.editor.container.style.width = this.width;
    this.editor.container.style.height = this.height;
  },
  methods: {
    handleInput(val) {
      this.$emit('input', val);
    }
  }
};
</script>
<style>
@import url(./utils/font.css);
</style>

.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="10px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="10px"]::before {
  content: '10px';
  font-size: 10px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="12px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="12px"]::before {
content: '12px';
font-size: 12px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="14px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="14px"]::before {
content: '14px';
font-size: 14px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="16px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="16px"]::before {
content: '16px';
font-size: 16px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="18px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="18px"]::before {
content: '18px';
font-size: 18px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="20px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="20px"]::before {
content: '20px';
font-size: 20px;
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value="24px"]::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value="24px"]::before {
content: '24px';
font-size: 24px;
}