<template>
<div class="el-transfer">
<transfer-panel
// 将父组件的props一块儿传给自组件
v-bind="$props"
// 组件注册引用
ref="leftPanel"
// 须要展现的数据
:data="sourceData"
// 标题
:title="titles[0] || t('el.transfer.titles.0')"
// 默认勾选项
:default-checked="leftDefaultChecked"
// 搜索栏默认的占位符
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
// 勾选项发生变化的回调
@checked-change="onSourceCheckedChange">
// 底部插槽
<slot name="left-footer"></slot>
</transfer-panel>
<div class="el-transfer__buttons">
<el-button
// 按钮类型
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
// 点击回调
@click.native="addToLeft"
// 动态绑定disable状态
:disabled="rightChecked.length === 0">
<i class="el-icon-arrow-left"></i>
// 按钮名称
<span v-if="buttonTexts[0] !== undefined">{{ buttonTexts[0] }}</span>
</el-button>
<el-button
type="primary"
:class="['el-transfer__button', hasButtonTexts ? 'is-with-texts' : '']"
@click.native="addToRight"
:disabled="leftChecked.length === 0">
<span v-if="buttonTexts[1] !== undefined">{{ buttonTexts[1] }}</span>
<i class="el-icon-arrow-right"></i>
</el-button>
</div>
<transfer-panel
v-bind="$props"
ref="rightPanel"
:data="targetData"
:title="titles[1] || t('el.transfer.titles.1')"
:default-checked="rightDefaultChecked"
:placeholder="filterPlaceholder || t('el.transfer.filterPlaceholder')"
@checked-change="onTargetCheckedChange">
<slot name="right-footer"></slot>
</transfer-panel>
</div>
</template>
复制代码
总体上,能够划分为左中右三块,左右两个 TransferPanel 组件承载数据展现。中间两个 ElButton 是左右移动的操做按钮。结构清晰。数组
mixins: [Emitter, Locale, Migrating],
复制代码
mixins 部分混入了三个对象。Locale 是国际化的东西,Migrating 是组件迁移的一些提示信息。须要关注的是 Emitter 部分,代码以下:bash
// 寻找全部子组件,直到找到名为componentName的组件,调用其$emit方法
function broadcast(componentName, eventName, params) {
this.$children.forEach(child => {
var name = child.$options.componentName;
if (name === componentName) {
child.$emit.apply(child, [eventName].concat(params));
} else {
broadcast.apply(child, [componentName, eventName].concat([params]));
}
});
}
export default {
// 事件定向传播
methods: {
// 寻找全部父组件,直到找到名为componentName的组件,调用其$emit方法
dispatch(componentName, eventName, params) {
var parent = this.$parent || this.$root;
var name = parent.$options.componentName;
while (parent && (!name || name !== componentName)) {
parent = parent.$parent;
if (parent) {
name = parent.$options.componentName;
}
}
if (parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
broadcast(componentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
};
复制代码
提供了两个方法: dispatch, broadcast 作事件的定向传播。app
// array[{ key, label, disabled }]
data: {
type: Array,
default() {
return [];
}
}
复制代码
传入的数组,每一项须要有三个属性,key : 惟一标识,label : 展现内容,disabled : 是否可勾选,若是不想用这三个属性名,能够经过 props 属性设置别名。函数
props: {
type: Object,
default() {
return {
label: 'label',
key: 'key',
disabled: 'disabled'
};
}
}
复制代码
// ['列表 1', '列表 2']
titles: {
type: Array,
default() {
return [];
}
}
复制代码
// ['到左边', '到右边']
buttonTexts: {
type: Array,
default() {
return [];
}
}
复制代码
filterPlaceholder: {
type: String,
default: ''
}
复制代码
filterMethod: Function
复制代码
leftDefaultChecked: {
type: Array,
default() {
return [];
}
}
复制代码
renderContent: Function,
复制代码
value: {
type: Array,
default() {
return [];
}
}
复制代码
// object{noChecked, hasChecked}
format: {
type: Object,
default() {
return {};
}
}
复制代码
filterable: Boolean
复制代码
targetOrder: {
type: String,
default: 'original'
}
复制代码
// [{key:1,label:'数据1',disabled:false}] => {1:{key:1,label:'数据1',disabled:false}
dataObj() {
const key = this.props.key;
return this.data.reduce((o, cur) => (o[cur[key]] = cur) && o, {});
}
复制代码
// 筛选在 data 中 ,可是不在 value 中的数据
sourceData() {
return this.data.filter(item => this.value.indexOf(item[this.props.key]) === -1);
}
复制代码
targetData() {
// 目标源排序顺序为 original,按照数据在 data 数组的前后顺序
if (this.targetOrder === 'original') {
return this.data.filter(item => this.value.indexOf(item[this.props.key]) > -1);
// 不然按照 value 数组中 key 的前后顺序
} else {
return this.value.reduce((arr, cur) => {
const val = this.dataObj[cur];
if (val) {
arr.push(val);
}
return arr;
}, []);
}
}
复制代码
// 当传入的 button-text 有两项的时候返回 true
hasButtonTexts() {
return this.buttonTexts.length === 2;
}
复制代码
methods 中 leftPanel 和 rightPanel 的部分是对称的,因此只选取 rightPanel 部分展现:ui
// val : 当前选中项的 key 数组
// movedKeys: 选中状态发生变化的 key 数组
onTargetCheckedChange(val, movedKeys) {
this.rightChecked = val;
if (movedKeys === undefined) return;
this.$emit('right-check-change', val, movedKeys);
},
addToLeft() {
// rightPanel 中数据项的 key 数组
let currentValue = this.value.slice();
// 从 currentValue 中删除选中的项
this.rightChecked.forEach(item => {
const index = currentValue.indexOf(item);
if (index > -1) {
currentValue.splice(index, 1);
}
});
// currentValue: 当前 rightPanel 中存在数据的 key 数组
// rightChecked: 选中移动的数据项的 key 数组
this.$emit('input', currentValue);
this.$emit('change', currentValue, 'left', this.rightChecked);
},
clearQuery(which) {
// 清除 leftPanel 搜索栏的搜索条件
if (which === 'left') {
this.$refs.leftPanel.query = '';
// 清除 rightPanel 搜索栏的搜索条件
} else if (which === 'right') {
this.$refs.rightPanel.query = '';
}
}
复制代码
Transfer 组件部分就这些内容,主要是控制传入 TransferPanel 的 data ,以及向外发射 change ,check-change 事件。this
<template>
<div class="el-transfer-panel">
<p class="el-transfer-panel__header">
// 全选框
<el-checkbox
// 绑定值初始为 false
v-model="allChecked"
// 勾选回调
@change="handleAllCheckedChange"
// 设置 indeterminate 状态,只负责样式控制
:indeterminate="isIndeterminate">
// 展现文本
{{ title }}
// 勾选总结文本
<span>{{ checkedSummary }}</span>
</el-checkbox>
</p>
<div :class="['el-transfer-panel__body', hasFooter ? 'is-with-footer' : '']">
// 搜索栏
<el-input
class="el-transfer-panel__filter"
// 绑定值,默认为‘’
v-model="query"
// 尺寸
size="small"
// 占位符
:placeholder="placeholder"
// mouseenter 事件
@mouseenter.native="inputHover = true"
// mouseleave 事件
@mouseleave.native="inputHover = false"
// 设置 filterable 才展现
v-if="filterable">
// prefix 插槽, 点击清除搜索栏条件
<i slot="prefix"
:class="['el-input__icon', 'el-icon-' + inputIcon]"
@click="clearQuery"
></i>
</el-input>
// 多选框组
<el-checkbox-group
// 绑定值,默认为 []
v-model="checked"
// 当有匹配数据而且数据源有内容的时候展现
v-show="!hasNoMatch && data.length > 0"
// 根据 filterable 动态绑定 class
:class="{ 'is-filterable': filterable }"
class="el-transfer-panel__list">
// v-for 列表渲染,数据源为 filteredData
<el-checkbox
class="el-transfer-panel__item"
// 选中状态的值
:label="item[keyProp]"
// 是否禁用
:disabled="item[disabledProp]"
:key="item[keyProp]"
v-for="item in filteredData">
// option-content 组件
<option-content :option="item"></option-content>
</el-checkbox>
</el-checkbox-group>
// 没有匹配数据时的展现内容
<p
class="el-transfer-panel__empty"
v-show="hasNoMatch">{{ t('el.transfer.noMatch') }}</p>
// 有匹配项而且数据项为空时展现内容
<p
class="el-transfer-panel__empty"
v-show="data.length === 0 && !hasNoMatch">{{ t('el.transfer.noData') }}</p>
</div>
// 底部插槽,当设置footer时展现
<p class="el-transfer-panel__footer" v-if="hasFooter">
<slot></slot>
</p>
</div>
</template>
复制代码
引入的组件中,须要关注下 option-content ,它是 render 函数直接定义的spa
OptionContent: {
props: {
option: Object
},
render(h) {
// 获取名为 ElTransferPanel 的父组件
const getParent = vm => {
if (vm.$options.componentName === 'ElTransferPanel') {
return vm;
} else if (vm.$parent) {
return getParent(vm.$parent);
} else {
return vm;
}
};
const panel = getParent(this);
// 获取 transfer 组件
const transfer = panel.$parent || panel;
// 若是设置了自定义数据项渲染函数,则调用自定义的渲染函数
// 若是没有定义 render-content 方法,则检查 Transfer 组件是否设置了 slot-scope 插槽内容
// 若是设置了,则用 slot-scope 内容渲染
// 不然用默认的 span 标签渲染
// 意味着数据项的渲染能够经过 render-content 或者 slot-scoped 自定义
return panel.renderContent
? panel.renderContent(h, this.option)
: transfer.$scopedSlots.default
? transfer.$scopedSlots.default({ option: this.option })
: <span>{ this.option[panel.labelProp] || this.option[panel.keyProp] }</span>;
}
}
复制代码
组件传入的 option 是 item ,item 来自 filteredData,3d
filteredData() {
// data 为 数据源, leftPanel 对应 sourceData
return this.data.filter(item => {
// 若是自定义了搜索方法,则调用自定义的方法
if (typeof this.filterMethod === 'function') {
return this.filterMethod(this.query, item);
// 默认搜索规则是数据项的 label 中是否包含输入的条件
} else {
const label = item[this.labelProp] || item[this.keyProp].toString();
return label.toLowerCase().indexOf(this.query.toLowerCase()) > -1;
}
});
}
复制代码
// 选择项发生变化
// val 当前选中的元素的 key 数组
// oldVal 前一状态选中的元素的 key 数组
checked(val, oldVal) {
// 更新全新状态
this.updateAllChecked();
// 若是改变是用户点击形成的
if (this.checkChangeByUser) {
// 选中状态发生变化的元素的 key 数组
const movedKeys = val.concat(oldVal)
.filter(v => val.indexOf(v) === -1 || oldVal.indexOf(v) === -1);
this.$emit('checked-change', val, movedKeys);
} else {
this.$emit('checked-change', val);
this.checkChangeByUser = true;
}
},
// 数据源发生变化
data() {
const checked = [];
const filteredDataKeys = this.filteredData.map(item => item[this.keyProp]);
this.checked.forEach(item => {
if (filteredDataKeys.indexOf(item) > -1) {
checked.push(item);
}
});
// 标记这次勾选状态改变不是由用户形成的
this.checkChangeByUser = false;
// 从新设置勾选的元素项
this.checked = checked;
},
// 可勾选的数据改变
checkableData() {
this.updateAllChecked();
},
// 默认选中的数据改变
defaultChecked: {
// 设置该回调将会在侦听开始以后被当即调用
immediate: true,
handler(val, oldVal) {
// 存在旧数据,且旧数据和当前数据包含项一致,返回,不进行后续赋值操做
if (oldVal && val.length === oldVal.length &&
val.every(item => oldVal.indexOf(item) > -1)) return;
const checked = [];
const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
val.forEach(item => {
if (checkableDataKeys.indexOf(item) > -1) {
checked.push(item);
}
});
this.checkChangeByUser = false;
this.checked = checked;
}
}
复制代码
Transferpanel 组件的 computed 比较简单,主要的 filteredData 在上面已经提过,下面看他的 methodscode
// 更新全选状态
updateAllChecked() {
// 全部可勾选数据项的 key 数组
const checkableDataKeys = this.checkableData.map(item => item[this.keyProp]);
// 全部可勾选的项都在已勾选数组中,则标记为全勾选状态
this.allChecked = checkableDataKeys.length > 0 &&
checkableDataKeys.every(item => this.checked.indexOf(item) > -1);
},
// 勾选全选框的回调
handleAllCheckedChange(value) {
// 若是是选中,则将全部可勾选数据项的 key 放入 checked 数组
// 若是是取消选中,则清空 checked 数组
this.checked = value
? this.checkableData.map(item => item[this.keyProp])
: [];
},
// 清空搜索栏
clearQuery() {
// 若是搜索栏输入了内容
if (this.inputIcon === 'circle-close') {
this.query = '';
}
}
复制代码