前言
今天在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去设置选中。乍一看没有问题。也选中了。
然后选择里面的一级标题,也是选中了。
目前是看没有问题的。
但是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属性。
如果没有,那就得手动设置上才行。
于是三下五除二,就改好了。
<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的问题。
链接在这里
官方回复说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遇到attr是selected并且tag是select,会把它当作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。
另外,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不兼容该怎么办,那么就要看看是哪边出现的问题,再去找对应的方法,如果没有,就可以看看有没有折中方法。
方法都是人想出来的,加油!