<commit_before>// Styles
import '../../stylus/components/_text-fields.styl'
import '../../stylus/components/_select.styl'

// Components
import VChip from '../VChip'
import VMenu from '../VMenu'
import VSelectList from './VSelectList'

// Extensions
import VTextField from '../VTextField/VTextField'

// Mixins
import Comparable from '../../mixins/comparable'
import Filterable from '../../mixins/filterable'
import Menuable from '../../mixins/menuable'

// Directives
import ClickOutside from '../../directives/click-outside'

// Helpers
import { camelize, getPropertyFromItem, keyCodes } from '../../util/helpers'
import { consoleError, consoleWarn } from '../../util/console'

export const defaultMenuProps = {
  closeOnClick: false,
  closeOnContentClick: false,
  openOnClick: false,
  maxHeight: 300
}

/* @vue/component */
export default {
  name: 'v-select',

  directives: {
    ClickOutside
  },

  extends: VTextField,

  mixins: [
    Comparable,
    Filterable
  ],

  props: {
    appendIcon: {
      type: String,
      default: '$vuetify.icons.dropdown'
    },
    appendIconCb: Function,
    attach: {
      type: null,
      default: false
    },
    auto: Boolean,
    browserAutocomplete: {
      type: String,
      default: 'on'
    },
    cacheItems: Boolean,
    chips: Boolean,
    clearable: Boolean,
    deletableChips: Boolean,
    dense: Boolean,
    hideSelected: Boolean,
    items: {
      type: Array,
      default: () => []
    },
    itemAvatar: {
      type: [String, Array, Function],
      default: 'avatar'
    },
    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,
    searchInput: {
      default: null
    },
    smallChips: Boolean
  },

  data: vm => ({
    attrsInput: { role: 'combobox' },
    cachedItems: vm.cacheItems ? vm.items : [],
    content: null,
    isBooted: 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: vm.value !== undefined
      ? vm.value
      : vm.multiple ? [] : undefined,
    selectedIndex: -1,
    selectedItems: []
  }),

  computed: {
    /* All items that the select has */
    allItems () {
      return this.filterDuplicates(this.cachedItems.concat(this.items))
    },
    classes () {
      return Object.assign({}, VTextField.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
      })
    },
    /* Used by other components to overwrite */
    computedItems () {
      return this.allItems
    },
    counterValue () {
      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 () {
      return {
        props: {
          action: this.multiple && !this.isHidingSelected,
          color: this.color,
          dense: this.dense,
          hideSelected: this.hideSelected,
          items: this.virtualizedItems,
          noDataText: this.$vuetify.t(this.noDataText),
          selectedItems: this.selectedItems,
          itemAvatar: this.itemAvatar,
          itemDisabled: this.itemDisabled,
          itemValue: this.itemValue,
          itemText: this.itemText
        },
        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 () { return true },
    $_menuProps () {
      let normalisedProps

      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,
        value: this.menuCanShow && this.isMenuActive,
        nudgeBottom: this.nudgeBottom
          ? this.nudgeBottom
          : normalisedProps.offsetY ? 1 : 0, // convert to int
        ...normalisedProps
      }
    }
  },

  watch: {
    internalValue (val) {
      this.initialValue = val
      this.$emit('change', this.internalValue)
      this.setSelectedItems()
    },
    isBooted () {
      this.$nextTick(() => {
        if (this.content && this.content.addEventListener) {
          this.content.addEventListener('scroll', this.onScroll, false)
        }
      })
    },
    isMenuActive (val) {
      if (!val) return

      this.isBooted = true
    },
    items: {
      immediate: true,
      handler (val) {
        if (this.cacheItems) {
          this.cachedItems = this.filterDuplicates(this.cachedItems.concat(val))
        }

        this.setSelectedItems()
      }
    }
  },

  mounted () {
    this.content = this.$refs.menu && this.$refs.menu.$refs.content
  },

  methods: {
    /** @public */
    blur () {
      this.isMenuActive = false
      this.isFocused = false
      this.$refs.input && this.$refs.input.blur()
      this.selectedIndex = -1
    },
    /** @public */
    activateMenu () {
      this.isMenuActive = true
    },
    clearableCallback () {
      this.internalValue = this.multiple ? [] : undefined
      this.$nextTick(() => this.$refs.input.focus())

      if (this.openOnClear) this.isMenuActive = true
    },
    closeConditional (e) {
      return (
        // Click originates from outside the menu content
        !!this.content &&
        !this.content.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))
    },
    genChipSelection (item, index) {
      const isDisabled = (
        this.disabled ||
        this.readonly ||
        this.getDisabled(item)
      )
      const focus = (e, cb) => {
        if (isDisabled) return

        e.stopPropagation()
        this.onFocus()
        cb && cb()
      }

      return this.$createElement(VChip, {
        staticClass: 'v-chip--select-multi',
        props: {
          close: this.deletableChips && !isDisabled,
          disabled: isDisabled,
          selected: index === this.selectedIndex,
          small: this.smallChips
        },
        on: {
          click: e => {
            focus(e, () => {
              this.selectedIndex = index
            })
          },
          focus,
          input: () => this.onChipInput(item)
        },
        key: this.getValue(item)
      }, this.getText(item))
    },
    genCommaSelection (item, index, last) {
      // Item may be an object
      // TODO: Remove JSON.stringify
      const key = JSON.stringify(this.getValue(item))
      const color = index === this.selectedIndex && this.color
      const isDisabled = (
        this.disabled ||
        this.readonly ||
        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
      }), `${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.$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.genMenu()
      ]
    },
    genInput () {
      const input = VTextField.methods.genInput.call(this)

      input.data.domProps.value = null
      input.data.attrs.readonly = true
      input.data.attrs['aria-readonly'] = String(this.readonly)

      return input
    },
    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']

      // Deprecate using menu props directly
      // TODO: remove (2.0)
      const inheritedProps = [...Object.keys(VMenu.options.props), ...Object.keys(Menuable.options.props)]

      const deprecatedProps = Object.keys(this.$attrs).reduce((acc, attr) => {
        if (inheritedProps.includes(camelize(attr))) acc.push(attr)
        return acc
      }, [])

      for (const prop of deprecatedProps) {
        props[camelize(prop)] = this.$attrs[prop]
      }

      // The warn process is a bit slow, so we won't do it in prod
      if (process.env.NODE_ENV !== 'production') {
        if (deprecatedProps.length) {
          const multiple = deprecatedProps.length > 1
          const replacement = JSON.stringify(deprecatedProps.reduce((acc, p) => {
            acc[camelize(p)] = this.$attrs[p]
            return acc
          }, {}), null, multiple ? 2 : 0).replace(/"([^(")"]+)":/g, '$1:').replace(/"/g, '\'')
          const props = deprecatedProps.map(p => `'${p}'`).join(', ')
          const separator = multiple ? '\n' : '\''

          consoleWarn(`${props} ${multiple ? 'are' : 'is'} deprecated, use ${separator}:menu-props="${replacement}"${separator} instead`, this)
        }
      }

      // 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))<commit_msg>fix(VSelect): remove 'auto' prop definition, update deprecation notice<commit_after>// Styles
import '../../stylus/components/_text-fields.styl'
import '../../stylus/components/_select.styl'

// Components
import VChip from '../VChip'
import VMenu from '../VMenu'
import VSelectList from './VSelectList'

// Extensions
import VTextField from '../VTextField/VTextField'

// Mixins
import Comparable from '../../mixins/comparable'
import Filterable from '../../mixins/filterable'

// Directives
import ClickOutside from '../../directives/click-outside'

// Helpers
import { camelize, getPropertyFromItem, keyCodes } from '../../util/helpers'
import { consoleError, consoleWarn } from '../../util/console'

export const defaultMenuProps = {
  closeOnClick: false,
  closeOnContentClick: false,
  openOnClick: false,
  maxHeight: 300
}

/* @vue/component */
export default {
  name: 'v-select',

  directives: {
    ClickOutside
  },

  extends: VTextField,

  mixins: [
    Comparable,
    Filterable
  ],

  props: {
    appendIcon: {
      type: String,
      default: '$vuetify.icons.dropdown'
    },
    appendIconCb: Function,
    attach: {
      type: null,
      default: false
    },
    browserAutocomplete: {
      type: String,
      default: 'on'
    },
    cacheItems: Boolean,
    chips: Boolean,
    clearable: Boolean,
    deletableChips: Boolean,
    dense: Boolean,
    hideSelected: Boolean,
    items: {
      type: Array,
      default: () => []
    },
    itemAvatar: {
      type: [String, Array, Function],
      default: 'avatar'
    },
    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,
    searchInput: {
      default: null
    },
    smallChips: Boolean
  },

  data: vm => ({
    attrsInput: { role: 'combobox' },
    cachedItems: vm.cacheItems ? vm.items : [],
    content: null,
    isBooted: 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: vm.value !== undefined
      ? vm.value
      : vm.multiple ? [] : undefined,
    selectedIndex: -1,
    selectedItems: []
  }),

  computed: {
    /* All items that the select has */
    allItems () {
      return this.filterDuplicates(this.cachedItems.concat(this.items))
    },
    classes () {
      return Object.assign({}, VTextField.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
      })
    },
    /* Used by other components to overwrite */
    computedItems () {
      return this.allItems
    },
    counterValue () {
      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 () {
      return {
        props: {
          action: this.multiple && !this.isHidingSelected,
          color: this.color,
          dense: this.dense,
          hideSelected: this.hideSelected,
          items: this.virtualizedItems,
          noDataText: this.$vuetify.t(this.noDataText),
          selectedItems: this.selectedItems,
          itemAvatar: this.itemAvatar,
          itemDisabled: this.itemDisabled,
          itemValue: this.itemValue,
          itemText: this.itemText
        },
        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 () { return true },
    $_menuProps () {
      let normalisedProps

      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,
        value: this.menuCanShow && this.isMenuActive,
        nudgeBottom: this.nudgeBottom
          ? this.nudgeBottom
          : normalisedProps.offsetY ? 1 : 0, // convert to int
        ...normalisedProps
      }
    }
  },

  watch: {
    internalValue (val) {
      this.initialValue = val
      this.$emit('change', this.internalValue)
      this.setSelectedItems()
    },
    isBooted () {
      this.$nextTick(() => {
        if (this.content && this.content.addEventListener) {
          this.content.addEventListener('scroll', this.onScroll, false)
        }
      })
    },
    isMenuActive (val) {
      if (!val) return

      this.isBooted = true
    },
    items: {
      immediate: true,
      handler (val) {
        if (this.cacheItems) {
          this.cachedItems = this.filterDuplicates(this.cachedItems.concat(val))
        }

        this.setSelectedItems()
      }
    }
  },

  mounted () {
    this.content = this.$refs.menu && this.$refs.menu.$refs.content
  },

  methods: {
    /** @public */
    blur () {
      this.isMenuActive = false
      this.isFocused = false
      this.$refs.input && this.$refs.input.blur()
      this.selectedIndex = -1
    },
    /** @public */
    activateMenu () {
      this.isMenuActive = true
    },
    clearableCallback () {
      this.internalValue = this.multiple ? [] : undefined
      this.$nextTick(() => this.$refs.input.focus())

      if (this.openOnClear) this.isMenuActive = true
    },
    closeConditional (e) {
      return (
        // Click originates from outside the menu content
        !!this.content &&
        !this.content.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))
    },
    genChipSelection (item, index) {
      const isDisabled = (
        this.disabled ||
        this.readonly ||
        this.getDisabled(item)
      )
      const focus = (e, cb) => {
        if (isDisabled) return

        e.stopPropagation()
        this.onFocus()
        cb && cb()
      }

      return this.$createElement(VChip, {
        staticClass: 'v-chip--select-multi',
        props: {
          close: this.deletableChips && !isDisabled,
          disabled: isDisabled,
          selected: index === this.selectedIndex,
          small: this.smallChips
        },
        on: {
          click: e => {
            focus(e, () => {
              this.selectedIndex = index
            })
          },
          focus,
          input: () => this.onChipInput(item)
        },
        key: this.getValue(item)
      }, this.getText(item))
    },
    genCommaSelection (item, index, last) {
      // Item may be an object
      // TODO: Remove JSON.stringify
      const key = JSON.stringify(this.getValue(item))
      const color = index === this.selectedIndex && this.color
      const isDisabled = (
        this.disabled ||
        this.readonly ||
        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
      }), `${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.$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.genMenu()
      ]
    },
    genInput () {
      const input = VTextField.methods.genInput.call(this)

      input.data.domProps.value = null
      input.data.attrs.readonly = true
      input.data.attrs['aria-readonly'] = String(this.readonly)

      return input
    },
    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']

      // Deprecate using menu props directly
      // TODO: remove (2.0)
      const inheritedProps = Object.keys(VMenu.options.props)

      const deprecatedProps = Object.keys(this.$attrs).reduce((acc, attr) => {
        if (inheritedProps.includes(camelize(attr))) acc.push(attr)
        return acc
      }, [])

      for (const prop of deprecatedProps) {
        props[camelize(prop)] = this.$attrs[prop]
      }

      if (process.env.NODE_ENV !== 'production') {
        if (deprecatedProps.length) {
          const multiple = deprecatedProps.length > 1
          let replacement = deprecatedProps.reduce((acc, p) => {
            acc[camelize(p)] = this.$attrs[p]
            return acc
          }, {})
          const props = deprecatedProps.map(p => `'${p}'`).join(', ')
          const separator = multiple ? '\n' : '\''

          const onlyBools = Object.keys(replacement).every(prop => {
            const propType = VMenu.options.props[prop]
            const value = replacement[prop]
            return value === true || ((propType.type || propType) === Boolean && value === '')
          })

          if (onlyBools) {
            replacement = Object.keys(replacement).join(', ')
          } else {
            replacement = JSON.stringify(replacement, null, multiple ? 2 : 0).replace(/"([^(")"]+)":/g, '$1:').replace(/"/g, '\'')
          }

          consoleWarn(`${props} ${multiple ? 'are' : 'is'} deprecated, use ${separator}:menu-props="${replacement}"${separator} instead`, this)
        }
      }

      // 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))
{'fraction_non_alphanumeric': 0.13906818448538905, 'fraction_numerical': 0.0014865234909830615, 'mean_word_length': 2.060823754789272, 'pattern_counts': {'":': 2, '<': 9, '<?xml version=': 0, '>': 44, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2470916', 'n_tokens_mistral': 7601, 'n_tokens_neox': 7457, 'n_words': 1626}