mirror of
				https://github.com/crater-invoice/crater.git
				synced 2025-10-30 21:21:09 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			723 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			723 lines
		
	
	
		
			19 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| function isEmpty (opt) {
 | ||
|   if (opt === 0) return false
 | ||
|   if (Array.isArray(opt) && opt.length === 0) return true
 | ||
|   return !opt
 | ||
| }
 | ||
| 
 | ||
| function not (fun) {
 | ||
|   return (...params) => !fun(...params)
 | ||
| }
 | ||
| 
 | ||
| function includes (str, query) {
 | ||
|   /* istanbul ignore else */
 | ||
|   if (str === undefined) str = 'undefined'
 | ||
|   if (str === null) str = 'null'
 | ||
|   if (str === false) str = 'false'
 | ||
|   const text = str.toString().toLowerCase()
 | ||
|   return text.indexOf(query.trim()) !== -1
 | ||
| }
 | ||
| 
 | ||
| function filterOptions (options, search, label, customLabel) {
 | ||
|   return options.filter(option => includes(customLabel(option, label), search))
 | ||
| }
 | ||
| 
 | ||
| function stripGroups (options) {
 | ||
|   return options.filter(option => !option.$isLabel)
 | ||
| }
 | ||
| 
 | ||
| function flattenOptions (values, label) {
 | ||
|   return (options) =>
 | ||
|     options.reduce((prev, curr) => {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (curr[values] && curr[values].length) {
 | ||
|         prev.push({
 | ||
|           $groupLabel: curr[label],
 | ||
|           $isLabel: true
 | ||
|         })
 | ||
|         return prev.concat(curr[values])
 | ||
|       }
 | ||
|       return prev
 | ||
|     }, [])
 | ||
| }
 | ||
| 
 | ||
| function filterGroups (search, label, values, groupLabel, customLabel) {
 | ||
|   return (groups) =>
 | ||
|     groups.map(group => {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (!group[values]) {
 | ||
|         console.warn(`Options passed to vue-multiselect do not contain groups, despite the config.`)
 | ||
|         return []
 | ||
|       }
 | ||
|       const groupOptions = filterOptions(group[values], search, label, customLabel)
 | ||
| 
 | ||
|       return groupOptions.length
 | ||
|         ? {
 | ||
|           [groupLabel]: group[groupLabel],
 | ||
|           [values]: groupOptions
 | ||
|         }
 | ||
|         : []
 | ||
|     })
 | ||
| }
 | ||
| 
 | ||
| const flow = (...fns) => x => fns.reduce((v, f) => f(v), x)
 | ||
| 
 | ||
| export default {
 | ||
|   data () {
 | ||
|     return {
 | ||
|       search: '',
 | ||
|       isOpen: false,
 | ||
|       preferredOpenDirection: 'below',
 | ||
|       optimizedHeight: this.maxHeight
 | ||
|     }
 | ||
|   },
 | ||
|   props: {
 | ||
|     initialSearch: {
 | ||
|       type: String,
 | ||
|       default: ''
 | ||
|     },
 | ||
|     /**
 | ||
|      * Decide whether to filter the results based on search query.
 | ||
|      * Useful for async filtering, where we search through more complex data.
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     internalSearch: {
 | ||
|       type: Boolean,
 | ||
|       default: true
 | ||
|     },
 | ||
|     /**
 | ||
|      * Array of available options: Objects, Strings or Integers.
 | ||
|      * If array of objects, visible label will default to option.label.
 | ||
|      * If `labal` prop is passed, label will equal option['label']
 | ||
|      * @type {Array}
 | ||
|      */
 | ||
|     options: {
 | ||
|       type: Array,
 | ||
|       required: true
 | ||
|     },
 | ||
|     /**
 | ||
|      * Equivalent to the `multiple` attribute on a `<select>` input.
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     multiple: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * Presets the selected options value.
 | ||
|      * @type {Object||Array||String||Integer}
 | ||
|      */
 | ||
|     value: {
 | ||
|       type: null,
 | ||
|       default () {
 | ||
|         return []
 | ||
|       }
 | ||
|     },
 | ||
|     /**
 | ||
|      * Key to compare objects
 | ||
|      * @default 'id'
 | ||
|      * @type {String}
 | ||
|      */
 | ||
|     trackBy: {
 | ||
|       type: String
 | ||
|     },
 | ||
|     /**
 | ||
|      * Label to look for in option Object
 | ||
|      * @default 'label'
 | ||
|      * @type {String}
 | ||
|      */
 | ||
|     label: {
 | ||
|       type: String
 | ||
|     },
 | ||
|     /**
 | ||
|      * Enable/disable search in options
 | ||
|      * @default true
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     searchable: {
 | ||
|       type: Boolean,
 | ||
|       default: true
 | ||
|     },
 | ||
|     /**
 | ||
|      * Clear the search input after `)
 | ||
|      * @default true
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     clearOnSelect: {
 | ||
|       type: Boolean,
 | ||
|       default: true
 | ||
|     },
 | ||
|     /**
 | ||
|      * Hide already selected options
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     hideSelected: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * Equivalent to the `placeholder` attribute on a `<select>` input.
 | ||
|      * @default 'Select option'
 | ||
|      * @type {String}
 | ||
|      */
 | ||
|     placeholder: {
 | ||
|       type: String,
 | ||
|       default: 'Select option'
 | ||
|     },
 | ||
|     /**
 | ||
|      * Allow to remove all selected values
 | ||
|      * @default true
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     allowEmpty: {
 | ||
|       type: Boolean,
 | ||
|       default: true
 | ||
|     },
 | ||
|     /**
 | ||
|      * Reset this.internalValue, this.search after this.internalValue changes.
 | ||
|      * Useful if want to create a stateless dropdown.
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     resetAfter: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * Enable/disable closing after selecting an option
 | ||
|      * @default true
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     closeOnSelect: {
 | ||
|       type: Boolean,
 | ||
|       default: true
 | ||
|     },
 | ||
|     /**
 | ||
|      * Function to interpolate the custom label
 | ||
|      * @default false
 | ||
|      * @type {Function}
 | ||
|      */
 | ||
|     customLabel: {
 | ||
|       type: Function,
 | ||
|       default (option, label) {
 | ||
|         if (isEmpty(option)) return ''
 | ||
|         return label ? option[label] : option
 | ||
|       }
 | ||
|     },
 | ||
|     /**
 | ||
|      * Disable / Enable tagging
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     taggable: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * String to show when highlighting a potential tag
 | ||
|      * @default 'Press enter to create a tag'
 | ||
|      * @type {String}
 | ||
|     */
 | ||
|     tagPlaceholder: {
 | ||
|       type: String,
 | ||
|       default: 'Press enter to create a tag'
 | ||
|     },
 | ||
|     /**
 | ||
|      * By default new tags will appear above the search results.
 | ||
|      * Changing to 'bottom' will revert this behaviour
 | ||
|      * and will proritize the search results
 | ||
|      * @default 'top'
 | ||
|      * @type {String}
 | ||
|     */
 | ||
|     tagPosition: {
 | ||
|       type: String,
 | ||
|       default: 'top'
 | ||
|     },
 | ||
|     /**
 | ||
|      * Number of allowed selected options. No limit if 0.
 | ||
|      * @default 0
 | ||
|      * @type {Number}
 | ||
|     */
 | ||
|     max: {
 | ||
|       type: [Number, Boolean],
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * Will be passed with all events as second param.
 | ||
|      * Useful for identifying events origin.
 | ||
|      * @default null
 | ||
|      * @type {String|Integer}
 | ||
|     */
 | ||
|     id: {
 | ||
|       default: null
 | ||
|     },
 | ||
|     /**
 | ||
|      * Limits the options displayed in the dropdown
 | ||
|      * to the first X options.
 | ||
|      * @default 1000
 | ||
|      * @type {Integer}
 | ||
|     */
 | ||
|     optionsLimit: {
 | ||
|       type: Number,
 | ||
|       default: 1000
 | ||
|     },
 | ||
|     /**
 | ||
|      * Name of the property containing
 | ||
|      * the group values
 | ||
|      * @default 1000
 | ||
|      * @type {String}
 | ||
|     */
 | ||
|     groupValues: {
 | ||
|       type: String
 | ||
|     },
 | ||
|     /**
 | ||
|      * Name of the property containing
 | ||
|      * the group label
 | ||
|      * @default 1000
 | ||
|      * @type {String}
 | ||
|     */
 | ||
|     groupLabel: {
 | ||
|       type: String
 | ||
|     },
 | ||
|     /**
 | ||
|      * Allow to select all group values
 | ||
|      * by selecting the group label
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|      */
 | ||
|     groupSelect: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * Array of keyboard keys to block
 | ||
|      * when selecting
 | ||
|      * @default 1000
 | ||
|      * @type {String}
 | ||
|     */
 | ||
|     blockKeys: {
 | ||
|       type: Array,
 | ||
|       default () {
 | ||
|         return []
 | ||
|       }
 | ||
|     },
 | ||
|     /**
 | ||
|      * Prevent from wiping up the search value
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|     */
 | ||
|     preserveSearch: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     },
 | ||
|     /**
 | ||
|      * Select 1st options if value is empty
 | ||
|      * @default false
 | ||
|      * @type {Boolean}
 | ||
|     */
 | ||
|     preselectFirst: {
 | ||
|       type: Boolean,
 | ||
|       default: false
 | ||
|     }
 | ||
|   },
 | ||
|   mounted () {
 | ||
|     /* istanbul ignore else */
 | ||
|     if (!this.multiple && this.max) {
 | ||
|       console.warn('[Vue-Multiselect warn]: Max prop should not be used when prop Multiple equals false.')
 | ||
|     }
 | ||
|     if (
 | ||
|       this.preselectFirst &&
 | ||
|       !this.internalValue.length &&
 | ||
|       this.options.length
 | ||
|     ) {
 | ||
|       this.select(this.filteredOptions[0])
 | ||
|     }
 | ||
| 
 | ||
|     if (this.initialSearch) {
 | ||
|       this.search = this.initialSearch
 | ||
|     }
 | ||
|   },
 | ||
|   computed: {
 | ||
|     internalValue () {
 | ||
|       return this.value || this.value === 0
 | ||
|         ? Array.isArray(this.value) ? this.value : [this.value]
 | ||
|         : []
 | ||
|     },
 | ||
|     filteredOptions () {
 | ||
|       const search = this.search || ''
 | ||
|       const normalizedSearch = search.toLowerCase().trim()
 | ||
| 
 | ||
|       let options = this.options.concat()
 | ||
| 
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.internalSearch) {
 | ||
|         options = this.groupValues
 | ||
|           ? this.filterAndFlat(options, normalizedSearch, this.label)
 | ||
|           : filterOptions(options, normalizedSearch, this.label, this.customLabel)
 | ||
|       } else {
 | ||
|         options = this.groupValues ? flattenOptions(this.groupValues, this.groupLabel)(options) : options
 | ||
|       }
 | ||
| 
 | ||
|       options = this.hideSelected
 | ||
|         ? options.filter(not(this.isSelected))
 | ||
|         : options
 | ||
| 
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.taggable && normalizedSearch.length && !this.isExistingOption(normalizedSearch)) {
 | ||
|         if (this.tagPosition === 'bottom') {
 | ||
|           options.push({ isTag: true, label: search })
 | ||
|         } else {
 | ||
|           options.unshift({ isTag: true, label: search })
 | ||
|         }
 | ||
|       }
 | ||
| 
 | ||
|       return options.slice(0, this.optionsLimit)
 | ||
|     },
 | ||
|     valueKeys () {
 | ||
|       if (this.trackBy) {
 | ||
|         return this.internalValue.map(element => element[this.trackBy])
 | ||
|       } else {
 | ||
|         return this.internalValue
 | ||
|       }
 | ||
|     },
 | ||
|     optionKeys () {
 | ||
|       const options = this.groupValues ? this.flatAndStrip(this.options) : this.options
 | ||
|       return options.map(element => this.customLabel(element, this.label).toString().toLowerCase())
 | ||
|     },
 | ||
|     currentOptionLabel () {
 | ||
|       return this.multiple
 | ||
|         ? this.searchable ? '' : this.placeholder
 | ||
|         : this.internalValue.length
 | ||
|           ? this.getOptionLabel(this.internalValue[0])
 | ||
|           : this.searchable ? '' : this.placeholder
 | ||
|     }
 | ||
|   },
 | ||
|   watch: {
 | ||
|     internalValue () {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.resetAfter && this.internalValue.length) {
 | ||
|         this.search = ''
 | ||
|         this.$emit('input', this.multiple ? [] : null)
 | ||
|       }
 | ||
|     },
 | ||
|     search () {
 | ||
|       this.$emit('search-change', this.search, this.id)
 | ||
|     }
 | ||
|   },
 | ||
|   methods: {
 | ||
|     /**
 | ||
|      * Returns the internalValue in a way it can be emited to the parent
 | ||
|      * @returns {Object||Array||String||Integer}
 | ||
|      */
 | ||
|     getValue () {
 | ||
|       return this.multiple
 | ||
|         ? this.internalValue
 | ||
|         : this.internalValue.length === 0
 | ||
|           ? null
 | ||
|           : this.internalValue[0]
 | ||
|     },
 | ||
|     /**
 | ||
|      * Filters and then flattens the options list
 | ||
|      * @param  {Array}
 | ||
|      * @returns {Array} returns a filtered and flat options list
 | ||
|      */
 | ||
|     filterAndFlat (options, search, label) {
 | ||
|       return flow(
 | ||
|         filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
 | ||
|         flattenOptions(this.groupValues, this.groupLabel)
 | ||
|       )(options)
 | ||
|     },
 | ||
|     /**
 | ||
|      * Flattens and then strips the group labels from the options list
 | ||
|      * @param  {Array}
 | ||
|      * @returns {Array} returns a flat options list without group labels
 | ||
|      */
 | ||
|     flatAndStrip (options) {
 | ||
|       return flow(
 | ||
|         flattenOptions(this.groupValues, this.groupLabel),
 | ||
|         stripGroups
 | ||
|       )(options)
 | ||
|     },
 | ||
|     /**
 | ||
|      * Updates the search value
 | ||
|      * @param  {String}
 | ||
|      */
 | ||
|     updateSearch (query) {
 | ||
|       this.search = query
 | ||
|       this.$emit('value', this.search)
 | ||
|     },
 | ||
|     /**
 | ||
|      * Finds out if the given query is already present
 | ||
|      * in the available options
 | ||
|      * @param  {String}
 | ||
|      * @returns {Boolean} returns true if element is available
 | ||
|      */
 | ||
|     isExistingOption (query) {
 | ||
|       return !this.options
 | ||
|         ? false
 | ||
|         : this.optionKeys.indexOf(query) > -1
 | ||
|     },
 | ||
|     /**
 | ||
|      * Finds out if the given element is already present
 | ||
|      * in the result value
 | ||
|      * @param  {Object||String||Integer} option passed element to check
 | ||
|      * @returns {Boolean} returns true if element is selected
 | ||
|      */
 | ||
|     isSelected (option) {
 | ||
|       const opt = this.trackBy
 | ||
|         ? option[this.trackBy]
 | ||
|         : option
 | ||
|       return this.valueKeys.indexOf(opt) > -1
 | ||
|     },
 | ||
|     /**
 | ||
|      * Finds out if the given option is disabled
 | ||
|      * @param  {Object||String||Integer} option passed element to check
 | ||
|      * @returns {Boolean} returns true if element is disabled
 | ||
|      */
 | ||
|     isOptionDisabled (option) {
 | ||
|       return !!option.$isDisabled
 | ||
|     },
 | ||
|     /**
 | ||
|      * Returns empty string when options is null/undefined
 | ||
|      * Returns tag query if option is tag.
 | ||
|      * Returns the customLabel() results and casts it to string.
 | ||
|      *
 | ||
|      * @param  {Object||String||Integer} Passed option
 | ||
|      * @returns {Object||String}
 | ||
|      */
 | ||
|     getOptionLabel (option) {
 | ||
|       if (isEmpty(option)) return ''
 | ||
|       /* istanbul ignore else */
 | ||
|       if (option.isTag) return option.label
 | ||
|       /* istanbul ignore else */
 | ||
|       if (option.$isLabel) return option.$groupLabel
 | ||
| 
 | ||
|       let label = this.customLabel(option, this.label)
 | ||
|       /* istanbul ignore else */
 | ||
|       if (isEmpty(label)) return ''
 | ||
|       return label
 | ||
|     },
 | ||
|     /**
 | ||
|      * Add the given option to the list of selected options
 | ||
|      * or sets the option as the selected option.
 | ||
|      * If option is already selected -> remove it from the results.
 | ||
|      *
 | ||
|      * @param  {Object||String||Integer} option to select/deselect
 | ||
|      * @param  {Boolean} block removing
 | ||
|      */
 | ||
|     select (option, key) {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (option.$isLabel && this.groupSelect) {
 | ||
|         this.selectGroup(option)
 | ||
|         return
 | ||
|       }
 | ||
|       if (this.blockKeys.indexOf(key) !== -1 ||
 | ||
|         this.disabled ||
 | ||
|         option.$isDisabled ||
 | ||
|         option.$isLabel
 | ||
|       ) return
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.max && this.multiple && this.internalValue.length === this.max) return
 | ||
|       /* istanbul ignore else */
 | ||
|       if (key === 'Tab' && !this.pointerDirty) return
 | ||
|       if (option.isTag) {
 | ||
|         this.$emit('tag', option.label, this.id)
 | ||
|         this.search = ''
 | ||
|         if (this.closeOnSelect && !this.multiple) this.deactivate()
 | ||
|       } else {
 | ||
|         const isSelected = this.isSelected(option)
 | ||
| 
 | ||
|         if (isSelected) {
 | ||
|           if (key !== 'Tab') this.removeElement(option)
 | ||
|           return
 | ||
|         }
 | ||
| 
 | ||
|         this.$emit('select', option, this.id)
 | ||
| 
 | ||
|         if (this.multiple) {
 | ||
|           this.$emit('input', this.internalValue.concat([option]), this.id)
 | ||
|         } else {
 | ||
|           this.$emit('input', option, this.id)
 | ||
|         }
 | ||
| 
 | ||
|         /* istanbul ignore else */
 | ||
|         if (this.clearOnSelect) this.search = ''
 | ||
|       }
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.closeOnSelect) this.deactivate()
 | ||
|     },
 | ||
|     /**
 | ||
|      * Add the given group options to the list of selected options
 | ||
|      * If all group optiona are already selected -> remove it from the results.
 | ||
|      *
 | ||
|      * @param  {Object||String||Integer} group to select/deselect
 | ||
|      */
 | ||
|     selectGroup (selectedGroup) {
 | ||
|       const group = this.options.find(option => {
 | ||
|         return option[this.groupLabel] === selectedGroup.$groupLabel
 | ||
|       })
 | ||
| 
 | ||
|       if (!group) return
 | ||
| 
 | ||
|       if (this.wholeGroupSelected(group)) {
 | ||
|         this.$emit('remove', group[this.groupValues], this.id)
 | ||
| 
 | ||
|         const newValue = this.internalValue.filter(
 | ||
|           option => group[this.groupValues].indexOf(option) === -1
 | ||
|         )
 | ||
| 
 | ||
|         this.$emit('input', newValue, this.id)
 | ||
|       } else {
 | ||
|         const optionsToAdd = group[this.groupValues].filter(
 | ||
|           option => !(this.isOptionDisabled(option) || this.isSelected(option))
 | ||
|         )
 | ||
| 
 | ||
|         this.$emit('select', optionsToAdd, this.id)
 | ||
|         this.$emit(
 | ||
|           'input',
 | ||
|           this.internalValue.concat(optionsToAdd),
 | ||
|           this.id
 | ||
|         )
 | ||
|       }
 | ||
|     },
 | ||
|     /**
 | ||
|      * Helper to identify if all values in a group are selected
 | ||
|      *
 | ||
|      * @param {Object} group to validated selected values against
 | ||
|      */
 | ||
|     wholeGroupSelected (group) {
 | ||
|       return group[this.groupValues].every(option => this.isSelected(option) || this.isOptionDisabled(option)
 | ||
|       )
 | ||
|     },
 | ||
|     /**
 | ||
|      * Helper to identify if all values in a group are disabled
 | ||
|      *
 | ||
|      * @param {Object} group to check for disabled values
 | ||
|      */
 | ||
|     wholeGroupDisabled (group) {
 | ||
|       return group[this.groupValues].every(this.isOptionDisabled)
 | ||
|     },
 | ||
|     /**
 | ||
|      * Removes the given option from the selected options.
 | ||
|      * Additionally checks this.allowEmpty prop if option can be removed when
 | ||
|      * it is the last selected option.
 | ||
|      *
 | ||
|      * @param  {type} option description
 | ||
|      * @returns {type}        description
 | ||
|      */
 | ||
|     removeElement (option, shouldClose = true) {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.disabled) return
 | ||
|       /* istanbul ignore else */
 | ||
|       if (option.$isDisabled) return
 | ||
|       /* istanbul ignore else */
 | ||
|       if (!this.allowEmpty && this.internalValue.length <= 1) {
 | ||
|         this.deactivate()
 | ||
|         return
 | ||
|       }
 | ||
| 
 | ||
|       const index = typeof option === 'object'
 | ||
|         ? this.valueKeys.indexOf(option[this.trackBy])
 | ||
|         : this.valueKeys.indexOf(option)
 | ||
| 
 | ||
|       this.$emit('remove', option, this.id)
 | ||
|       if (this.multiple) {
 | ||
|         const newValue = this.internalValue.slice(0, index).concat(this.internalValue.slice(index + 1))
 | ||
|         this.$emit('input', newValue, this.id)
 | ||
|       } else {
 | ||
|         this.$emit('input', null, this.id)
 | ||
|       }
 | ||
| 
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.closeOnSelect && shouldClose) this.deactivate()
 | ||
|     },
 | ||
|     /**
 | ||
|      * Calls this.removeElement() with the last element
 | ||
|      * from this.internalValue (selected element Array)
 | ||
|      *
 | ||
|      * @fires this#removeElement
 | ||
|      */
 | ||
|     removeLastElement () {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.blockKeys.indexOf('Delete') !== -1) return
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.search.length === 0 && Array.isArray(this.internalValue) && this.internalValue.length) {
 | ||
|         this.removeElement(this.internalValue[this.internalValue.length - 1], false)
 | ||
|       }
 | ||
|     },
 | ||
|     /**
 | ||
|      * Opens the multiselect’s dropdown.
 | ||
|      * Sets this.isOpen to TRUE
 | ||
|      */
 | ||
|     activate () {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (this.isOpen || this.disabled) return
 | ||
| 
 | ||
|       this.adjustPosition()
 | ||
|       /* istanbul ignore else  */
 | ||
|       if (this.groupValues && this.pointer === 0 && this.filteredOptions.length) {
 | ||
|         this.pointer = 1
 | ||
|       }
 | ||
| 
 | ||
|       this.isOpen = true
 | ||
|       /* istanbul ignore else  */
 | ||
|       if (this.searchable) {
 | ||
|         if (!this.preserveSearch) this.search = ''
 | ||
|         this.$nextTick(() => this.$refs.search && this.$refs.search.focus())
 | ||
|       } else {
 | ||
|         this.$el.focus()
 | ||
|       }
 | ||
|       this.$emit('open', this.id)
 | ||
|     },
 | ||
|     /**
 | ||
|      * Closes the multiselect’s dropdown.
 | ||
|      * Sets this.isOpen to FALSE
 | ||
|      */
 | ||
|     deactivate () {
 | ||
|       /* istanbul ignore else */
 | ||
|       if (!this.isOpen) return
 | ||
|       this.isOpen = false
 | ||
|       /* istanbul ignore else  */
 | ||
|       if (this.searchable) {
 | ||
|         this.$refs.search && this.$refs.search.blur()
 | ||
|       } else {
 | ||
|         this.$el.blur()
 | ||
|       }
 | ||
|       if (!this.preserveSearch) this.search = ''
 | ||
|       this.$emit('close', this.getValue(), this.id)
 | ||
|     },
 | ||
|     /**
 | ||
|      * Call this.activate() or this.deactivate()
 | ||
|      * depending on this.isOpen value.
 | ||
|      *
 | ||
|      * @fires this#activate || this#deactivate
 | ||
|      * @property {Boolean} isOpen indicates if dropdown is open
 | ||
|      */
 | ||
|     toggle () {
 | ||
|       this.isOpen
 | ||
|         ? this.deactivate()
 | ||
|         : this.activate()
 | ||
|     },
 | ||
|     /**
 | ||
|      * Updates the hasEnoughSpace variable used for
 | ||
|      * detecting where to expand the dropdown
 | ||
|      */
 | ||
|     adjustPosition () {
 | ||
|       if (typeof window === 'undefined') return
 | ||
| 
 | ||
|       const spaceAbove = this.$el.getBoundingClientRect().top
 | ||
|       const spaceBelow = window.innerHeight - this.$el.getBoundingClientRect().bottom
 | ||
|       const hasEnoughSpaceBelow = spaceBelow > this.maxHeight
 | ||
| 
 | ||
|       if (hasEnoughSpaceBelow || spaceBelow > spaceAbove || this.openDirection === 'below' || this.openDirection === 'bottom') {
 | ||
|         this.preferredOpenDirection = 'below'
 | ||
|         this.optimizedHeight = Math.min(spaceBelow - 40, this.maxHeight)
 | ||
|       } else {
 | ||
|         this.preferredOpenDirection = 'above'
 | ||
|         this.optimizedHeight = Math.min(spaceAbove - 40, this.maxHeight)
 | ||
|       }
 | ||
|     }
 | ||
|   }
 | ||
| }
 |