Vue如果和Quill不兼容该怎么办?

828 阅读4分钟

前言

今天在review之前的代码,发现了之前使用Quill的时候,遇到了Vue和Quill不兼容的地方,后面解决了。但是没有记录下来,今天把它记录一下,方便以后查阅。

问题出现

Quill的工具栏模块toolbar,我们是自定义文案,所以通过dom实现,把对应的dom的id传给toolbar的container。

  <div id="toolbar">
    <span class="ql-formats">
      <select class="ql-header" v-model="headerVal">
        <option
          v-for="item in headerArr"
          :key="item.label"
          :value="item.value"
        >{{item.label}}</option>
      </select>
    </span>
  </div>  
  
// js相关代码:
data () {
  return {
     editOptions: {
       modules: {
         toolbar: {
           container: '#toolbar'
         }
       }  
     },       
     headerVal: '',
     headerArr: [
          {
            label: '正文',
            value: ''
          }, {
            label: '一级标题',
            value: '1'
          }, {
            label: '二级标题',
            value: '2'
          }, {
            label: '三级标题',
            value: '3'
          }
       ]
  }
}       

我们想默认选中正文,通过设置headerVal去设置选中。乍一看没有问题。也选中了。

image.png

然后选择里面的一级标题,也是选中了。

image.png

目前是看没有问题的。

但是Quill它本身有一个逻辑,就是当你的光标聚焦在标题下,toolbar的标题那项会变成对应的标题那项。

如果你聚焦在普通文本下,toolbar的标题那项会变成正文那项并选中。

但是现在这样写后,如果上次是聚焦在标题下,然后现在聚焦在普通文本下,toolbar的标题并不会重置成正文

到底是什么影响了?

查找问题

思索无果,就去查看Quill的源码,看看它原本是怎么实现了。

摘取部分相关代码:

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;
    }
 }

可以看到它会找当前设置了selected属性的option,如果有就设置选中,没有就不选中。

然后我们去看我们渲染后的dom,看看有没有selected属性,才发现,没有selected属性。

image.png

如果没有,那就得手动设置上才行。

于是三下五除二,就改好了。

  <select class="ql-header">
    <option
      v-for="item in headerArr"
      :key="item.label"
      :value="item.value"
      :selected="item.selected"
    >{{item.label}}</option>
  </select>
  // headerArr格式
   headerArr: [
      {
        label: '正文',
        value: '',
        selected: true
      }, {
        label: '一级标题',
        value: '1'
      }, {
        label: '二级标题',
        value: '2'
      }, {
        label: '三级标题',
        value: '3'
      }
   ]         

正常来说,这样手动设置应该可以设置上了吧。

但是出乎意料的是,没有设置上。还是没有selected属性。

难不成是vue过滤了selected属性?

不会主动设置selected属性?

带着这样的疑惑,去查找下资料,才发现,真的是vue的问题。

链接在这里

image.png

官方回复说selected是当作元素的prop,而不是元素的属性。所以说设置selected属性是没有意义的。

摘自vue 2.6.14版本部分源码:

  // attributes that should be using props for binding
  var acceptValue = makeMap('input,textarea,option,select,progress');
  var mustUseProp = function (tag, type, attr) {
    return (
      (attr === 'value' && acceptValue(tag)) && type !== 'button' ||
      (attr === 'selected' && tag === 'option') ||
      (attr === 'checked' && tag === 'input') ||
      (attr === 'muted' && tag === 'video')
    )
  };

从源码里面可以看到,vue2遇到attrselected并且tagselect,会把它当作prop,而不是attribute

摘自 vue 3.2.21版本部分源码:

  function shouldSetAsProp(el, key, value, isSVG) {
       // 省略部分代码
      if (key === 'form') {
          return false;
      }
      // #1526 <input list> must be set as attribute
      if (key === 'list' && el.tagName === 'INPUT') {
          return false;
      }
      // #2766 <textarea type> must be set as attribute
      if (key === 'type' && el.tagName === 'TEXTAREA') {
          return false;
      }
      // native onclick with string value, must be set as attribute
      if (nativeOnRE.test(key) && isString(value)) {
          return false;
      }
      return key in el;
  }

从源码里面可以看到,vue3会判断key是不是in el,selected是属于el的一部分,所以会当作props, 而不是attribute

image.png

另外,vue2和vue3只会设置option数组的selected值和select的selectedIndex值,不会设置dom的selected属性。

问题解决

那要怎么解决呢?这时候可以使用指令解决。

通过指令插入dom的时候,把它的selected属性属性设置上。这样应该就能解决了,真是一波三折。

先注册指令:

import Vue from 'vue'
// 属性指令 v-attr:selected="true" v-attr:selected="false"
Vue.directive('attr', function (el, binding, vnode) {
  let value = binding.value
  // true使用空字符串代替
  if (value === true) value = ''
  if (value === '' || value) {
    el.setAttribute(binding.arg, value)
  }
})

接着去使用指令

<select class="ql-header">
    <option
      v-for="item in headerArr"
      :key="item.label"
      :value="item.value"
      v-attr:selected="item.selected"
    >{{item.label}}</option>
</select>

这样修改后鼠标聚焦到不同的文本会展示对应的标题或者正文,算是解决问题了。

总结

Vue如果和Quill不兼容该怎么办,那么就要看看是哪边出现的问题,再去找对应的方法,如果没有,就可以看看有没有折中方法。

方法都是人想出来的,加油!