element-plus
Version:
A Component Library for Vue3.0
388 lines (379 loc) • 14.4 kB
JavaScript
import { defineComponent, ref, computed, watch, onMounted, onUpdated, nextTick, resolveComponent, resolveDirective, openBlock, createBlock, withCtx, withDirectives, createVNode, mergeProps, withKeys, withModifiers, createSlots, renderSlot, Fragment, renderList, createTextVNode, toDisplayString } from 'vue';
import { useAttrs } from '../hooks';
import debounce from 'lodash/debounce';
import { ClickOutside } from '../directives';
import { generateId, isArray } from '../utils/util';
import { UPDATE_MODEL_EVENT } from '../utils/constants';
import throwError from '../utils/error';
import ElInput from '../el-input';
import ElScrollbar from '../el-scrollbar';
import ElPopper from '../el-popper';
/**
* Make a map and return a function for checking if a key
* is in that map.
* IMPORTANT: all calls of this function must be prefixed with
* \/\*#\_\_PURE\_\_\*\/
* So that rollup can tree-shake them if necessary.
*/
const EMPTY_OBJ = (process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {};
const EMPTY_ARR = (process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];
const NOOP = () => { };
var script = defineComponent({
name: 'ElAutocomplete',
components: {
ElPopper,
ElInput,
ElScrollbar,
},
directives: {
clickoutside: ClickOutside,
},
inheritAttrs: false,
props: {
valueKey: {
type: String,
default: 'value',
},
modelValue: {
type: [String, Number],
default: '',
},
debounce: {
type: Number,
default: 300,
},
placement: {
type: String,
validator: (val) => {
return ['top', 'top-start', 'top-end', 'bottom', 'bottom-start', 'bottom-end'].includes(val);
},
default: 'bottom-start',
},
fetchSuggestions: {
type: Function,
default: NOOP,
},
popperClass: {
type: String,
default: '',
},
triggerOnFocus: {
type: Boolean,
default: true,
},
selectWhenUnmatched: {
type: Boolean,
default: false,
},
hideLoading: {
type: Boolean,
default: false,
},
popperAppendToBody: {
type: Boolean,
default: true,
},
highlightFirstItem: {
type: Boolean,
default: false,
},
},
emits: [UPDATE_MODEL_EVENT, 'input', 'change', 'focus', 'blur', 'clear', 'select'],
setup(props, ctx) {
const attrs = useAttrs();
const suggestions = ref([]);
const highlightedIndex = ref(-1);
const dropdownWidth = ref('');
const activated = ref(false);
const suggestionDisabled = ref(false);
const loading = ref(false);
const inputRef = ref(null);
const regionRef = ref(null);
const popper = ref(null);
const id = computed(() => {
return `el-autocomplete-${generateId()}`;
});
const suggestionVisible = computed(() => {
const isValidData = isArray(suggestions.value) && suggestions.value.length > 0;
return (isValidData || loading.value) && activated.value;
});
const suggestionLoading = computed(() => {
return !props.hideLoading && loading.value;
});
const updatePopperPosition = () => {
nextTick(popper.value.update);
};
watch(suggestionVisible, () => {
dropdownWidth.value = `${inputRef.value.$el.offsetWidth}px`;
});
onMounted(() => {
inputRef.value.inputOrTextarea.setAttribute('role', 'textbox');
inputRef.value.inputOrTextarea.setAttribute('aria-autocomplete', 'list');
inputRef.value.inputOrTextarea.setAttribute('aria-controls', 'id');
inputRef.value.inputOrTextarea.setAttribute('aria-activedescendant', `${id.value}-item-${highlightedIndex.value}`);
const $ul = regionRef.value.querySelector('.el-autocomplete-suggestion__list');
$ul.setAttribute('role', 'listbox');
$ul.setAttribute('id', id.value);
});
onUpdated(updatePopperPosition);
const getData = queryString => {
if (suggestionDisabled.value) {
return;
}
loading.value = true;
updatePopperPosition();
props.fetchSuggestions(queryString, suggestionsArg => {
loading.value = false;
if (suggestionDisabled.value) {
return;
}
if (isArray(suggestionsArg)) {
suggestions.value = suggestionsArg;
highlightedIndex.value = props.highlightFirstItem ? 0 : -1;
}
else {
throwError('ElAutocomplete', 'autocomplete suggestions must be an array');
}
});
};
const debouncedGetData = debounce(getData, props.debounce);
const handleInput = value => {
ctx.emit('input', value);
ctx.emit(UPDATE_MODEL_EVENT, value);
suggestionDisabled.value = false;
if (!props.triggerOnFocus && !value) {
suggestionDisabled.value = true;
suggestions.value = [];
return;
}
debouncedGetData(value);
};
const handleChange = value => {
ctx.emit('change', value);
};
const handleFocus = e => {
activated.value = true;
ctx.emit('focus', e);
if (props.triggerOnFocus) {
debouncedGetData(props.modelValue);
}
};
const handleBlur = e => {
ctx.emit('blur', e);
};
const handleClear = () => {
activated.value = false;
ctx.emit(UPDATE_MODEL_EVENT, '');
ctx.emit('clear');
};
const handleKeyEnter = () => {
if (suggestionVisible.value
&& highlightedIndex.value >= 0
&& highlightedIndex.value < suggestions.value.length) {
select(suggestions.value[highlightedIndex.value]);
}
else if (props.selectWhenUnmatched) {
ctx.emit('select', { value: props.modelValue });
nextTick(() => {
suggestions.value = [];
highlightedIndex.value = -1;
});
}
};
const close = () => {
activated.value = false;
};
const focus = () => {
inputRef.value.focus();
};
const select = item => {
ctx.emit('input', item[props.valueKey]);
ctx.emit(UPDATE_MODEL_EVENT, item[props.valueKey]);
ctx.emit('select', item);
nextTick(() => {
suggestions.value = [];
highlightedIndex.value = -1;
});
};
const highlight = index => {
if (!suggestionVisible.value || loading.value) {
return;
}
if (index < 0) {
highlightedIndex.value = -1;
return;
}
if (index >= suggestions.value.length) {
index = suggestions.value.length - 1;
}
const suggestion = regionRef.value.querySelector('.el-autocomplete-suggestion__wrap');
const suggestionList = suggestion.querySelectorAll('.el-autocomplete-suggestion__list li');
const highlightItem = suggestionList[index];
const scrollTop = suggestion.scrollTop;
const offsetTop = highlightItem.offsetTop;
if (offsetTop + highlightItem.scrollHeight > (scrollTop + suggestion.clientHeight)) {
suggestion.scrollTop += highlightItem.scrollHeight;
}
if (offsetTop < scrollTop) {
suggestion.scrollTop -= highlightItem.scrollHeight;
}
highlightedIndex.value = index;
inputRef.value.inputOrTextarea.setAttribute('aria-activedescendant', `${id.value}-item-${highlightedIndex.value}`);
};
return {
attrs,
suggestions,
highlightedIndex,
dropdownWidth,
activated,
suggestionDisabled,
loading,
inputRef,
regionRef,
popper,
id,
suggestionVisible,
suggestionLoading,
getData,
handleInput,
handleChange,
handleFocus,
handleBlur,
handleClear,
handleKeyEnter,
close,
focus,
select,
highlight,
};
},
});
const _hoisted_1 = { key: 0 };
const _hoisted_2 = /*#__PURE__*/createVNode("i", { class: "el-icon-loading" }, null, -1 /* HOISTED */);
function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_el_input = resolveComponent("el-input");
const _component_el_scrollbar = resolveComponent("el-scrollbar");
const _component_el_popper = resolveComponent("el-popper");
const _directive_clickoutside = resolveDirective("clickoutside");
return (openBlock(), createBlock(_component_el_popper, {
ref: "popper",
visible: _ctx.suggestionVisible,
"onUpdate:visible": _cache[3] || (_cache[3] = $event => (_ctx.suggestionVisible = $event)),
placement: _ctx.placement,
"popper-class": `el-autocomplete__popper ${_ctx.popperClass}`,
"append-to-body": _ctx.popperAppendToBody,
pure: "",
"manual-mode": "",
effect: "light",
trigger: "click",
transition: "el-zoom-in-top",
"gpu-acceleration": false
}, {
trigger: withCtx(() => [
withDirectives(createVNode("div", {
class: ['el-autocomplete', _ctx.$attrs.class],
style: _ctx.$attrs.style,
role: "combobox",
"aria-haspopup": "listbox",
"aria-expanded": _ctx.suggestionVisible,
"aria-owns": _ctx.id
}, [
createVNode(_component_el_input, mergeProps({ ref: "inputRef" }, _ctx.attrs, {
"model-value": _ctx.modelValue,
onInput: _ctx.handleInput,
onChange: _ctx.handleChange,
onFocus: _ctx.handleFocus,
onBlur: _ctx.handleBlur,
onClear: _ctx.handleClear,
onKeydown: [
_cache[1] || (_cache[1] = withKeys(withModifiers($event => (_ctx.highlight(_ctx.highlightedIndex - 1)), ["prevent"]), ["up"])),
_cache[2] || (_cache[2] = withKeys(withModifiers($event => (_ctx.highlight(_ctx.highlightedIndex + 1)), ["prevent"]), ["down"])),
withKeys(withModifiers(_ctx.handleKeyEnter, ["prevent"]), ["enter"]),
withKeys(withModifiers(_ctx.close, ["prevent"]), ["tab"])
]
}), createSlots({ _: 2 /* DYNAMIC */ }, [
(_ctx.$slots.prepend)
? {
name: "prepend",
fn: withCtx(() => [
renderSlot(_ctx.$slots, "prepend")
])
}
: undefined,
(_ctx.$slots.append)
? {
name: "append",
fn: withCtx(() => [
renderSlot(_ctx.$slots, "append")
])
}
: undefined,
(_ctx.$slots.prefix)
? {
name: "prefix",
fn: withCtx(() => [
renderSlot(_ctx.$slots, "prefix")
])
}
: undefined,
(_ctx.$slots.suffix)
? {
name: "suffix",
fn: withCtx(() => [
renderSlot(_ctx.$slots, "suffix")
])
}
: undefined
]), 1040 /* FULL_PROPS, DYNAMIC_SLOTS */, ["model-value", "onInput", "onChange", "onFocus", "onBlur", "onClear", "onKeydown"])
], 14 /* CLASS, STYLE, PROPS */, ["aria-expanded", "aria-owns"]), [
[_directive_clickoutside, _ctx.close]
])
]),
default: withCtx(() => [
createVNode("div", {
ref: "regionRef",
class: ['el-autocomplete-suggestion', _ctx.suggestionLoading && 'is-loading'],
style: { width: _ctx.dropdownWidth, outline: 'none' },
role: "region"
}, [
createVNode(_component_el_scrollbar, {
tag: "ul",
"wrap-class": "el-autocomplete-suggestion__wrap",
"view-class": "el-autocomplete-suggestion__list"
}, {
default: withCtx(() => [
(_ctx.suggestionLoading)
? (openBlock(), createBlock("li", _hoisted_1, [
_hoisted_2
]))
: (openBlock(true), createBlock(Fragment, { key: 1 }, renderList(_ctx.suggestions, (item, index) => {
return (openBlock(), createBlock("li", {
id: `${_ctx.id}-item-${index}`,
key: index,
class: {'highlighted': _ctx.highlightedIndex === index},
role: "option",
"aria-selected": _ctx.highlightedIndex === index,
onClick: $event => (_ctx.select(item))
}, [
renderSlot(_ctx.$slots, "default", { item: item }, () => [
createTextVNode(toDisplayString(item[_ctx.valueKey]), 1 /* TEXT */)
])
], 10 /* CLASS, PROPS */, ["id", "aria-selected", "onClick"]))
}), 128 /* KEYED_FRAGMENT */))
]),
_: 1 /* STABLE */
})
], 6 /* CLASS, STYLE */)
]),
_: 1 /* STABLE */
}, 8 /* PROPS */, ["visible", "placement", "popper-class", "append-to-body"]))
}
script.render = render;
script.__file = "packages/autocomplete/src/index.vue";
script.install = (app) => {
app.component(script.name, script);
};
const _Autocomplete = script;
export default _Autocomplete;