first
This commit is contained in:
650
packages/cascader/src/cascader.vue
Normal file
650
packages/cascader/src/cascader.vue
Normal file
@ -0,0 +1,650 @@
|
||||
<template>
|
||||
<div
|
||||
ref="reference"
|
||||
:class="[
|
||||
'el-cascader',
|
||||
realSize && `el-cascader--${realSize}`,
|
||||
{ 'is-disabled': isDisabled }
|
||||
]"
|
||||
v-clickoutside="() => toggleDropDownVisible(false)"
|
||||
@mouseenter="inputHover = true"
|
||||
@mouseleave="inputHover = false"
|
||||
@click="() => toggleDropDownVisible(readonly ? undefined : true)"
|
||||
@keydown="handleKeyDown">
|
||||
|
||||
<el-input
|
||||
ref="input"
|
||||
v-model="multiple ? presentText : inputValue"
|
||||
:size="realSize"
|
||||
:placeholder="placeholder"
|
||||
:readonly="readonly"
|
||||
:disabled="isDisabled"
|
||||
:validate-event="false"
|
||||
:class="{ 'is-focus': dropDownVisible }"
|
||||
@focus="handleFocus"
|
||||
@blur="handleBlur"
|
||||
@input="handleInput">
|
||||
<template slot="suffix">
|
||||
<i
|
||||
v-if="clearBtnVisible"
|
||||
key="clear"
|
||||
class="el-input__icon el-icon-circle-close"
|
||||
@click.stop="handleClear"></i>
|
||||
<i
|
||||
v-else
|
||||
key="arrow-down"
|
||||
:class="[
|
||||
'el-input__icon',
|
||||
'el-icon-arrow-down',
|
||||
dropDownVisible && 'is-reverse'
|
||||
]"
|
||||
@click.stop="toggleDropDownVisible()"></i>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<div v-if="multiple" class="el-cascader__tags">
|
||||
<el-tag
|
||||
v-for="(tag, index) in presentTags"
|
||||
:key="tag.key"
|
||||
type="info"
|
||||
:size="tagSize"
|
||||
:hit="tag.hitState"
|
||||
:closable="tag.closable"
|
||||
disable-transitions
|
||||
@close="deleteTag(index)">
|
||||
<span>{{ tag.text }}</span>
|
||||
</el-tag>
|
||||
<input
|
||||
v-if="filterable && !isDisabled"
|
||||
v-model.trim="inputValue"
|
||||
type="text"
|
||||
class="el-cascader__search-input"
|
||||
:placeholder="presentTags.length ? '' : placeholder"
|
||||
@input="e => handleInput(inputValue, e)"
|
||||
@click.stop="toggleDropDownVisible(true)"
|
||||
@keydown.delete="handleDelete">
|
||||
</div>
|
||||
|
||||
<transition name="el-zoom-in-top" @after-leave="handleDropdownLeave">
|
||||
<div
|
||||
v-show="dropDownVisible"
|
||||
ref="popper"
|
||||
:class="['el-popper', 'el-cascader__dropdown', popperClass]">
|
||||
<el-cascader-panel
|
||||
ref="panel"
|
||||
v-show="!filtering"
|
||||
v-model="checkedValue"
|
||||
:options="options"
|
||||
:props="config"
|
||||
:border="false"
|
||||
:render-label="$scopedSlots.default"
|
||||
@expand-change="handleExpandChange"
|
||||
@close="toggleDropDownVisible(false)"></el-cascader-panel>
|
||||
<el-scrollbar
|
||||
ref="suggestionPanel"
|
||||
v-if="filterable"
|
||||
v-show="filtering"
|
||||
tag="ul"
|
||||
class="el-cascader__suggestion-panel"
|
||||
view-class="el-cascader__suggestion-list"
|
||||
@keydown.native="handleSuggestionKeyDown">
|
||||
<template v-if="suggestions.length">
|
||||
<li
|
||||
v-for="(item, index) in suggestions"
|
||||
:key="item.uid"
|
||||
:class="[
|
||||
'el-cascader__suggestion-item',
|
||||
item.checked && 'is-checked'
|
||||
]"
|
||||
:tabindex="-1"
|
||||
@click="handleSuggestionClick(index)">
|
||||
<span>{{ item.text }}</span>
|
||||
<i v-if="item.checked" class="el-icon-check"></i>
|
||||
</li>
|
||||
</template>
|
||||
<slot v-else name="empty">
|
||||
<li class="el-cascader__empty-text">{{ t('el.cascader.noMatch') }}</li>
|
||||
</slot>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Popper from 'element-ui/src/utils/vue-popper';
|
||||
import Clickoutside from 'element-ui/src/utils/clickoutside';
|
||||
import Emitter from 'element-ui/src/mixins/emitter';
|
||||
import Locale from 'element-ui/src/mixins/locale';
|
||||
import Migrating from 'element-ui/src/mixins/migrating';
|
||||
import ElInput from 'element-ui/packages/input';
|
||||
import ElTag from 'element-ui/packages/tag';
|
||||
import ElScrollbar from 'element-ui/packages/scrollbar';
|
||||
import ElCascaderPanel from 'element-ui/packages/cascader-panel';
|
||||
import AriaUtils from 'element-ui/src/utils/aria-utils';
|
||||
import { t } from 'element-ui/src/locale';
|
||||
import { isEqual, isEmpty, kebabCase } from 'element-ui/src/utils/util';
|
||||
import { isUndefined, isFunction } from 'element-ui/src/utils/types';
|
||||
import { isDef } from 'element-ui/src/utils/shared';
|
||||
import { addResizeListener, removeResizeListener } from 'element-ui/src/utils/resize-event';
|
||||
import debounce from 'throttle-debounce/debounce';
|
||||
|
||||
const { keys: KeyCode } = AriaUtils;
|
||||
const MigratingProps = {
|
||||
expandTrigger: {
|
||||
newProp: 'expandTrigger',
|
||||
type: String
|
||||
},
|
||||
changeOnSelect: {
|
||||
newProp: 'checkStrictly',
|
||||
type: Boolean
|
||||
},
|
||||
hoverThreshold: {
|
||||
newProp: 'hoverThreshold',
|
||||
type: Number
|
||||
}
|
||||
};
|
||||
|
||||
const PopperMixin = {
|
||||
props: {
|
||||
placement: {
|
||||
type: String,
|
||||
default: 'bottom-start'
|
||||
},
|
||||
appendToBody: Popper.props.appendToBody,
|
||||
visibleArrow: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
arrowOffset: Popper.props.arrowOffset,
|
||||
offset: Popper.props.offset,
|
||||
boundariesPadding: Popper.props.boundariesPadding,
|
||||
popperOptions: Popper.props.popperOptions
|
||||
},
|
||||
methods: Popper.methods,
|
||||
data: Popper.data,
|
||||
beforeDestroy: Popper.beforeDestroy
|
||||
};
|
||||
|
||||
const InputSizeMap = {
|
||||
medium: 36,
|
||||
small: 32,
|
||||
mini: 28
|
||||
};
|
||||
|
||||
export default {
|
||||
name: 'ElCascader',
|
||||
|
||||
directives: { Clickoutside },
|
||||
|
||||
mixins: [PopperMixin, Emitter, Locale, Migrating],
|
||||
|
||||
inject: {
|
||||
elForm: {
|
||||
default: ''
|
||||
},
|
||||
elFormItem: {
|
||||
default: ''
|
||||
}
|
||||
},
|
||||
|
||||
components: {
|
||||
ElInput,
|
||||
ElTag,
|
||||
ElScrollbar,
|
||||
ElCascaderPanel
|
||||
},
|
||||
|
||||
props: {
|
||||
value: {},
|
||||
options: Array,
|
||||
props: Object,
|
||||
size: String,
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: () => t('el.cascader.placeholder')
|
||||
},
|
||||
disabled: Boolean,
|
||||
clearable: Boolean,
|
||||
filterable: Boolean,
|
||||
filterMethod: Function,
|
||||
separator: {
|
||||
type: String,
|
||||
default: ' / '
|
||||
},
|
||||
showAllLevels: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
collapseTags: Boolean,
|
||||
debounce: {
|
||||
type: Number,
|
||||
default: 300
|
||||
},
|
||||
beforeFilter: {
|
||||
type: Function,
|
||||
default: () => (() => {})
|
||||
},
|
||||
popperClass: String
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
dropDownVisible: false,
|
||||
checkedValue: this.value || null,
|
||||
inputHover: false,
|
||||
inputValue: null,
|
||||
presentText: null,
|
||||
presentTags: [],
|
||||
checkedNodes: [],
|
||||
filtering: false,
|
||||
suggestions: [],
|
||||
inputInitialHeight: 0,
|
||||
pressDeleteCount: 0
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
realSize() {
|
||||
const _elFormItemSize = (this.elFormItem || {}).elFormItemSize;
|
||||
return this.size || _elFormItemSize || (this.$ELEMENT || {}).size;
|
||||
},
|
||||
tagSize() {
|
||||
return ['small', 'mini'].indexOf(this.realSize) > -1
|
||||
? 'mini'
|
||||
: 'small';
|
||||
},
|
||||
isDisabled() {
|
||||
return this.disabled || (this.elForm || {}).disabled;
|
||||
},
|
||||
config() {
|
||||
const config = this.props || {};
|
||||
const { $attrs } = this;
|
||||
|
||||
Object
|
||||
.keys(MigratingProps)
|
||||
.forEach(oldProp => {
|
||||
const { newProp, type } = MigratingProps[oldProp];
|
||||
let oldValue = $attrs[oldProp] || $attrs[kebabCase(oldProp)];
|
||||
if (isDef(oldProp) && !isDef(config[newProp])) {
|
||||
if (type === Boolean && oldValue === '') {
|
||||
oldValue = true;
|
||||
}
|
||||
config[newProp] = oldValue;
|
||||
}
|
||||
});
|
||||
|
||||
return config;
|
||||
},
|
||||
multiple() {
|
||||
return this.config.multiple;
|
||||
},
|
||||
leafOnly() {
|
||||
return !this.config.checkStrictly;
|
||||
},
|
||||
readonly() {
|
||||
return !this.filterable || this.multiple;
|
||||
},
|
||||
clearBtnVisible() {
|
||||
if (!this.clearable || this.isDisabled || this.filtering || !this.inputHover) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.multiple
|
||||
? !!this.checkedNodes.filter(node => !node.isDisabled).length
|
||||
: !!this.presentText;
|
||||
},
|
||||
panel() {
|
||||
return this.$refs.panel;
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
disabled() {
|
||||
this.computePresentContent();
|
||||
},
|
||||
value(val) {
|
||||
if (!isEqual(val, this.checkedValue)) {
|
||||
this.checkedValue = val;
|
||||
this.computePresentContent();
|
||||
}
|
||||
},
|
||||
checkedValue(val) {
|
||||
const { value, dropDownVisible } = this;
|
||||
const { checkStrictly, multiple } = this.config;
|
||||
|
||||
if (!isEqual(val, value) || isUndefined(value)) {
|
||||
this.computePresentContent();
|
||||
// hide dropdown when single mode
|
||||
if (!multiple && !checkStrictly && dropDownVisible) {
|
||||
this.toggleDropDownVisible(false);
|
||||
}
|
||||
|
||||
this.$emit('input', val);
|
||||
this.$emit('change', val);
|
||||
this.dispatch('ElFormItem', 'el.form.change', [val]);
|
||||
}
|
||||
},
|
||||
options: {
|
||||
handler: function() {
|
||||
this.$nextTick(this.computePresentContent);
|
||||
},
|
||||
deep: true
|
||||
},
|
||||
presentText(val) {
|
||||
this.inputValue = val;
|
||||
},
|
||||
presentTags(val, oldVal) {
|
||||
if (this.multiple && (val.length || oldVal.length)) {
|
||||
this.$nextTick(this.updateStyle);
|
||||
}
|
||||
},
|
||||
filtering(val) {
|
||||
this.$nextTick(this.updatePopper);
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const { input } = this.$refs;
|
||||
if (input && input.$el) {
|
||||
this.inputInitialHeight = input.$el.offsetHeight || InputSizeMap[this.realSize] || 40;
|
||||
}
|
||||
|
||||
if (!isEmpty(this.value)) {
|
||||
this.computePresentContent();
|
||||
}
|
||||
|
||||
this.filterHandler = debounce(this.debounce, () => {
|
||||
const { inputValue } = this;
|
||||
|
||||
if (!inputValue) {
|
||||
this.filtering = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const before = this.beforeFilter(inputValue);
|
||||
if (before && before.then) {
|
||||
before.then(this.getSuggestions);
|
||||
} else if (before !== false) {
|
||||
this.getSuggestions();
|
||||
} else {
|
||||
this.filtering = false;
|
||||
}
|
||||
});
|
||||
|
||||
addResizeListener(this.$el, this.updateStyle);
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
removeResizeListener(this.$el, this.updateStyle);
|
||||
},
|
||||
|
||||
methods: {
|
||||
getMigratingConfig() {
|
||||
return {
|
||||
props: {
|
||||
'expand-trigger': 'expand-trigger is removed, use `props.expandTrigger` instead.',
|
||||
'change-on-select': 'change-on-select is removed, use `props.checkStrictly` instead.',
|
||||
'hover-threshold': 'hover-threshold is removed, use `props.hoverThreshold` instead'
|
||||
},
|
||||
events: {
|
||||
'active-item-change': 'active-item-change is renamed to expand-change'
|
||||
}
|
||||
};
|
||||
},
|
||||
toggleDropDownVisible(visible) {
|
||||
if (this.isDisabled) return;
|
||||
|
||||
const { dropDownVisible } = this;
|
||||
const { input } = this.$refs;
|
||||
visible = isDef(visible) ? visible : !dropDownVisible;
|
||||
if (visible !== dropDownVisible) {
|
||||
this.dropDownVisible = visible;
|
||||
if (visible) {
|
||||
this.$nextTick(() => {
|
||||
this.updatePopper();
|
||||
this.panel.scrollIntoView();
|
||||
});
|
||||
}
|
||||
input.$refs.input.setAttribute('aria-expanded', visible);
|
||||
this.$emit('visible-change', visible);
|
||||
}
|
||||
},
|
||||
handleDropdownLeave() {
|
||||
this.filtering = false;
|
||||
this.inputValue = this.presentText;
|
||||
},
|
||||
handleKeyDown(event) {
|
||||
switch (event.keyCode) {
|
||||
case KeyCode.enter:
|
||||
this.toggleDropDownVisible();
|
||||
break;
|
||||
case KeyCode.down:
|
||||
this.toggleDropDownVisible(true);
|
||||
this.focusFirstNode();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case KeyCode.esc:
|
||||
case KeyCode.tab:
|
||||
this.toggleDropDownVisible(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleFocus(e) {
|
||||
this.$emit('focus', e);
|
||||
},
|
||||
handleBlur(e) {
|
||||
this.$emit('blur', e);
|
||||
},
|
||||
handleInput(val, event) {
|
||||
!this.dropDownVisible && this.toggleDropDownVisible(true);
|
||||
|
||||
if (event && event.isComposing) return;
|
||||
if (val) {
|
||||
this.filterHandler();
|
||||
} else {
|
||||
this.filtering = false;
|
||||
}
|
||||
},
|
||||
handleClear() {
|
||||
this.presentText = '';
|
||||
this.panel.clearCheckedNodes();
|
||||
},
|
||||
handleExpandChange(value) {
|
||||
this.$nextTick(this.updatePopper.bind(this));
|
||||
this.$emit('expand-change', value);
|
||||
this.$emit('active-item-change', value); // Deprecated
|
||||
},
|
||||
focusFirstNode() {
|
||||
this.$nextTick(() => {
|
||||
const { filtering } = this;
|
||||
const { popper, suggestionPanel } = this.$refs;
|
||||
let firstNode = null;
|
||||
|
||||
if (filtering && suggestionPanel) {
|
||||
firstNode = suggestionPanel.$el.querySelector('.el-cascader__suggestion-item');
|
||||
} else {
|
||||
const firstMenu = popper.querySelector('.el-cascader-menu');
|
||||
firstNode = firstMenu.querySelector('.el-cascader-node[tabindex="-1"]');
|
||||
}
|
||||
|
||||
if (firstNode) {
|
||||
firstNode.focus();
|
||||
!filtering && firstNode.click();
|
||||
}
|
||||
});
|
||||
},
|
||||
computePresentContent() {
|
||||
// nextTick is required, because checked nodes may not change right now
|
||||
this.$nextTick(() => {
|
||||
if (this.config.multiple) {
|
||||
this.computePresentTags();
|
||||
this.presentText = this.presentTags.length ? ' ' : null;
|
||||
} else {
|
||||
this.computePresentText();
|
||||
}
|
||||
});
|
||||
},
|
||||
computePresentText() {
|
||||
const { checkedValue, config } = this;
|
||||
if (!isEmpty(checkedValue)) {
|
||||
const node = this.panel.getNodeByValue(checkedValue);
|
||||
if (node && (config.checkStrictly || node.isLeaf)) {
|
||||
this.presentText = node.getText(this.showAllLevels, this.separator);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.presentText = null;
|
||||
},
|
||||
computePresentTags() {
|
||||
const { isDisabled, leafOnly, showAllLevels, separator, collapseTags } = this;
|
||||
const checkedNodes = this.getCheckedNodes(leafOnly);
|
||||
const tags = [];
|
||||
|
||||
const genTag = node => ({
|
||||
node,
|
||||
key: node.uid,
|
||||
text: node.getText(showAllLevels, separator),
|
||||
hitState: false,
|
||||
closable: !isDisabled && !node.isDisabled
|
||||
});
|
||||
|
||||
if (checkedNodes.length) {
|
||||
const [first, ...rest] = checkedNodes;
|
||||
const restCount = rest.length;
|
||||
tags.push(genTag(first));
|
||||
|
||||
if (restCount) {
|
||||
if (collapseTags) {
|
||||
tags.push({
|
||||
key: -1,
|
||||
text: `+ ${restCount}`,
|
||||
closable: false
|
||||
});
|
||||
} else {
|
||||
rest.forEach(node => tags.push(genTag(node)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.checkedNodes = checkedNodes;
|
||||
this.presentTags = tags;
|
||||
},
|
||||
getSuggestions() {
|
||||
let { filterMethod } = this;
|
||||
|
||||
if (!isFunction(filterMethod)) {
|
||||
filterMethod = (node, keyword) => node.text.includes(keyword);
|
||||
}
|
||||
|
||||
const suggestions = this.panel.getFlattedNodes(this.leafOnly)
|
||||
.filter(node => {
|
||||
if (node.isDisabled) return false;
|
||||
node.text = node.getText(this.showAllLevels, this.separator) || '';
|
||||
return filterMethod(node, this.inputValue);
|
||||
});
|
||||
|
||||
if (this.multiple) {
|
||||
this.presentTags.forEach(tag => {
|
||||
tag.hitState = false;
|
||||
});
|
||||
} else {
|
||||
suggestions.forEach(node => {
|
||||
node.checked = isEqual(this.checkedValue, node.getValueByOption());
|
||||
});
|
||||
}
|
||||
|
||||
this.filtering = true;
|
||||
this.suggestions = suggestions;
|
||||
this.$nextTick(this.updatePopper);
|
||||
},
|
||||
handleSuggestionKeyDown(event) {
|
||||
const { keyCode, target } = event;
|
||||
switch (keyCode) {
|
||||
case KeyCode.enter:
|
||||
target.click();
|
||||
break;
|
||||
case KeyCode.up:
|
||||
const prev = target.previousElementSibling;
|
||||
prev && prev.focus();
|
||||
break;
|
||||
case KeyCode.down:
|
||||
const next = target.nextElementSibling;
|
||||
next && next.focus();
|
||||
break;
|
||||
case KeyCode.esc:
|
||||
case KeyCode.tab:
|
||||
this.toggleDropDownVisible(false);
|
||||
break;
|
||||
}
|
||||
},
|
||||
handleDelete() {
|
||||
const { inputValue, pressDeleteCount, presentTags } = this;
|
||||
const lastIndex = presentTags.length - 1;
|
||||
const lastTag = presentTags[lastIndex];
|
||||
this.pressDeleteCount = inputValue ? 0 : pressDeleteCount + 1;
|
||||
|
||||
if (!lastTag) return;
|
||||
|
||||
if (this.pressDeleteCount) {
|
||||
if (lastTag.hitState) {
|
||||
this.deleteTag(lastIndex);
|
||||
} else {
|
||||
lastTag.hitState = true;
|
||||
}
|
||||
}
|
||||
},
|
||||
handleSuggestionClick(index) {
|
||||
const { multiple } = this;
|
||||
const targetNode = this.suggestions[index];
|
||||
|
||||
if (multiple) {
|
||||
const { checked } = targetNode;
|
||||
targetNode.doCheck(!checked);
|
||||
this.panel.calculateMultiCheckedValue();
|
||||
} else {
|
||||
this.checkedValue = targetNode.getValueByOption();
|
||||
this.toggleDropDownVisible(false);
|
||||
}
|
||||
},
|
||||
deleteTag(index) {
|
||||
const { checkedValue } = this;
|
||||
const val = checkedValue[index];
|
||||
this.checkedValue = checkedValue.filter((n, i) => i !== index);
|
||||
this.$emit('remove-tag', val);
|
||||
},
|
||||
updateStyle() {
|
||||
const { $el, inputInitialHeight } = this;
|
||||
if (this.$isServer || !$el) return;
|
||||
|
||||
const { suggestionPanel } = this.$refs;
|
||||
const inputInner = $el.querySelector('.el-input__inner');
|
||||
|
||||
if (!inputInner) return;
|
||||
|
||||
const tags = $el.querySelector('.el-cascader__tags');
|
||||
let suggestionPanelEl = null;
|
||||
|
||||
if (suggestionPanel && (suggestionPanelEl = suggestionPanel.$el)) {
|
||||
const suggestionList = suggestionPanelEl.querySelector('.el-cascader__suggestion-list');
|
||||
suggestionList.style.minWidth = inputInner.offsetWidth + 'px';
|
||||
}
|
||||
|
||||
if (tags) {
|
||||
const { offsetHeight } = tags;
|
||||
const height = Math.max(offsetHeight + 6, inputInitialHeight) + 'px';
|
||||
inputInner.style.height = height;
|
||||
this.updatePopper();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* public methods
|
||||
*/
|
||||
getCheckedNodes(leafOnly) {
|
||||
return this.panel.getCheckedNodes(leafOnly);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
Reference in New Issue
Block a user