|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778 |
- // Styles
- import "../../../src/components/VTextField/VTextField.sass";
- import "../../../src/components/VSelect/VSelect.sass"; // Components
-
- import VChip from '../VChip';
- import VMenu from '../VMenu';
- import VSelectList from './VSelectList'; // Extensions
-
- import VInput from '../VInput';
- import VTextField from '../VTextField/VTextField'; // Mixins
-
- import Comparable from '../../mixins/comparable';
- import Filterable from '../../mixins/filterable'; // Directives
-
- import ClickOutside from '../../directives/click-outside'; // Utilities
-
- import mergeData from '../../util/mergeData';
- import { getPropertyFromItem, getObjectValueByPath, keyCodes } from '../../util/helpers';
- import { consoleError } from '../../util/console'; // Types
-
- import mixins from '../../util/mixins';
- export const defaultMenuProps = {
- closeOnClick: false,
- closeOnContentClick: false,
- disableKeys: true,
- openOnClick: false,
- maxHeight: 304
- }; // Types
-
- const baseMixins = mixins(VTextField, Comparable, Filterable);
- /* @vue/component */
-
- export default baseMixins.extend().extend({
- name: 'v-select',
- directives: {
- ClickOutside
- },
- props: {
- appendIcon: {
- type: String,
- default: '$dropdown'
- },
- attach: {
- type: null,
- default: false
- },
- cacheItems: Boolean,
- chips: Boolean,
- clearable: Boolean,
- deletableChips: Boolean,
- disableLookup: Boolean,
- eager: Boolean,
- hideSelected: Boolean,
- items: {
- type: Array,
- default: () => []
- },
- itemColor: {
- type: String,
- default: 'primary'
- },
- itemDisabled: {
- type: [String, Array, Function],
- default: 'disabled'
- },
- itemText: {
- type: [String, Array, Function],
- default: 'text'
- },
- itemValue: {
- type: [String, Array, Function],
- default: 'value'
- },
- menuProps: {
- type: [String, Array, Object],
- default: () => defaultMenuProps
- },
- multiple: Boolean,
- openOnClear: Boolean,
- returnObject: Boolean,
- smallChips: Boolean
- },
-
- data() {
- return {
- cachedItems: this.cacheItems ? this.items : [],
- menuIsBooted: false,
- isMenuActive: false,
- lastItem: 20,
- // As long as a value is defined, show it
- // Otherwise, check if multiple
- // to determine which default to provide
- lazyValue: this.value !== undefined ? this.value : this.multiple ? [] : undefined,
- selectedIndex: -1,
- selectedItems: [],
- keyboardLookupPrefix: '',
- keyboardLookupLastTime: 0
- };
- },
-
- computed: {
- /* All items that the select has */
- allItems() {
- return this.filterDuplicates(this.cachedItems.concat(this.items));
- },
-
- classes() {
- return { ...VTextField.options.computed.classes.call(this),
- 'v-select': true,
- 'v-select--chips': this.hasChips,
- 'v-select--chips--small': this.smallChips,
- 'v-select--is-menu-active': this.isMenuActive,
- 'v-select--is-multi': this.multiple
- };
- },
-
- /* Used by other components to overwrite */
- computedItems() {
- return this.allItems;
- },
-
- computedOwns() {
- return `list-${this._uid}`;
- },
-
- computedCounterValue() {
- return this.multiple ? this.selectedItems.length : (this.getText(this.selectedItems[0]) || '').toString().length;
- },
-
- directives() {
- return this.isFocused ? [{
- name: 'click-outside',
- value: this.blur,
- args: {
- closeConditional: this.closeConditional
- }
- }] : undefined;
- },
-
- dynamicHeight() {
- return 'auto';
- },
-
- hasChips() {
- return this.chips || this.smallChips;
- },
-
- hasSlot() {
- return Boolean(this.hasChips || this.$scopedSlots.selection);
- },
-
- isDirty() {
- return this.selectedItems.length > 0;
- },
-
- listData() {
- const scopeId = this.$vnode && this.$vnode.context.$options._scopeId;
- const attrs = scopeId ? {
- [scopeId]: true
- } : {};
- return {
- attrs: { ...attrs,
- id: this.computedOwns
- },
- props: {
- action: this.multiple,
- color: this.itemColor,
- dense: this.dense,
- hideSelected: this.hideSelected,
- items: this.virtualizedItems,
- itemDisabled: this.itemDisabled,
- itemText: this.itemText,
- itemValue: this.itemValue,
- noDataText: this.$vuetify.lang.t(this.noDataText),
- selectedItems: this.selectedItems
- },
- on: {
- select: this.selectItem
- },
- scopedSlots: {
- item: this.$scopedSlots.item
- }
- };
- },
-
- staticList() {
- if (this.$slots['no-data'] || this.$slots['prepend-item'] || this.$slots['append-item']) {
- consoleError('assert: staticList should not be called if slots are used');
- }
-
- return this.$createElement(VSelectList, this.listData);
- },
-
- virtualizedItems() {
- return this.$_menuProps.auto ? this.computedItems : this.computedItems.slice(0, this.lastItem);
- },
-
- menuCanShow: () => true,
-
- $_menuProps() {
- let normalisedProps = typeof this.menuProps === 'string' ? this.menuProps.split(',') : this.menuProps;
-
- if (Array.isArray(normalisedProps)) {
- normalisedProps = normalisedProps.reduce((acc, p) => {
- acc[p.trim()] = true;
- return acc;
- }, {});
- }
-
- return { ...defaultMenuProps,
- eager: this.eager,
- value: this.menuCanShow && this.isMenuActive,
- nudgeBottom: normalisedProps.offsetY ? 1 : 0,
- ...normalisedProps
- };
- }
-
- },
- watch: {
- internalValue(val) {
- this.initialValue = val;
- this.setSelectedItems();
- },
-
- menuIsBooted() {
- window.setTimeout(() => {
- if (this.getContent() && this.getContent().addEventListener) {
- this.getContent().addEventListener('scroll', this.onScroll, false);
- }
- });
- },
-
- isMenuActive(val) {
- window.setTimeout(() => this.onMenuActiveChange(val));
- if (!val) return;
- this.menuIsBooted = true;
- },
-
- items: {
- immediate: true,
-
- handler(val) {
- if (this.cacheItems) {
- // Breaks vue-test-utils if
- // this isn't calculated
- // on the next tick
- this.$nextTick(() => {
- this.cachedItems = this.filterDuplicates(this.cachedItems.concat(val));
- });
- }
-
- this.setSelectedItems();
- }
-
- }
- },
- methods: {
- /** @public */
- blur(e) {
- VTextField.options.methods.blur.call(this, e);
- this.isMenuActive = false;
- this.isFocused = false;
- this.selectedIndex = -1;
- },
-
- /** @public */
- activateMenu() {
- if (this.disabled || this.readonly || this.isMenuActive) return;
- this.isMenuActive = true;
- },
-
- clearableCallback() {
- this.setValue(this.multiple ? [] : undefined);
- this.setMenuIndex(-1);
- this.$nextTick(() => this.$refs.input && this.$refs.input.focus());
- if (this.openOnClear) this.isMenuActive = true;
- },
-
- closeConditional(e) {
- if (!this.isMenuActive) return true;
- return !this._isDestroyed && ( // Click originates from outside the menu content
- // Multiple selects don't close when an item is clicked
- !this.getContent() || !this.getContent().contains(e.target)) && // Click originates from outside the element
- this.$el && !this.$el.contains(e.target) && e.target !== this.$el;
- },
-
- filterDuplicates(arr) {
- const uniqueValues = new Map();
-
- for (let index = 0; index < arr.length; ++index) {
- const item = arr[index];
- const val = this.getValue(item); // TODO: comparator
-
- !uniqueValues.has(val) && uniqueValues.set(val, item);
- }
-
- return Array.from(uniqueValues.values());
- },
-
- findExistingIndex(item) {
- const itemValue = this.getValue(item);
- return (this.internalValue || []).findIndex(i => this.valueComparator(this.getValue(i), itemValue));
- },
-
- getContent() {
- return this.$refs.menu && this.$refs.menu.$refs.content;
- },
-
- genChipSelection(item, index) {
- const isDisabled = this.disabled || this.readonly || this.getDisabled(item);
- return this.$createElement(VChip, {
- staticClass: 'v-chip--select',
- attrs: {
- tabindex: -1
- },
- props: {
- close: this.deletableChips && !isDisabled,
- disabled: isDisabled,
- inputValue: index === this.selectedIndex,
- small: this.smallChips
- },
- on: {
- click: e => {
- if (isDisabled) return;
- e.stopPropagation();
- this.selectedIndex = index;
- },
- 'click:close': () => this.onChipInput(item)
- },
- key: JSON.stringify(this.getValue(item))
- }, this.getText(item));
- },
-
- genCommaSelection(item, index, last) {
- const color = index === this.selectedIndex && this.computedColor;
- const isDisabled = this.disabled || this.getDisabled(item);
- return this.$createElement('div', this.setTextColor(color, {
- staticClass: 'v-select__selection v-select__selection--comma',
- class: {
- 'v-select__selection--disabled': isDisabled
- },
- key: JSON.stringify(this.getValue(item))
- }), `${this.getText(item)}${last ? '' : ', '}`);
- },
-
- genDefaultSlot() {
- const selections = this.genSelections();
- const input = this.genInput(); // If the return is an empty array
- // push the input
-
- if (Array.isArray(selections)) {
- selections.push(input); // Otherwise push it into children
- } else {
- selections.children = selections.children || [];
- selections.children.push(input);
- }
-
- return [this.genFieldset(), this.$createElement('div', {
- staticClass: 'v-select__slot',
- directives: this.directives
- }, [this.genLabel(), this.prefix ? this.genAffix('prefix') : null, selections, this.suffix ? this.genAffix('suffix') : null, this.genClearIcon(), this.genIconSlot(), this.genHiddenInput()]), this.genMenu(), this.genProgress()];
- },
-
- genIcon(type, cb, extraData) {
- const icon = VInput.options.methods.genIcon.call(this, type, cb, extraData);
-
- if (type === 'append') {
- // Don't allow the dropdown icon to be focused
- icon.children[0].data = mergeData(icon.children[0].data, {
- attrs: {
- tabindex: icon.children[0].componentOptions.listeners && '-1',
- 'aria-hidden': 'true',
- 'aria-label': undefined
- }
- });
- }
-
- return icon;
- },
-
- genInput() {
- const input = VTextField.options.methods.genInput.call(this);
- delete input.data.attrs.name;
- input.data = mergeData(input.data, {
- domProps: {
- value: null
- },
- attrs: {
- readonly: true,
- type: 'text',
- 'aria-readonly': String(this.readonly),
- 'aria-activedescendant': getObjectValueByPath(this.$refs.menu, 'activeTile.id'),
- autocomplete: getObjectValueByPath(input.data, 'attrs.autocomplete', 'off')
- },
- on: {
- keypress: this.onKeyPress
- }
- });
- return input;
- },
-
- genHiddenInput() {
- return this.$createElement('input', {
- domProps: {
- value: this.lazyValue
- },
- attrs: {
- type: 'hidden',
- name: this.attrs$.name
- }
- });
- },
-
- genInputSlot() {
- const render = VTextField.options.methods.genInputSlot.call(this);
- render.data.attrs = { ...render.data.attrs,
- role: 'button',
- 'aria-haspopup': 'listbox',
- 'aria-expanded': String(this.isMenuActive),
- 'aria-owns': this.computedOwns
- };
- return render;
- },
-
- genList() {
- // If there's no slots, we can use a cached VNode to improve performance
- if (this.$slots['no-data'] || this.$slots['prepend-item'] || this.$slots['append-item']) {
- return this.genListWithSlot();
- } else {
- return this.staticList;
- }
- },
-
- genListWithSlot() {
- const slots = ['prepend-item', 'no-data', 'append-item'].filter(slotName => this.$slots[slotName]).map(slotName => this.$createElement('template', {
- slot: slotName
- }, this.$slots[slotName])); // Requires destructuring due to Vue
- // modifying the `on` property when passed
- // as a referenced object
-
- return this.$createElement(VSelectList, { ...this.listData
- }, slots);
- },
-
- genMenu() {
- const props = this.$_menuProps;
- props.activator = this.$refs['input-slot']; // Attach to root el so that
- // menu covers prepend/append icons
-
- if ( // TODO: make this a computed property or helper or something
- this.attach === '' || // If used as a boolean prop (<v-menu attach>)
- this.attach === true || // If bound to a boolean (<v-menu :attach="true">)
- this.attach === 'attach' // If bound as boolean prop in pug (v-menu(attach))
- ) {
- props.attach = this.$el;
- } else {
- props.attach = this.attach;
- }
-
- return this.$createElement(VMenu, {
- attrs: {
- role: undefined,
- offsetY: true
- },
- props,
- on: {
- input: val => {
- this.isMenuActive = val;
- this.isFocused = val;
- }
- },
- ref: 'menu'
- }, [this.genList()]);
- },
-
- genSelections() {
- let length = this.selectedItems.length;
- const children = new Array(length);
- let genSelection;
-
- if (this.$scopedSlots.selection) {
- genSelection = this.genSlotSelection;
- } else if (this.hasChips) {
- genSelection = this.genChipSelection;
- } else {
- genSelection = this.genCommaSelection;
- }
-
- while (length--) {
- children[length] = genSelection(this.selectedItems[length], length, length === children.length - 1);
- }
-
- return this.$createElement('div', {
- staticClass: 'v-select__selections'
- }, children);
- },
-
- genSlotSelection(item, index) {
- return this.$scopedSlots.selection({
- attrs: {
- class: 'v-chip--select'
- },
- parent: this,
- item,
- index,
- select: e => {
- e.stopPropagation();
- this.selectedIndex = index;
- },
- selected: index === this.selectedIndex,
- disabled: this.disabled || this.readonly
- });
- },
-
- getMenuIndex() {
- return this.$refs.menu ? this.$refs.menu.listIndex : -1;
- },
-
- getDisabled(item) {
- return getPropertyFromItem(item, this.itemDisabled, false);
- },
-
- getText(item) {
- return getPropertyFromItem(item, this.itemText, item);
- },
-
- getValue(item) {
- return getPropertyFromItem(item, this.itemValue, this.getText(item));
- },
-
- onBlur(e) {
- e && this.$emit('blur', e);
- },
-
- onChipInput(item) {
- if (this.multiple) this.selectItem(item);else this.setValue(null); // If all items have been deleted,
- // open `v-menu`
-
- if (this.selectedItems.length === 0) {
- this.isMenuActive = true;
- } else {
- this.isMenuActive = false;
- }
-
- this.selectedIndex = -1;
- },
-
- onClick(e) {
- if (this.isDisabled) return;
-
- if (!this.isAppendInner(e.target)) {
- this.isMenuActive = true;
- }
-
- if (!this.isFocused) {
- this.isFocused = true;
- this.$emit('focus');
- }
-
- this.$emit('click', e);
- },
-
- onEscDown(e) {
- e.preventDefault();
-
- if (this.isMenuActive) {
- e.stopPropagation();
- this.isMenuActive = false;
- }
- },
-
- onKeyPress(e) {
- if (this.multiple || this.readonly || this.disableLookup) return;
- const KEYBOARD_LOOKUP_THRESHOLD = 1000; // milliseconds
-
- const now = performance.now();
-
- if (now - this.keyboardLookupLastTime > KEYBOARD_LOOKUP_THRESHOLD) {
- this.keyboardLookupPrefix = '';
- }
-
- this.keyboardLookupPrefix += e.key.toLowerCase();
- this.keyboardLookupLastTime = now;
- const index = this.allItems.findIndex(item => {
- const text = (this.getText(item) || '').toString();
- return text.toLowerCase().startsWith(this.keyboardLookupPrefix);
- });
- const item = this.allItems[index];
-
- if (index !== -1) {
- this.lastItem = Math.max(this.lastItem, index + 5);
- this.setValue(this.returnObject ? item : this.getValue(item));
- this.$nextTick(() => this.$refs.menu.getTiles());
- setTimeout(() => this.setMenuIndex(index));
- }
- },
-
- onKeyDown(e) {
- if (this.readonly && e.keyCode !== keyCodes.tab) return;
- const keyCode = e.keyCode;
- const menu = this.$refs.menu; // If enter, space, open menu
-
- if ([keyCodes.enter, keyCodes.space].includes(keyCode)) this.activateMenu();
- this.$emit('keydown', e);
- if (!menu) return; // If menu is active, allow default
- // listIndex change from menu
-
- if (this.isMenuActive && keyCode !== keyCodes.tab) {
- this.$nextTick(() => {
- menu.changeListIndex(e);
- this.$emit('update:list-index', menu.listIndex);
- });
- } // If menu is not active, up and down can do
- // one of 2 things. If multiple, opens the
- // menu, if not, will cycle through all
- // available options
-
-
- if (!this.isMenuActive && [keyCodes.up, keyCodes.down].includes(keyCode)) return this.onUpDown(e); // If escape deactivate the menu
-
- if (keyCode === keyCodes.esc) return this.onEscDown(e); // If tab - select item or close menu
-
- if (keyCode === keyCodes.tab) return this.onTabDown(e); // If space preventDefault
-
- if (keyCode === keyCodes.space) return this.onSpaceDown(e);
- },
-
- onMenuActiveChange(val) {
- // If menu is closing and mulitple
- // or menuIndex is already set
- // skip menu index recalculation
- if (this.multiple && !val || this.getMenuIndex() > -1) return;
- const menu = this.$refs.menu;
- if (!menu || !this.isDirty) return; // When menu opens, set index of first active item
-
- for (let i = 0; i < menu.tiles.length; i++) {
- if (menu.tiles[i].getAttribute('aria-selected') === 'true') {
- this.setMenuIndex(i);
- break;
- }
- }
- },
-
- onMouseUp(e) {
- if (this.hasMouseDown && e.which !== 3 && !this.isDisabled) {
- // If append inner is present
- // and the target is itself
- // or inside, toggle menu
- if (this.isAppendInner(e.target)) {
- this.$nextTick(() => this.isMenuActive = !this.isMenuActive); // If user is clicking in the container
- // and field is enclosed, activate it
- } else if (this.isEnclosed) {
- this.isMenuActive = true;
- }
- }
-
- VTextField.options.methods.onMouseUp.call(this, e);
- },
-
- onScroll() {
- if (!this.isMenuActive) {
- requestAnimationFrame(() => this.getContent().scrollTop = 0);
- } else {
- if (this.lastItem >= this.computedItems.length) return;
- const showMoreItems = this.getContent().scrollHeight - (this.getContent().scrollTop + this.getContent().clientHeight) < 200;
-
- if (showMoreItems) {
- this.lastItem += 20;
- }
- }
- },
-
- onSpaceDown(e) {
- e.preventDefault();
- },
-
- onTabDown(e) {
- const menu = this.$refs.menu;
- if (!menu) return;
- const activeTile = menu.activeTile; // An item that is selected by
- // menu-index should toggled
-
- if (!this.multiple && activeTile && this.isMenuActive) {
- e.preventDefault();
- e.stopPropagation();
- activeTile.click();
- } else {
- // If we make it here,
- // the user has no selected indexes
- // and is probably tabbing out
- this.blur(e);
- }
- },
-
- onUpDown(e) {
- const menu = this.$refs.menu;
- if (!menu) return;
- e.preventDefault(); // Multiple selects do not cycle their value
- // when pressing up or down, instead activate
- // the menu
-
- if (this.multiple) return this.activateMenu();
- const keyCode = e.keyCode; // Cycle through available values to achieve
- // select native behavior
-
- menu.isBooted = true;
- window.requestAnimationFrame(() => {
- menu.getTiles();
- keyCodes.up === keyCode ? menu.prevTile() : menu.nextTile();
- menu.activeTile && menu.activeTile.click();
- });
- },
-
- selectItem(item) {
- if (!this.multiple) {
- this.setValue(this.returnObject ? item : this.getValue(item));
- this.isMenuActive = false;
- } else {
- const internalValue = (this.internalValue || []).slice();
- const i = this.findExistingIndex(item);
- i !== -1 ? internalValue.splice(i, 1) : internalValue.push(item);
- this.setValue(internalValue.map(i => {
- return this.returnObject ? i : this.getValue(i);
- })); // When selecting multiple
- // adjust menu after each
- // selection
-
- this.$nextTick(() => {
- this.$refs.menu && this.$refs.menu.updateDimensions();
- }); // We only need to reset list index for multiple
- // to keep highlight when an item is toggled
- // on and off
-
- if (!this.multiple) return;
- const listIndex = this.getMenuIndex();
- this.setMenuIndex(-1); // There is no item to re-highlight
- // when selections are hidden
-
- if (this.hideSelected) return;
- this.$nextTick(() => this.setMenuIndex(listIndex));
- }
- },
-
- setMenuIndex(index) {
- this.$refs.menu && (this.$refs.menu.listIndex = index);
- },
-
- setSelectedItems() {
- const selectedItems = [];
- const values = !this.multiple || !Array.isArray(this.internalValue) ? [this.internalValue] : this.internalValue;
-
- for (const value of values) {
- const index = this.allItems.findIndex(v => this.valueComparator(this.getValue(v), this.getValue(value)));
-
- if (index > -1) {
- selectedItems.push(this.allItems[index]);
- }
- }
-
- this.selectedItems = selectedItems;
- },
-
- setValue(value) {
- const oldValue = this.internalValue;
- this.internalValue = value;
- value !== oldValue && this.$emit('change', value);
- },
-
- isAppendInner(target) {
- // return true if append inner is present
- // and the target is itself or inside
- const appendInner = this.$refs['append-inner'];
- return appendInner && (appendInner === target || appendInner.contains(target));
- }
-
- }
- });
- //# sourceMappingURL=VSelect.js.map
|