123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650 |
- <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>
|