update the master branch to the latest

This commit is contained in:
梁灏 2019-08-27 09:42:40 +08:00
parent 67d534df27
commit 23a0ba9831
611 changed files with 122648 additions and 0 deletions

View file

@ -0,0 +1,118 @@
<template>
<div class="ivu-select-dropdown" :class="className" :style="styles"><slot></slot></div>
</template>
<script>
import Vue from 'vue';
const isServer = Vue.prototype.$isServer;
import { getStyle } from '../../utils/assist';
const Popper = isServer ? function() {} : require('popper.js/dist/umd/popper.js'); // eslint-disable-line
import { transferIndex, transferIncrease } from '../../utils/transfer-queue';
export default {
name: 'Drop',
props: {
placement: {
type: String,
default: 'bottom-start'
},
className: {
type: String
},
transfer: {
type: Boolean
}
},
data () {
return {
popper: null,
width: '',
popperStatus: false,
tIndex: this.handleGetIndex()
};
},
computed: {
styles () {
let style = {};
if (this.width) style.minWidth = `${this.width}px`;
if (this.transfer) style['z-index'] = 1060 + this.tIndex;
return style;
}
},
methods: {
update () {
if (isServer) return;
if (this.popper) {
this.$nextTick(() => {
this.popper.update();
this.popperStatus = true;
});
} else {
this.$nextTick(() => {
this.popper = new Popper(this.$parent.$refs.reference, this.$el, {
placement: this.placement,
modifiers: {
computeStyle:{
gpuAcceleration: false
},
preventOverflow :{
boundariesElement: 'window'
}
},
onCreate:()=>{
this.resetTransformOrigin();
this.$nextTick(this.popper.update());
},
onUpdate:()=>{
this.resetTransformOrigin();
}
});
});
}
// set a height for parent is Modal and Select's width is 100%
if (this.$parent.$options.name === 'iSelect') {
this.width = parseInt(getStyle(this.$parent.$el, 'width'));
}
this.tIndex = this.handleGetIndex();
},
destroy () {
if (this.popper) {
setTimeout(() => {
if (this.popper && !this.popperStatus) {
this.popper.destroy();
this.popper = null;
}
this.popperStatus = false;
}, 300);
}
},
resetTransformOrigin() {
// Select
if (!this.popper) return;
let x_placement = this.popper.popper.getAttribute('x-placement');
let placementStart = x_placement.split('-')[0];
let placementEnd = x_placement.split('-')[1];
const leftOrRight = x_placement === 'left' || x_placement === 'right';
if(!leftOrRight){
this.popper.popper.style.transformOrigin = placementStart==='bottom' || ( placementStart !== 'top' && placementEnd === 'start') ? 'center top' : 'center bottom';
}
},
handleGetIndex () {
transferIncrease();
return transferIndex;
},
},
created () {
this.$on('on-update-popper', this.update);
this.$on('on-destroy-popper', this.destroy);
},
beforeDestroy () {
if (this.popper) {
this.popper.destroy();
}
}
};
</script>

View file

@ -0,0 +1,29 @@
<script>
const returnArrayFn = () => [];
export default {
props: {
options: {
type: Array,
default: returnArrayFn
},
slotOptions: {
type: Array,
default: returnArrayFn
},
slotUpdateHook: {
type: Function,
default: () => {}
},
},
functional: true,
render(h, {props, parent}){
// to detect changes in the $slot children/options we do this hack
// so we can trigger the parents computed properties and have everything reactive
// although $slot.default is not
if (props.slotOptions !== parent.$slots.default) props.slotUpdateHook();
return props.options;
}
};
</script>

View file

@ -0,0 +1,7 @@
import Select from './select.vue';
import Option from './option.vue';
import OptionGroup from './option-group.vue';
export { Select, Option, OptionGroup };
export default Select;

View file

@ -0,0 +1,48 @@
<template>
<li :class="[prefixCls + '-wrap']" v-show="!hidden">
<div :class="[prefixCls + '-title']">{{ label }}</div>
<ul>
<li :class="[prefixCls]" ref="options"><slot></slot></li>
</ul>
</li>
</template>
<script>
const prefixCls = 'ivu-select-group';
export default {
name: 'OptionGroup',
props: {
label: {
type: String,
default: ''
}
},
data () {
return {
prefixCls: prefixCls,
hidden: false // for search
};
},
methods: {
queryChange () {
this.$nextTick(() => {
const options = this.$refs.options.querySelectorAll('.ivu-select-item');
let hasVisibleOption = false;
for (let i = 0; i < options.length; i++) {
if (options[i].style.display !== 'none') {
hasVisibleOption = true;
break;
}
}
this.hidden = !hasVisibleOption;
});
}
},
mounted () {
this.$on('on-query-change', () => {
this.queryChange();
return true;
});
}
};
</script>

View file

@ -0,0 +1,82 @@
<template>
<li
:class="classes"
@click.stop="select"
@mousedown.prevent
><slot>{{ showLabel }}</slot></li>
</template>
<script>
import Emitter from '../../mixins/emitter';
import { findComponentUpward } from '../../utils/assist';
const prefixCls = 'ivu-select-item';
export default {
name: 'iOption',
componentName: 'select-item',
mixins: [ Emitter ],
props: {
value: {
type: [String, Number],
required: true
},
label: {
type: [String, Number]
},
disabled: {
type: Boolean,
default: false
},
selected: {
type: Boolean,
default: false
},
isFocused: {
type: Boolean,
default: false
}
},
data () {
return {
searchLabel: '', // the slot value (textContent)
autoComplete: false
};
},
computed: {
classes () {
return [
`${prefixCls}`,
{
[`${prefixCls}-disabled`]: this.disabled,
[`${prefixCls}-selected`]: this.selected && !this.autoComplete,
[`${prefixCls}-focus`]: this.isFocused
}
];
},
showLabel () {
return (this.label) ? this.label : this.value;
},
optionLabel(){
return this.label || (this.$el && this.$el.textContent);
}
},
methods: {
select () {
if (this.disabled) return false;
this.dispatch('iSelect', 'on-select-selected', {
value: this.value,
label: this.optionLabel,
});
this.$emit('on-select-selected', {
value: this.value,
label: this.optionLabel,
});
},
},
mounted () {
const Select = findComponentUpward(this, 'iSelect');
if (Select) this.autoComplete = Select.autoComplete;
},
};
</script>

View file

@ -0,0 +1,271 @@
<template>
<div @click="onHeaderClick" :class="headCls">
<span :class="[prefixCls + '-prefix']" v-if="$slots.prefix || prefix">
<slot name="prefix">
<Icon :type="prefix" v-if="prefix" />
</slot>
</span>
<div
class="ivu-tag ivu-tag-checked"
v-for="(item, index) in selectedMultiple"
v-if="maxTagCount === undefined || index < maxTagCount">
<span class="ivu-tag-text">{{ item.label }}</span>
<Icon type="ios-close" @click.native.stop="removeTag(item)"></Icon>
</div><div class="ivu-tag ivu-tag-checked" v-if="maxTagCount !== undefined && selectedMultiple.length > maxTagCount">
<span class="ivu-tag-text ivu-select-max-tag">
<template v-if="maxTagPlaceholder">{{ maxTagPlaceholder(selectedMultiple.length - maxTagCount) }}</template>
<template v-else>+ {{ selectedMultiple.length - maxTagCount }}...</template>
</span>
</div>
<span
:class="singleDisplayClasses"
v-show="singleDisplayValue"
>{{ singleDisplayValue }}</span>
<input
:id="inputElementId"
type="text"
v-if="filterable"
v-model="query"
:disabled="disabled"
:class="[prefixCls + '-input']"
:placeholder="showPlaceholder ? localePlaceholder : ''"
:style="inputStyle"
autocomplete="off"
spellcheck="false"
@keydown="resetInputState"
@keydown.delete="handleInputDelete"
@focus="onInputFocus"
@blur="onInputBlur"
ref="input">
<Icon type="ios-close-circle" :class="[prefixCls + '-arrow']" v-if="resetSelect" @click.native.stop="onClear"></Icon>
<Icon :type="arrowType" :custom="customArrowType" :size="arrowSize" :class="[prefixCls + '-arrow']" v-if="!resetSelect && !remote"></Icon>
</div>
</template>
<script>
import Icon from '../icon';
import Emitter from '../../mixins/emitter';
import Locale from '../../mixins/locale';
const prefixCls = 'ivu-select';
export default {
name: 'iSelectHead',
mixins: [ Emitter, Locale ],
components: { Icon },
props: {
disabled: {
type: Boolean,
default: false
},
filterable: {
type: Boolean,
default: false
},
multiple: {
type: Boolean,
default: false
},
remote: {
type: Boolean,
default: false
},
initialLabel: {
type: [String, Number, Array],
},
values: {
type: Array,
default: () => []
},
clearable: {
type: [Function, Boolean],
default: false,
},
inputElementId: {
type: String
},
placeholder: {
type: String
},
queryProp: {
type: String,
default: ''
},
prefix: {
type: String
},
// 3.4.0
maxTagCount: {
type: Number
},
// 3.4.0
maxTagPlaceholder: {
type: Function
}
},
data () {
return {
prefixCls: prefixCls,
query: '',
inputLength: 20,
remoteInitialLabel: this.initialLabel,
preventRemoteCall: false,
};
},
computed: {
singleDisplayClasses(){
const {filterable, multiple, showPlaceholder} = this;
return [{
[prefixCls + '-head-with-prefix']: this.$slots.prefix || this.prefix,
[prefixCls + '-placeholder']: showPlaceholder && !filterable,
[prefixCls + '-selected-value']: !showPlaceholder && !multiple && !filterable,
}];
},
singleDisplayValue(){
if ((this.multiple && this.values.length > 0) || this.filterable) return '';
return `${this.selectedSingle}` || this.localePlaceholder;
},
showPlaceholder () {
let status = false;
if (!this.multiple) {
const value = this.values[0];
if (typeof value === 'undefined' || String(value).trim() === ''){
status = !this.remoteInitialLabel;
}
} else {
if (!this.values.length > 0) {
status = true;
}
}
return status;
},
resetSelect(){
return !this.showPlaceholder && this.clearable;
},
inputStyle () {
let style = {};
if (this.multiple) {
if (this.showPlaceholder) {
style.width = '100%';
} else {
style.width = `${this.inputLength}px`;
}
}
return style;
},
localePlaceholder () {
if (this.placeholder === undefined) {
return this.t('i.select.placeholder');
} else {
return this.placeholder;
}
},
selectedSingle(){
const selected = this.values[0];
return selected ? selected.label : (this.remoteInitialLabel || '');
},
selectedMultiple(){
return this.multiple ? this.values : [];
},
// 使 prefix filterable
headCls () {
return {
[`${prefixCls}-head-flex`]: this.filterable && (this.$slots.prefix || this.prefix)
};
},
// 3.4.0, global setting customArrow arrow
arrowType () {
let type = 'ios-arrow-down';
if (this.$IVIEW) {
if (this.$IVIEW.select.customArrow) {
type = '';
} else if (this.$IVIEW.select.arrow) {
type = this.$IVIEW.select.arrow;
}
}
return type;
},
// 3.4.0, global setting
customArrowType () {
let type = '';
if (this.$IVIEW) {
if (this.$IVIEW.select.customArrow) {
type = this.$IVIEW.select.customArrow;
}
}
return type;
},
// 3.4.0, global setting
arrowSize () {
let size = '';
if (this.$IVIEW) {
if (this.$IVIEW.select.arrowSize) {
size = this.$IVIEW.select.arrowSize;
}
}
return size;
}
},
methods: {
onInputFocus(){
this.$emit('on-input-focus');
},
onInputBlur () {
if (!this.values.length) this.query = ''; // #5155
this.$emit('on-input-blur');
},
removeTag (value) {
if (this.disabled) return false;
this.dispatch('iSelect', 'on-select-selected', value);
},
resetInputState () {
this.inputLength = this.$refs.input.value.length * 12 + 20;
this.$emit('on-keydown');
},
handleInputDelete () {
if (this.multiple && this.selectedMultiple.length && this.query === '') {
this.removeTag(this.selectedMultiple[this.selectedMultiple.length - 1]);
}
},
onHeaderClick(e){
if (this.filterable && e.target === this.$el){
this.$refs.input.focus();
}
},
onClear(){
this.$emit('on-clear');
}
},
watch: {
values ([value]) {
if (!this.filterable) return;
this.preventRemoteCall = true;
if (this.multiple){
this.query = '';
this.preventRemoteCall = false; // this should be after the query change setter above
return;
}
// #982
if (typeof value === 'undefined' || value === '' || value === null) this.query = '';
else this.query = value.label;
this.$nextTick(() => this.preventRemoteCall = false); // this should be after the query change setter above
},
query (val) {
if (this.preventRemoteCall) {
this.preventRemoteCall = false;
return;
}
this.$emit('on-query-change', val);
},
queryProp(query){
if (query !== this.query) this.query = query;
},
}
};
</script>

View file

@ -0,0 +1,824 @@
<template>
<div
:class="classes"
v-click-outside.capture="onClickOutside"
v-click-outside:mousedown.capture="onClickOutside"
v-click-outside:touchstart.capture="onClickOutside"
>
<div
ref="reference"
:class="selectionCls"
:tabindex="selectTabindex"
@blur="toggleHeaderFocus"
@focus="toggleHeaderFocus"
@click="toggleMenu"
@keydown.esc="handleKeydown"
@keydown.enter="handleKeydown"
@keydown.up.prevent="handleKeydown"
@keydown.down.prevent="handleKeydown"
@keydown.tab="handleKeydown"
@keydown.delete="handleKeydown"
@mouseenter="hasMouseHoverHead = true"
@mouseleave="hasMouseHoverHead = false"
>
<slot name="input">
<input type="hidden" :name="name" :value="publicValue">
<select-head
:filterable="filterable"
:multiple="multiple"
:values="values"
:clearable="canBeCleared"
:prefix="prefix"
:disabled="disabled"
:remote="remote"
:input-element-id="elementId"
:initial-label="initialLabel"
:placeholder="placeholder"
:query-prop="query"
:max-tag-count="maxTagCount"
:max-tag-placeholder="maxTagPlaceholder"
@on-query-change="onQueryChange"
@on-input-focus="isFocused = true"
@on-input-blur="isFocused = false"
@on-clear="clearSingleSelect"
>
<slot name="prefix" slot="prefix"></slot>
</select-head>
</slot>
</div>
<transition name="transition-drop">
<Drop
:class="dropdownCls"
v-show="dropVisible"
:placement="placement"
ref="dropdown"
:data-transfer="transfer"
:transfer="transfer"
v-transfer-dom
>
<ul v-show="showNotFoundLabel" :class="[prefixCls + '-not-found']"><li>{{ localeNotFoundText }}</li></ul>
<ul :class="prefixCls + '-dropdown-list'">
<functional-options
v-if="(!remote) || (remote && !loading)"
:options="selectOptions"
:slot-update-hook="updateSlotOptions"
:slot-options="slotOptions"
></functional-options>
</ul>
<ul v-show="loading" :class="[prefixCls + '-loading']">{{ localeLoadingText }}</ul>
</Drop>
</transition>
</div>
</template>
<script>
import Drop from './dropdown.vue';
import {directive as clickOutside} from 'v-click-outside-x';
import TransferDom from '../../directives/transfer-dom';
import { oneOf } from '../../utils/assist';
import Emitter from '../../mixins/emitter';
import Locale from '../../mixins/locale';
import SelectHead from './select-head.vue';
import FunctionalOptions from './functional-options.vue';
const prefixCls = 'ivu-select';
const optionRegexp = /^i-option$|^Option$/i;
const optionGroupRegexp = /option-?group/i;
const findChild = (instance, checkFn) => {
let match = checkFn(instance);
if (match) return instance;
for (let i = 0, l = instance.$children.length; i < l; i++){
const child = instance.$children[i];
match = findChild(child, checkFn);
if (match) return match;
}
};
const findOptionsInVNode = (node) => {
const opts = node.componentOptions;
if (opts && opts.tag.match(optionRegexp)) return [node];
if (!node.children && (!opts || !opts.children)) return [];
const children = [...(node.children || []), ...(opts && opts.children || [])];
const options = children.reduce(
(arr, el) => [...arr, ...findOptionsInVNode(el)], []
).filter(Boolean);
return options.length > 0 ? options : [];
};
const extractOptions = (options) => options.reduce((options, slotEntry) => {
return options.concat(findOptionsInVNode(slotEntry));
}, []);
const applyProp = (node, propName, value) => {
return {
...node,
componentOptions: {
...node.componentOptions,
propsData: {
...node.componentOptions.propsData,
[propName]: value,
}
}
};
};
const getNestedProperty = (obj, path) => {
const keys = path.split('.');
return keys.reduce((o, key) => o && o[key] || null, obj);
};
const getOptionLabel = option => {
if (option.componentOptions.propsData.label) return option.componentOptions.propsData.label;
const textContent = (option.componentOptions.children || []).reduce((str, child) => str + (child.text || ''), '');
const innerHTML = getNestedProperty(option, 'data.domProps.innerHTML');
return textContent || (typeof innerHTML === 'string' ? innerHTML : '');
};
const checkValuesNotEqual = (value,publicValue,values) => {
const strValue = JSON.stringify(value);
const strPublic = JSON.stringify(publicValue);
const strValues = JSON.stringify(values.map( item => {
return item.value;
}));
return strValue !== strPublic || strValue !== strValues || strValues !== strPublic;
};
const ANIMATION_TIMEOUT = 300;
export default {
name: 'iSelect',
mixins: [ Emitter, Locale ],
components: { FunctionalOptions, Drop, SelectHead },
directives: { clickOutside, TransferDom },
props: {
value: {
type: [String, Number, Array],
default: ''
},
// 使 value
label: {
type: [String, Number, Array],
default: ''
},
multiple: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
},
clearable: {
type: Boolean,
default: false
},
placeholder: {
type: String
},
filterable: {
type: Boolean,
default: false
},
filterMethod: {
type: Function
},
remoteMethod: {
type: Function
},
loading: {
type: Boolean,
default: false
},
loadingText: {
type: String
},
size: {
validator (value) {
return oneOf(value, ['small', 'large', 'default']);
},
default () {
return !this.$IVIEW || this.$IVIEW.size === '' ? 'default' : this.$IVIEW.size;
}
},
labelInValue: {
type: Boolean,
default: false
},
notFoundText: {
type: String
},
placement: {
validator (value) {
return oneOf(value, ['top', 'bottom', 'top-start', 'bottom-start', 'top-end', 'bottom-end']);
},
default: 'bottom-start'
},
transfer: {
type: Boolean,
default () {
return !this.$IVIEW || this.$IVIEW.transfer === '' ? false : this.$IVIEW.transfer;
}
},
// Use for AutoComplete
autoComplete: {
type: Boolean,
default: false
},
name: {
type: String
},
elementId: {
type: String
},
transferClassName: {
type: String
},
// 3.4.0
prefix: {
type: String
},
// 3.4.0
maxTagCount: {
type: Number
},
// 3.4.0
maxTagPlaceholder: {
type: Function
}
},
mounted(){
this.$on('on-select-selected', this.onOptionClick);
// set the initial values if there are any
if (!this.remote && this.selectOptions.length > 0){
this.values = this.getInitialValue().map(value => {
if (typeof value !== 'number' && !value) return null;
return this.getOptionData(value);
}).filter(Boolean);
}
this.checkUpdateStatus();
},
data () {
return {
prefixCls: prefixCls,
values: [],
dropDownWidth: 0,
visible: false,
focusIndex: -1,
isFocused: false,
query: '',
initialLabel: this.label,
hasMouseHoverHead: false,
slotOptions: this.$slots.default,
caretPosition: -1,
lastRemoteQuery: '',
unchangedQuery: true,
hasExpectedValue: false,
preventRemoteCall: false,
filterQueryChange: false, // #4273
};
},
computed: {
classes () {
return [
`${prefixCls}`,
{
[`${prefixCls}-visible`]: this.visible,
[`${prefixCls}-disabled`]: this.disabled,
[`${prefixCls}-multiple`]: this.multiple,
[`${prefixCls}-single`]: !this.multiple,
[`${prefixCls}-show-clear`]: this.showCloseIcon,
[`${prefixCls}-${this.size}`]: !!this.size
}
];
},
dropdownCls () {
return {
[prefixCls + '-dropdown-transfer']: this.transfer,
[prefixCls + '-multiple']: this.multiple && this.transfer,
['ivu-auto-complete']: this.autoComplete,
[this.transferClassName]: this.transferClassName
};
},
selectionCls () {
return {
[`${prefixCls}-selection`]: !this.autoComplete,
[`${prefixCls}-selection-focused`]: this.isFocused
};
},
localeNotFoundText () {
if (typeof this.notFoundText === 'undefined') {
return this.t('i.select.noMatch');
} else {
return this.notFoundText;
}
},
localeLoadingText () {
if (typeof this.loadingText === 'undefined') {
return this.t('i.select.loading');
} else {
return this.loadingText;
}
},
transitionName () {
return this.placement === 'bottom' ? 'slide-up' : 'slide-down';
},
dropVisible () {
let status = true;
const noOptions = !this.selectOptions || this.selectOptions.length === 0;
if (!this.loading && this.remote && this.query === '' && noOptions) status = false;
if (this.autoComplete && noOptions) status = false;
return this.visible && status;
},
showNotFoundLabel () {
const {loading, remote, selectOptions} = this;
return selectOptions && selectOptions.length === 0 && (!remote || (remote && !loading));
},
publicValue(){
if (this.labelInValue){
return this.multiple ? this.values : this.values[0];
} else {
return this.multiple ? this.values.map(option => option.value) : (this.values[0] || {}).value;
}
},
canBeCleared(){
const uiStateMatch = this.hasMouseHoverHead || this.active;
const qualifiesForClear = !this.multiple && !this.disabled && this.clearable;
return uiStateMatch && qualifiesForClear && this.reset; // we return a function
},
selectOptions() {
const selectOptions = [];
const slotOptions = (this.slotOptions || []);
let optionCounter = -1;
const currentIndex = this.focusIndex;
const selectedValues = this.values.filter(Boolean).map(({value}) => value);
if (this.autoComplete) {
const copyChildren = (node, fn) => {
return {
...node,
children: (node.children || []).map(fn).map(child => copyChildren(child, fn))
};
};
const autoCompleteOptions = extractOptions(slotOptions);
const selectedSlotOption = autoCompleteOptions[currentIndex];
return slotOptions.map(node => {
if (node === selectedSlotOption || getNestedProperty(node, 'componentOptions.propsData.value') === this.value) return applyProp(node, 'isFocused', true);
return copyChildren(node, (child) => {
if (child !== selectedSlotOption) return child;
return applyProp(child, 'isFocused', true);
});
});
}
for (let option of slotOptions) {
const cOptions = option.componentOptions;
if (!cOptions) continue;
if (cOptions.tag.match(optionGroupRegexp)){
let children = cOptions.children;
// remove filtered children
if (this.filterable){
children = children.filter(
({componentOptions}) => this.validateOption(componentOptions)
);
}
// fix #4371
children = children.map(opt => {
optionCounter = optionCounter + 1;
return this.processOption(opt, selectedValues, optionCounter === currentIndex);
});
// keep the group if it still has children // fix #4371
if (children.length > 0) selectOptions.push({...option,componentOptions:{...cOptions,children:children}});
} else {
// ignore option if not passing filter
if (this.filterQueryChange) {
const optionPassesFilter = this.filterable ? this.validateOption(cOptions) : option;
if (!optionPassesFilter) continue;
}
optionCounter = optionCounter + 1;
selectOptions.push(this.processOption(option, selectedValues, optionCounter === currentIndex));
}
}
return selectOptions;
},
flatOptions(){
return extractOptions(this.selectOptions);
},
selectTabindex(){
return this.disabled || this.filterable ? -1 : 0;
},
remote(){
return typeof this.remoteMethod === 'function';
}
},
methods: {
setQuery(query){ // PUBLIC API
if (query) {
this.onQueryChange(query);
return;
}
if (query === null) {
this.onQueryChange('');
this.values = [];
// #5620,
this.lastRemoteQuery = '';
}
},
clearSingleSelect(){ // PUBLIC API
this.$emit('on-clear');
this.hideMenu();
if (this.clearable) this.reset();
},
getOptionData(value){
const option = this.flatOptions.find(({componentOptions}) => componentOptions.propsData.value === value);
if (!option) return null;
const label = getOptionLabel(option);
return {
value: value,
label: label,
};
},
getInitialValue(){
const {multiple, remote, value} = this;
let initialValue = Array.isArray(value) ? value : [value];
if (!multiple && (typeof initialValue[0] === 'undefined' || (String(initialValue[0]).trim() === '' && !Number.isFinite(initialValue[0])))) initialValue = [];
if (remote && !multiple && value) {
const data = this.getOptionData(value);
this.query = data ? data.label : String(value);
}
return initialValue.filter((item) => {
return Boolean(item) || item === 0;
});
},
processOption(option, values, isFocused){
if (!option.componentOptions) return option;
const optionValue = option.componentOptions.propsData.value;
const disabled = option.componentOptions.propsData.disabled;
const isSelected = values.includes(optionValue);
const propsData = {
...option.componentOptions.propsData,
selected: isSelected,
isFocused: isFocused,
disabled: typeof disabled === 'undefined' ? false : disabled !== false,
};
return {
...option,
componentOptions: {
...option.componentOptions,
propsData: propsData
}
};
},
validateOption({children, elm, propsData}){
const value = propsData.value;
const label = propsData.label || '';
const textContent = (elm && elm.textContent) || (children || []).reduce((str, node) => {
const nodeText = node.elm ? node.elm.textContent : node.text;
return `${str} ${nodeText}`;
}, '') || '';
const stringValues = JSON.stringify([value, label, textContent]);
const query = this.query.toLowerCase().trim();
return stringValues.toLowerCase().includes(query);
},
toggleMenu (e, force) {
if (this.disabled) {
return false;
}
this.visible = typeof force !== 'undefined' ? force : !this.visible;
if (this.visible){
this.dropDownWidth = this.$el.getBoundingClientRect().width;
this.broadcast('Drop', 'on-update-popper');
}
},
hideMenu () {
this.toggleMenu(null, false);
setTimeout(() => this.unchangedQuery = true, ANIMATION_TIMEOUT);
},
onClickOutside(event){
if (this.visible) {
if (event.type === 'mousedown') {
event.preventDefault();
return;
}
if (this.transfer) {
const {$el} = this.$refs.dropdown;
if ($el === event.target || $el.contains(event.target)) {
return;
}
}
if (this.filterable) {
const input = this.$el.querySelector('input[type="text"]');
this.caretPosition = input.selectionStart;
this.$nextTick(() => {
const caretPosition = this.caretPosition === -1 ? input.value.length : this.caretPosition;
input.setSelectionRange(caretPosition, caretPosition);
});
}
if (!this.autoComplete) event.stopPropagation();
event.preventDefault();
this.hideMenu();
this.isFocused = true;
} else {
this.caretPosition = -1;
this.isFocused = false;
}
},
reset(){
this.query = '';
this.focusIndex = -1;
this.unchangedQuery = true;
this.values = [];
this.filterQueryChange = false;
},
handleKeydown (e) {
if (e.key === 'Backspace'){
return; // so we don't call preventDefault
}
if (this.visible) {
e.preventDefault();
if (e.key === 'Tab'){
e.stopPropagation();
}
// Esc slide-up
if (e.key === 'Escape') {
e.stopPropagation();
this.hideMenu();
}
// next
if (e.key === 'ArrowUp') {
this.navigateOptions(-1);
}
// prev
if (e.key === 'ArrowDown') {
this.navigateOptions(1);
}
// enter
if (e.key === 'Enter') {
if (this.focusIndex === -1) return this.hideMenu();
const optionComponent = this.flatOptions[this.focusIndex];
// fix a script error when searching
if (optionComponent) {
const option = this.getOptionData(optionComponent.componentOptions.propsData.value);
this.onOptionClick(option);
} else {
this.hideMenu();
}
}
} else {
const keysThatCanOpenSelect = ['ArrowUp', 'ArrowDown'];
if (keysThatCanOpenSelect.includes(e.key)) this.toggleMenu(null, true);
}
},
navigateOptions(direction){
const optionsLength = this.flatOptions.length - 1;
let index = this.focusIndex + direction;
if (index < 0) index = optionsLength;
if (index > optionsLength) index = 0;
// find nearest option in case of disabled options in between
if (direction > 0){
let nearestActiveOption = -1;
for (let i = 0; i < this.flatOptions.length; i++){
const optionIsActive = !this.flatOptions[i].componentOptions.propsData.disabled;
if (optionIsActive) nearestActiveOption = i;
if (nearestActiveOption >= index) break;
}
index = nearestActiveOption;
} else {
let nearestActiveOption = this.flatOptions.length;
for (let i = optionsLength; i >= 0; i--){
const optionIsActive = !this.flatOptions[i].componentOptions.propsData.disabled;
if (optionIsActive) nearestActiveOption = i;
if (nearestActiveOption <= index) break;
}
index = nearestActiveOption;
}
this.focusIndex = index;
},
onOptionClick(option) {
if (this.multiple){
// keep the query for remote select
if (this.remote) this.lastRemoteQuery = this.lastRemoteQuery || this.query;
else this.lastRemoteQuery = '';
const valueIsSelected = this.values.find(({value}) => value === option.value);
if (valueIsSelected){
this.values = this.values.filter(({value}) => value !== option.value);
} else {
this.values = this.values.concat(option);
}
this.isFocused = true; // so we put back focus after clicking with mouse on option elements
} else {
this.query = String(option.label).trim();
this.values = [option];
this.lastRemoteQuery = '';
this.hideMenu();
}
this.focusIndex = this.flatOptions.findIndex((opt) => {
if (!opt || !opt.componentOptions) return false;
return opt.componentOptions.propsData.value === option.value;
});
if (this.filterable){
const inputField = this.$el.querySelector('input[type="text"]');
if (!this.autoComplete) this.$nextTick(() => inputField.focus());
}
this.broadcast('Drop', 'on-update-popper');
setTimeout(() => {
this.filterQueryChange = false;
}, ANIMATION_TIMEOUT);
},
onQueryChange(query) {
if (query.length > 0 && query !== this.query) {
// in 'AutoComplete', when set an initial value asynchronously,
// the 'dropdown list' should be stay hidden.
// [issue #5150]
if (this.autoComplete) {
let isInputFocused =
document.hasFocus &&
document.hasFocus() &&
document.activeElement === this.$el.querySelector('input');
this.visible = isInputFocused;
} else {
this.visible = true;
}
}
this.query = query;
this.unchangedQuery = this.visible;
this.filterQueryChange = true;
},
toggleHeaderFocus({type}){
if (this.disabled) {
return;
}
this.isFocused = type === 'focus';
},
updateSlotOptions(){
this.slotOptions = this.$slots.default;
},
checkUpdateStatus() {
if (this.getInitialValue().length > 0 && this.selectOptions.length === 0) {
this.hasExpectedValue = true;
}
},
},
watch: {
value(value){
const {getInitialValue, getOptionData, publicValue, values} = this;
this.checkUpdateStatus();
if (value === '') this.values = [];
else if (checkValuesNotEqual(value,publicValue,values)) {
this.$nextTick(() => this.values = getInitialValue().map(getOptionData).filter(Boolean));
this.dispatch('FormItem', 'on-form-change', this.publicValue);
}
},
values(now, before){
const newValue = JSON.stringify(now);
const oldValue = JSON.stringify(before);
// v-model is always just the value, event with labelInValue === true
const vModelValue = (this.publicValue && this.labelInValue) ?
(this.multiple ? this.publicValue.map(({value}) => value) : this.publicValue.value) :
this.publicValue;
const shouldEmitInput = newValue !== oldValue && vModelValue !== this.value;
if (shouldEmitInput) {
this.$emit('input', vModelValue); // to update v-model
this.$emit('on-change', this.publicValue);
this.dispatch('FormItem', 'on-form-change', this.publicValue);
}
},
query (query) {
this.$emit('on-query-change', query);
const {remoteMethod, lastRemoteQuery} = this;
const hasValidQuery = query !== '' && (query !== lastRemoteQuery || !lastRemoteQuery);
const shouldCallRemoteMethod = remoteMethod && hasValidQuery && !this.preventRemoteCall;
this.preventRemoteCall = false; // remove the flag
if (shouldCallRemoteMethod){
this.focusIndex = -1;
const promise = this.remoteMethod(query);
this.initialLabel = '';
if (promise && promise.then){
promise.then(options => {
if (options) this.options = options;
});
}
}
if (query !== '' && this.remote) this.lastRemoteQuery = query;
},
loading(state){
if (state === false){
this.updateSlotOptions();
}
},
isFocused(focused){
const el = this.filterable ? this.$el.querySelector('input[type="text"]') : this.$el;
el[this.isFocused ? 'focus' : 'blur']();
// restore query value in filterable single selects
const [selectedOption] = this.values;
if (selectedOption && this.filterable && !this.multiple && !focused){
const selectedLabel = String(selectedOption.label || selectedOption.value).trim();
if (selectedLabel && this.query !== selectedLabel) {
this.preventRemoteCall = true;
this.query = selectedLabel;
}
}
},
focusIndex(index){
if (index < 0 || this.autoComplete) return;
// update scroll
const optionValue = this.flatOptions[index].componentOptions.propsData.value;
const optionInstance = findChild(this, ({$options}) => {
return $options.componentName === 'select-item' && $options.propsData.value === optionValue;
});
let bottomOverflowDistance = optionInstance.$el.getBoundingClientRect().bottom - this.$refs.dropdown.$el.getBoundingClientRect().bottom;
let topOverflowDistance = optionInstance.$el.getBoundingClientRect().top - this.$refs.dropdown.$el.getBoundingClientRect().top;
if (bottomOverflowDistance > 0) {
this.$refs.dropdown.$el.scrollTop += bottomOverflowDistance;
}
if (topOverflowDistance < 0) {
this.$refs.dropdown.$el.scrollTop += topOverflowDistance;
}
},
dropVisible(open){
this.broadcast('Drop', open ? 'on-update-popper' : 'on-destroy-popper');
},
selectOptions(){
if (this.hasExpectedValue && this.selectOptions.length > 0){
if (this.values.length === 0) {
this.values = this.getInitialValue();
}
this.values = this.values.map(this.getOptionData).filter(Boolean);
this.hasExpectedValue = false;
}
if (this.slotOptions && this.slotOptions.length === 0){
this.query = '';
}
// dropdown ()
// dropdown
this.broadcast('Drop', 'on-update-popper');
},
visible(state){
this.$emit('on-open-change', state);
},
slotOptions(options, old){
// #4626 Options label v-model
// remote getInitialValue bug
if (!this.remote) {
const values = this.getInitialValue();
if (this.flatOptions && this.flatOptions.length && values.length && !this.multiple) {
this.values = values.map(this.getOptionData).filter(Boolean);
}
}
// dropdown
// dropdown
if (options && old && options.length !== old.length) {
this.broadcast('Drop', 'on-update-popper');
}
},
}
};
</script>

View file

@ -0,0 +1,14 @@
export function debounce(fn) {
let waiting;
return function() {
if (waiting) return;
waiting = true;
const context = this,
args = arguments;
const later = function() {
waiting = false;
fn.apply(context, args);
};
this.$nextTick(later);
};
}