1
// Styles
2
import '../VTextField/VTextField.sass'
3
import './VSelect.sass'
4

5
// Components
6
import VChip from '../VChip'
7
import VMenu from '../VMenu'
8
import VSelectList from './VSelectList'
9

10
// Extensions
11
import VInput from '../VInput'
12
import VTextField from '../VTextField/VTextField'
13

14
// Mixins
15
import Comparable from '../../mixins/comparable'
16
import Dependent from '../../mixins/dependent'
17
import Filterable from '../../mixins/filterable'
18

19
// Directives
20
import ClickOutside from '../../directives/click-outside'
21

22
// Utilities
23
import mergeData from '../../util/mergeData'
24
import { getPropertyFromItem, getObjectValueByPath, keyCodes } from '../../util/helpers'
25
import { consoleError } from '../../util/console'
26

27
// Types
28
import mixins from '../../util/mixins'
29
import { VNode, VNodeDirective, PropType, VNodeData } from 'vue'
30
import { PropValidator } from 'vue/types/options'
31
import { SelectItemKey } from 'vuetify/types'
32

33 1
export const defaultMenuProps = {
34
  closeOnClick: false,
35
  closeOnContentClick: false,
36
  disableKeys: true,
37
  openOnClick: false,
38
  maxHeight: 304,
39
}
40

41
// Types
42 1
const baseMixins = mixins(
43
  VTextField,
44
  Comparable,
45
  Dependent,
46
  Filterable
47
)
48

49
interface options extends InstanceType<typeof baseMixins> {
50
  $refs: {
51
    menu: InstanceType<typeof VMenu>
52
    content: HTMLElement
53
    label: HTMLElement
54
    input: HTMLInputElement
55
    'prepend-inner': HTMLElement
56
    'append-inner': HTMLElement
57
    prefix: HTMLElement
58
    suffix: HTMLElement
59
  }
60
}
61

62
/* @vue/component */
63
export default baseMixins.extend<options>().extend({
64
  name: 'v-select',
65

66
  directives: {
67
    ClickOutside,
68
  },
69

70
  props: {
71
    appendIcon: {
72
      type: String,
73
      default: '$dropdown',
74
    },
75
    attach: {
76
      type: null as unknown as PropType<string | boolean | Element | VNode>,
77
      default: false,
78
    },
79
    cacheItems: Boolean,
80
    chips: Boolean,
81
    clearable: Boolean,
82
    deletableChips: Boolean,
83
    disableLookup: Boolean,
84
    eager: Boolean,
85
    hideSelected: Boolean,
86
    items: {
87
      type: Array,
88 1
      default: () => [],
89
    } as PropValidator<any[]>,
90
    itemColor: {
91
      type: String,
92
      default: 'primary',
93
    },
94
    itemDisabled: {
95
      type: [String, Array, Function] as PropType<SelectItemKey>,
96
      default: 'disabled',
97
    },
98
    itemText: {
99
      type: [String, Array, Function] as PropType<SelectItemKey>,
100
      default: 'text',
101
    },
102
    itemValue: {
103
      type: [String, Array, Function] as PropType<SelectItemKey>,
104
      default: 'value',
105
    },
106
    menuProps: {
107
      type: [String, Array, Object],
108 1
      default: () => defaultMenuProps,
109
    },
110
    multiple: Boolean,
111
    openOnClear: Boolean,
112
    returnObject: Boolean,
113
    smallChips: Boolean,
114
  },
115

116 1
  data () {
117 1
    return {
118 1
      cachedItems: this.cacheItems ? this.items : [],
119
      menuIsBooted: false,
120
      isMenuActive: false,
121
      lastItem: 20,
122
      // As long as a value is defined, show it
123
      // Otherwise, check if multiple
124
      // to determine which default to provide
125 1
      lazyValue: this.value !== undefined
126 1
        ? this.value
127 1
        : this.multiple ? [] : undefined,
128
      selectedIndex: -1,
129
      selectedItems: [] as any[],
130
      keyboardLookupPrefix: '',
131
      keyboardLookupLastTime: 0,
132
    }
133
  },
134

135
  computed: {
136
    /* All items that the select has */
137 1
    allItems (): object[] {
138 1
      return this.filterDuplicates(this.cachedItems.concat(this.items))
139
    },
140 1
    classes (): object {
141 1
      return {
142
        ...VTextField.options.computed.classes.call(this),
143
        'v-select': true,
144
        'v-select--chips': this.hasChips,
145
        'v-select--chips--small': this.smallChips,
146
        'v-select--is-menu-active': this.isMenuActive,
147
        'v-select--is-multi': this.multiple,
148
      }
149
    },
150
    /* Used by other components to overwrite */
151 1
    computedItems (): object[] {
152 1
      return this.allItems
153
    },
154 1
    computedOwns (): string {
155 1
      return `list-${this._uid}`
156
    },
157 0
    computedCounterValue (): number {
158 1
      return this.multiple
159 0
        ? this.selectedItems.length
160 1
        : (this.getText(this.selectedItems[0]) || '').toString().length
161
    },
162 1
    directives (): VNodeDirective[] | undefined {
163 1
      return this.isFocused ? [{
164
        name: 'click-outside',
165
        value: {
166
          handler: this.blur,
167
          closeConditional: this.closeConditional,
168 1
          include: () => this.getOpenDependentElements(),
169
        },
170 1
      }] : undefined
171
    },
172 0
    dynamicHeight () {
173 0
      return 'auto'
174
    },
175 1
    hasChips (): boolean {
176 1
      return this.chips || this.smallChips
177
    },
178 1
    hasSlot (): boolean {
179 1
      return Boolean(this.hasChips || this.$scopedSlots.selection)
180
    },
181 1
    isDirty (): boolean {
182 1
      return this.selectedItems.length > 0
183
    },
184 1
    listData (): object {
185 1
      const scopeId = this.$vnode && (this.$vnode.context!.$options as { [key: string]: any })._scopeId
186 1
      const attrs = scopeId ? {
187
        [scopeId]: true,
188 1
      } : {}
189

190 1
      return {
191
        attrs: {
192
          ...attrs,
193
          id: this.computedOwns,
194
        },
195
        props: {
196
          action: this.multiple,
197
          color: this.itemColor,
198
          dense: this.dense,
199
          hideSelected: this.hideSelected,
200
          items: this.virtualizedItems,
201
          itemDisabled: this.itemDisabled,
202
          itemText: this.itemText,
203
          itemValue: this.itemValue,
204
          noDataText: this.$vuetify.lang.t(this.noDataText),
205
          selectedItems: this.selectedItems,
206
        },
207
        on: {
208
          select: this.selectItem,
209
        },
210
        scopedSlots: {
211
          item: this.$scopedSlots.item,
212
        },
213
      }
214
    },
215 1
    staticList (): VNode {
216 1
      if (this.$slots['no-data'] || this.$slots['prepend-item'] || this.$slots['append-item']) {
217 0
        consoleError('assert: staticList should not be called if slots are used')
218
      }
219

220 1
      return this.$createElement(VSelectList, this.listData)
221
    },
222 1
    virtualizedItems (): object[] {
223 1
      return (this.$_menuProps as any).auto
224 1
        ? this.computedItems
225 1
        : this.computedItems.slice(0, this.lastItem)
226
    },
227 1
    menuCanShow: () => true,
228 1
    $_menuProps (): object {
229 1
      let normalisedProps = typeof this.menuProps === 'string'
230 1
        ? this.menuProps.split(',')
231 1
        : this.menuProps
232

233 1
      if (Array.isArray(normalisedProps)) {
234 1
        normalisedProps = normalisedProps.reduce((acc, p) => {
235 1
          acc[p.trim()] = true
236 1
          return acc
237
        }, {})
238
      }
239

240 1
      return {
241
        ...defaultMenuProps,
242
        eager: this.eager,
243 1
        value: this.menuCanShow && this.isMenuActive,
244 1
        nudgeBottom: normalisedProps.offsetY ? 1 : 0, // convert to int
245
        ...normalisedProps,
246
      }
247
    },
248
  },
249

250
  watch: {
251 1
    internalValue (val) {
252 1
      this.initialValue = val
253 1
      this.setSelectedItems()
254
    },
255 1
    isMenuActive (val) {
256 1
      window.setTimeout(() => this.onMenuActiveChange(val))
257
    },
258
    items: {
259
      immediate: true,
260 1
      handler (val) {
261 1
        if (this.cacheItems) {
262
          // Breaks vue-test-utils if
263
          // this isn't calculated
264
          // on the next tick
265 1
          this.$nextTick(() => {
266 1
            this.cachedItems = this.filterDuplicates(this.cachedItems.concat(val))
267
          })
268
        }
269

270 1
        this.setSelectedItems()
271
      },
272
    },
273
  },
274

275
  methods: {
276
    /** @public */
277 1
    blur (e?: Event) {
278 1
      VTextField.options.methods.blur.call(this, e)
279 1
      this.isMenuActive = false
280 1
      this.isFocused = false
281 1
      this.selectedIndex = -1
282
    },
283
    /** @public */
284 1
    activateMenu () {
285 1
      if (
286 1
        !this.isInteractive ||
287 1
        this.isMenuActive
288 1
      ) return
289

290 1
      this.isMenuActive = true
291
    },
292 1
    clearableCallback () {
293 1
      this.setValue(this.multiple ? [] : undefined)
294 1
      this.setMenuIndex(-1)
295 1
      this.$nextTick(() => this.$refs.input && this.$refs.input.focus())
296

297 1
      if (this.openOnClear) this.isMenuActive = true
298
    },
299 1
    closeConditional (e: Event) {
300 1
      if (!this.isMenuActive) return true
301

302 1
      return (
303 1
        !this._isDestroyed &&
304

305
        // Click originates from outside the menu content
306
        // Multiple selects don't close when an item is clicked
307 1
        (!this.getContent() ||
308 1
        !this.getContent().contains(e.target as Node)) &&
309

310
        // Click originates from outside the element
311 1
        this.$el &&
312 1
        !this.$el.contains(e.target as Node) &&
313 1
        e.target !== this.$el
314
      )
315
    },
316 1
    filterDuplicates (arr: any[]) {
317 1
      const uniqueValues = new Map()
318 1
      for (let index = 0; index < arr.length; ++index) {
319 1
        const item = arr[index]
320

321
        // Do not deduplicate headers or dividers (#12517)
322 1
        if (item.header || item.divider) {
323 0
          uniqueValues.set(item, item)
324 0
          continue
325
        }
326

327 1
        const val = this.getValue(item)
328

329
        // TODO: comparator
330 1
        !uniqueValues.has(val) && uniqueValues.set(val, item)
331
      }
332 1
      return Array.from(uniqueValues.values())
333
    },
334 1
    findExistingIndex (item: object) {
335 1
      const itemValue = this.getValue(item)
336

337 1
      return (this.internalValue || []).findIndex((i: object) => this.valueComparator(this.getValue(i), itemValue))
338
    },
339 1
    getContent () {
340 1
      return this.$refs.menu && this.$refs.menu.$refs.content
341
    },
342 1
    genChipSelection (item: object, index: number) {
343
      const isDisabled = (
344 1
        !this.isInteractive ||
345 1
        this.getDisabled(item)
346
      )
347

348 1
      return this.$createElement(VChip, {
349
        staticClass: 'v-chip--select',
350
        attrs: { tabindex: -1 },
351
        props: {
352 1
          close: this.deletableChips && !isDisabled,
353
          disabled: isDisabled,
354
          inputValue: index === this.selectedIndex,
355
          small: this.smallChips,
356
        },
357
        on: {
358 1
          click: (e: MouseEvent) => {
359 1
            if (isDisabled) return
360

361 1
            e.stopPropagation()
362

363 1
            this.selectedIndex = index
364
          },
365 1
          'click:close': () => this.onChipInput(item),
366
        },
367
        key: JSON.stringify(this.getValue(item)),
368
      }, this.getText(item))
369
    },
370 1
    genCommaSelection (item: object, index: number, last: boolean) {
371 1
      const color = index === this.selectedIndex && this.computedColor
372
      const isDisabled = (
373 1
        !this.isInteractive ||
374 1
        this.getDisabled(item)
375
      )
376

377 1
      return this.$createElement('div', this.setTextColor(color, {
378
        staticClass: 'v-select__selection v-select__selection--comma',
379
        class: {
380
          'v-select__selection--disabled': isDisabled,
381
        },
382
        key: JSON.stringify(this.getValue(item)),
383 1
      }), `${this.getText(item)}${last ? '' : ', '}`)
384
    },
385 1
    genDefaultSlot (): (VNode | VNode[] | null)[] {
386 1
      const selections = this.genSelections()
387 1
      const input = this.genInput()
388

389
      // If the return is an empty array
390
      // push the input
391 1
      if (Array.isArray(selections)) {
392 1
        selections.push(input)
393
      // Otherwise push it into children
394
      } else {
395 1
        selections.children = selections.children || []
396 1
        selections.children.push(input)
397
      }
398

399 1
      return [
400
        this.genFieldset(),
401
        this.$createElement('div', {
402
          staticClass: 'v-select__slot',
403
          directives: this.directives,
404
        }, [
405
          this.genLabel(),
406 1
          this.prefix ? this.genAffix('prefix') : null,
407
          selections,
408 1
          this.suffix ? this.genAffix('suffix') : null,
409
          this.genClearIcon(),
410
          this.genIconSlot(),
411
          this.genHiddenInput(),
412
        ]),
413
        this.genMenu(),
414
        this.genProgress(),
415
      ]
416
    },
417
    genIcon (
418
      type: string,
419
      cb?: (e: Event) => void,
420
      extraData?: VNodeData
421 1
    ) {
422 1
      const icon = VInput.options.methods.genIcon.call(this, type, cb, extraData)
423

424 1
      if (type === 'append') {
425
        // Don't allow the dropdown icon to be focused
426 1
        icon.children![0].data = mergeData(icon.children![0].data!, {
427
          attrs: {
428 1
            tabindex: icon.children![0].componentOptions!.listeners && '-1',
429
            'aria-hidden': 'true',
430
            'aria-label': undefined,
431
          },
432
        })
433
      }
434

435 1
      return icon
436
    },
437 1
    genInput (): VNode {
438 1
      const input = VTextField.options.methods.genInput.call(this)
439

440 1
      delete input.data!.attrs!.name
441

442 1
      input.data = mergeData(input.data!, {
443
        domProps: { value: null },
444
        attrs: {
445
          readonly: true,
446
          type: 'text',
447
          'aria-readonly': String(this.isReadonly),
448
          'aria-activedescendant': getObjectValueByPath(this.$refs.menu, 'activeTile.id'),
449
          autocomplete: getObjectValueByPath(input.data!, 'attrs.autocomplete', 'off'),
450
        },
451
        on: { keypress: this.onKeyPress },
452
      })
453

454 1
      return input
455
    },
456 1
    genHiddenInput (): VNode {
457 1
      return this.$createElement('input', {
458
        domProps: { value: this.lazyValue },
459
        attrs: {
460
          type: 'hidden',
461
          name: this.attrs$.name,
462
        },
463
      })
464
    },
465 1
    genInputSlot (): VNode {
466 1
      const render = VTextField.options.methods.genInputSlot.call(this)
467

468 1
      render.data!.attrs = {
469
        ...render.data!.attrs,
470
        role: 'button',
471
        'aria-haspopup': 'listbox',
472
        'aria-expanded': String(this.isMenuActive),
473
        'aria-owns': this.computedOwns,
474
      }
475

476 1
      return render
477
    },
478 1
    genList (): VNode {
479
      // If there's no slots, we can use a cached VNode to improve performance
480 1
      if (this.$slots['no-data'] || this.$slots['prepend-item'] || this.$slots['append-item']) {
481 1
        return this.genListWithSlot()
482
      } else {
483 1
        return this.staticList
484
      }
485
    },
486 1
    genListWithSlot (): VNode {
487 1
      const slots = ['prepend-item', 'no-data', 'append-item']
488 1
        .filter(slotName => this.$slots[slotName])
489 1
        .map(slotName => this.$createElement('template', {
490
          slot: slotName,
491
        }, this.$slots[slotName]))
492
      // Requires destructuring due to Vue
493
      // modifying the `on` property when passed
494
      // as a referenced object
495 1
      return this.$createElement(VSelectList, {
496
        ...this.listData,
497
      }, slots)
498
    },
499 1
    genMenu (): VNode {
500 1
      const props = this.$_menuProps as any
501 1
      props.activator = this.$refs['input-slot']
502

503
      // Attach to root el so that
504
      // menu covers prepend/append icons
505 1
      if (
506
        // TODO: make this a computed property or helper or something
507 1
        this.attach === '' || // If used as a boolean prop (<v-menu attach>)
508 1
        this.attach === true || // If bound to a boolean (<v-menu :attach="true">)
509 1
        this.attach === 'attach' // If bound as boolean prop in pug (v-menu(attach))
510
      ) {
511 1
        props.attach = this.$el
512
      } else {
513 1
        props.attach = this.attach
514
      }
515

516 1
      return this.$createElement(VMenu, {
517
        attrs: { role: undefined },
518
        props,
519
        on: {
520 1
          input: (val: boolean) => {
521 1
            this.isMenuActive = val
522 1
            this.isFocused = val
523
          },
524
          scroll: this.onScroll,
525
        },
526
        ref: 'menu',
527
      }, [this.genList()])
528
    },
529 1
    genSelections (): VNode {
530 1
      let length = this.selectedItems.length
531 1
      const children = new Array(length)
532

533
      let genSelection
534 1
      if (this.$scopedSlots.selection) {
535 1
        genSelection = this.genSlotSelection
536 1
      } else if (this.hasChips) {
537 1
        genSelection = this.genChipSelection
538
      } else {
539 1
        genSelection = this.genCommaSelection
540
      }
541

542 1
      while (length--) {
543 1
        children[length] = genSelection(
544
          this.selectedItems[length],
545
          length,
546
          length === children.length - 1
547
        )
548
      }
549

550 1
      return this.$createElement('div', {
551
        staticClass: 'v-select__selections',
552
      }, children)
553
    },
554 1
    genSlotSelection (item: object, index: number): VNode[] | undefined {
555 1
      return this.$scopedSlots.selection!({
556
        attrs: {
557
          class: 'v-chip--select',
558
        },
559
        parent: this,
560
        item,
561
        index,
562 0
        select: (e: Event) => {
563 0
          e.stopPropagation()
564 0
          this.selectedIndex = index
565
        },
566
        selected: index === this.selectedIndex,
567
        disabled: !this.isInteractive,
568
      })
569
    },
570 1
    getMenuIndex () {
571 1
      return this.$refs.menu ? (this.$refs.menu as { [key: string]: any }).listIndex : -1
572
    },
573 1
    getDisabled (item: object) {
574 1
      return getPropertyFromItem(item, this.itemDisabled, false)
575
    },
576 1
    getText (item: object) {
577 1
      return getPropertyFromItem(item, this.itemText, item)
578
    },
579 1
    getValue (item: object) {
580 1
      return getPropertyFromItem(item, this.itemValue, this.getText(item))
581
    },
582 1
    onBlur (e?: Event) {
583 1
      e && this.$emit('blur', e)
584
    },
585 1
    onChipInput (item: object) {
586 1
      if (this.multiple) this.selectItem(item)
587 0
      else this.setValue(null)
588
      // If all items have been deleted,
589
      // open `v-menu`
590 1
      if (this.selectedItems.length === 0) {
591 0
        this.isMenuActive = true
592
      } else {
593 1
        this.isMenuActive = false
594
      }
595 1
      this.selectedIndex = -1
596
    },
597 1
    onClick (e: MouseEvent) {
598 1
      if (!this.isInteractive) return
599

600 1
      if (!this.isAppendInner(e.target)) {
601 1
        this.isMenuActive = true
602
      }
603

604 1
      if (!this.isFocused) {
605 1
        this.isFocused = true
606 1
        this.$emit('focus')
607
      }
608

609 1
      this.$emit('click', e)
610
    },
611 1
    onEscDown (e: Event) {
612 1
      e.preventDefault()
613 1
      if (this.isMenuActive) {
614 1
        e.stopPropagation()
615 1
        this.isMenuActive = false
616
      }
617
    },
618 1
    onKeyPress (e: KeyboardEvent) {
619 1
      if (
620 1
        this.multiple ||
621 1
        !this.isInteractive ||
622 1
        this.disableLookup
623 1
      ) return
624

625 1
      const KEYBOARD_LOOKUP_THRESHOLD = 1000 // milliseconds
626 1
      const now = performance.now()
627 1
      if (now - this.keyboardLookupLastTime > KEYBOARD_LOOKUP_THRESHOLD) {
628 0
        this.keyboardLookupPrefix = ''
629
      }
630 1
      this.keyboardLookupPrefix += e.key.toLowerCase()
631 1
      this.keyboardLookupLastTime = now
632

633 1
      const index = this.allItems.findIndex(item => {
634 1
        const text = (this.getText(item) || '').toString()
635

636 1
        return text.toLowerCase().startsWith(this.keyboardLookupPrefix)
637
      })
638 1
      const item = this.allItems[index]
639 1
      if (index !== -1) {
640 1
        this.lastItem = Math.max(this.lastItem, index + 5)
641 1
        this.setValue(this.returnObject ? item : this.getValue(item))
642 1
        this.$nextTick(() => this.$refs.menu.getTiles())
643 1
        setTimeout(() => this.setMenuIndex(index))
644
      }
645
    },
646 1
    onKeyDown (e: KeyboardEvent) {
647 1
      if (this.isReadonly && e.keyCode !== keyCodes.tab) return
648

649 1
      const keyCode = e.keyCode
650 1
      const menu = this.$refs.menu
651

652
      // If enter, space, open menu
653 1
      if ([
654
        keyCodes.enter,
655
        keyCodes.space,
656 1
      ].includes(keyCode)) this.activateMenu()
657

658 1
      this.$emit('keydown', e)
659

660 1
      if (!menu) return
661

662
      // If menu is active, allow default
663
      // listIndex change from menu
664 1
      if (this.isMenuActive && keyCode !== keyCodes.tab) {
665 1
        this.$nextTick(() => {
666 1
          menu.changeListIndex(e)
667 1
          this.$emit('update:list-index', menu.listIndex)
668
        })
669
      }
670

671
      // If menu is not active, up and down can do
672
      // one of 2 things. If multiple, opens the
673
      // menu, if not, will cycle through all
674
      // available options
675 1
      if (
676 1
        !this.isMenuActive &&
677 1
        [keyCodes.up, keyCodes.down].includes(keyCode)
678 1
      ) return this.onUpDown(e)
679

680
      // If escape deactivate the menu
681 1
      if (keyCode === keyCodes.esc) return this.onEscDown(e)
682

683
      // If tab - select item or close menu
684 1
      if (keyCode === keyCodes.tab) return this.onTabDown(e)
685

686
      // If space preventDefault
687 1
      if (keyCode === keyCodes.space) return this.onSpaceDown(e)
688
    },
689 1
    onMenuActiveChange (val: boolean) {
690
      // If menu is closing and mulitple
691
      // or menuIndex is already set
692
      // skip menu index recalculation
693 1
      if (
694 1
        (this.multiple && !val) ||
695 1
        this.getMenuIndex() > -1
696 0
      ) return
697

698 1
      const menu = this.$refs.menu
699

700 1
      if (!menu || !this.isDirty) return
701

702
      // When menu opens, set index of first active item
703 1
      for (let i = 0; i < menu.tiles.length; i++) {
704 1
        if (menu.tiles[i].getAttribute('aria-selected') === 'true') {
705 0
          this.setMenuIndex(i)
706 0
          break
707
        }
708
      }
709
    },
710 1
    onMouseUp (e: MouseEvent) {
711
      // eslint-disable-next-line sonarjs/no-collapsible-if
712 1
      if (
713 1
        this.hasMouseDown &&
714 1
        e.which !== 3 &&
715 1
        this.isInteractive
716
      ) {
717
        // If append inner is present
718
        // and the target is itself
719
        // or inside, toggle menu
720 1
        if (this.isAppendInner(e.target)) {
721 1
          this.$nextTick(() => (this.isMenuActive = !this.isMenuActive))
722
        }
723
      }
724

725 1
      VTextField.options.methods.onMouseUp.call(this, e)
726
    },
727 0
    onScroll () {
728 1
      if (!this.isMenuActive) {
729 0
        requestAnimationFrame(() => (this.getContent().scrollTop = 0))
730
      } else {
731 1
        if (this.lastItem > this.computedItems.length) return
732

733 0
        const showMoreItems = (
734
          this.getContent().scrollHeight -
735
          (this.getContent().scrollTop +
736
          this.getContent().clientHeight)
737
        ) < 200
738

739 1
        if (showMoreItems) {
740 0
          this.lastItem += 20
741
        }
742
      }
743
    },
744 1
    onSpaceDown (e: KeyboardEvent) {
745 1
      e.preventDefault()
746
    },
747 1
    onTabDown (e: KeyboardEvent) {
748 1
      const menu = this.$refs.menu
749

750 1
      if (!menu) return
751

752 1
      const activeTile = menu.activeTile
753

754
      // An item that is selected by
755
      // menu-index should toggled
756 1
      if (
757 1
        !this.multiple &&
758 1
        activeTile &&
759 0
        this.isMenuActive
760
      ) {
761 0
        e.preventDefault()
762 0
        e.stopPropagation()
763

764 0
        activeTile.click()
765
      } else {
766
        // If we make it here,
767
        // the user has no selected indexes
768
        // and is probably tabbing out
769 1
        this.blur(e)
770
      }
771
    },
772 1
    onUpDown (e: KeyboardEvent) {
773 1
      const menu = this.$refs.menu
774

775 1
      if (!menu) return
776

777 1
      e.preventDefault()
778

779
      // Multiple selects do not cycle their value
780
      // when pressing up or down, instead activate
781
      // the menu
782 1
      if (this.multiple) return this.activateMenu()
783

784 1
      const keyCode = e.keyCode
785

786
      // Cycle through available values to achieve
787
      // select native behavior
788 1
      menu.isBooted = true
789

790 1
      window.requestAnimationFrame(() => {
791 1
        menu.getTiles()
792 1
        keyCodes.up === keyCode ? menu.prevTile() : menu.nextTile()
793 1
        menu.activeTile && menu.activeTile.click()
794
      })
795
    },
796 1
    selectItem (item: object) {
797 1
      if (!this.multiple) {
798 1
        this.setValue(this.returnObject ? item : this.getValue(item))
799 1
        this.isMenuActive = false
800
      } else {
801 1
        const internalValue = (this.internalValue || []).slice()
802 1
        const i = this.findExistingIndex(item)
803

804 1
        i !== -1 ? internalValue.splice(i, 1) : internalValue.push(item)
805 1
        this.setValue(internalValue.map((i: object) => {
806 1
          return this.returnObject ? i : this.getValue(i)
807
        }))
808

809
        // When selecting multiple
810
        // adjust menu after each
811
        // selection
812 1
        this.$nextTick(() => {
813 1
          this.$refs.menu &&
814 1
            (this.$refs.menu as { [key: string]: any }).updateDimensions()
815
        })
816

817
        // We only need to reset list index for multiple
818
        // to keep highlight when an item is toggled
819
        // on and off
820 1
        if (!this.multiple) return
821

822 1
        const listIndex = this.getMenuIndex()
823

824 1
        this.setMenuIndex(-1)
825

826
        // There is no item to re-highlight
827
        // when selections are hidden
828 1
        if (this.hideSelected) return
829

830 1
        this.$nextTick(() => this.setMenuIndex(listIndex))
831
      }
832
    },
833 1
    setMenuIndex (index: number) {
834 1
      this.$refs.menu && ((this.$refs.menu as { [key: string]: any }).listIndex = index)
835
    },
836 1
    setSelectedItems () {
837 1
      const selectedItems = []
838 1
      const values = !this.multiple || !Array.isArray(this.internalValue)
839 1
        ? [this.internalValue]
840 1
        : this.internalValue
841

842 1
      for (const value of values) {
843 1
        const index = this.allItems.findIndex(v => this.valueComparator(
844
          this.getValue(v),
845
          this.getValue(value)
846
        ))
847

848 1
        if (index > -1) {
849 1
          selectedItems.push(this.allItems[index])
850
        }
851
      }
852

853 1
      this.selectedItems = selectedItems
854
    },
855 1
    setValue (value: any) {
856 1
      const oldValue = this.internalValue
857 1
      this.internalValue = value
858 1
      value !== oldValue && this.$emit('change', value)
859
    },
860 1
    isAppendInner (target: any) {
861
      // return true if append inner is present
862
      // and the target is itself or inside
863 1
      const appendInner = this.$refs['append-inner']
864

865 1
      return appendInner && (appendInner === target || appendInner.contains(target))
866
    },
867
  },
868
})

Read our documentation on viewing source code .

Loading